Compare commits

..

7 Commits

Author SHA1 Message Date
Jesse Mazzella
b117890f27 feat(ElementItem): optionally hide grippy 2023-01-17 20:53:45 -08:00
Shefali Joshi
afc37209d2 Allow customizing multiple y axes (#6095)
* Allow displaying and updating multiple y axes

* Fix label updates when series are added or removed

* Add tests for y axes configuration in the inspector

* Addresses review comments: Renames properties for clarity and reorganize code for readability.

* test: fix selector for snapshot test

* Fix e2e selectors

Co-authored-by: Khalid Adil <khalidadil29@gmail.com>
Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2023-01-17 10:09:56 -08:00
Jesse Mazzella
e45b58e2a8 feat(MultiYAxis): allow drag/drop of series directly into YAxis buckets (#6099)
* feat: allow dragging series directly into yAxis buckets

* fix: insert into first YAxis elements list
2023-01-12 10:49:00 -08:00
Shefali Joshi
8b3487bdbe Fix y axes rendering and style for 3rd y axis (#6081)
* Update plot to render y axes based on which series are added to the y axes, also add style to render third axis to the right of plots main canvas

* Clean up hardcoded numbers for consts and use objects for styles

* Rename parameter for readability

Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
2022-12-29 13:50:58 -08:00
Jesse Mazzella
50719b1383 [Plot] Allow bucketing of plot series into different Y Axes (#5878)
* WIP

* [Plot] Set yKey on each plot series (#5790)

* Set yKey on each model

* Add a test

* WIP 2: Electric Boogaloo

* Add uuid to YAxisModel

* determine dragover status for element group

* styling for elements group

* Drag and drop between groups, WIP on applySearch

* fix error

* Persist YAxisId on composition

* update yAxisId in cache

* cleanup

* remove unused import

* remove console.log

* mutate yAxisId whether or not it existed already

* Use null coalescing operator

* Cleanup some more

- remove unused vars

- rename vars

- add comments

* Handle when domainObject info is not set on the drag event

* Use constants

* cleanup, fix reorder index logic

* remove unused uuid

* more cleanup, add some comments

* Rename test file

* Clean up existing e2e test with our new patterns

* Simplify code and fix off-by-one index problem

* Add aria-label to element group

* Add test for elements pool reordering

* refactor: remove empty components object

* Draft: add 3rd y axis to the right of plots

* Fix CSS for left and right y axes making them more generic

* refactor: move logic to method

* fix: calculated --> computed 🤦‍♂️

* fix(WIP): initial work for dynamic yAxes

* fix(WIP): generic logic for reordering elements

* fix: calculate from/to indexes correctly

* refactor: remove unused constants

* refactor: use `areIdsEqual` API method

* refactor: remove redundant check

* fix: restore code removed by accident

* Remove y-axes bug fixes from elements pool changes.

* fix: make v-for key more unique

* fix: code review comments

- Only mutate domainObject if yAxisId has changed

- Initialize elements with blank arrays

- Only call `setYAxisIds` if `parentObject` is not null

Co-authored-by: Khalid Adil <khalidadil29@gmail.com>
Co-authored-by: Shefali <simplyrender@gmail.com>
2022-12-27 16:02:48 -08:00
Shefali Joshi
cfda067794 Multiple y axes model changes for plots (#6026)
* Update version for sprint 2.1.1

* Basic changes (very draft) for multiple y axis for plots

* Generalize additional axes model handling

* [Plot] Set yKey on each plot series (#5790)

* Set yKey on each model

* Add a test

* Update yaxis to be independent of mctplot

* Fix yAxis ticks

* Fix yAxisModel label

* Add multiple y axes option for overlay plot. UI changes to display 2 y-axes

* Fix Chart bug

* Bump version to `2.1.3` (#5973)

* Preserve Gauge configuration changes on create/edit (#5986)

* fix(#5985): deep merge on create/edit properties

- Perform a deep merge of old and new properties on Create/Edit properties actions

* refactor(e2e): improve selector in appActions

* test(e2e): add tests for gauges

- test creating a non-default gauge (checks only for console errors)
- test updating a gauge (checks only for console errors)

* fix(e2e): use pluginFixtures for gauge tests

* fix(e2e): prevent fail if testNotes is undefined

* Make the tree key unique (#5989)

* Refactor iterating over all yAxis while drawing to the viewport

* Refactor code to remove hardcoding of yAxisIds

* style: auto-fix lint errors

* Remove configurability of number of y axes. Use yAxisIds from Series to show/hide axes.
Adds test for multiple y axis display

* Add more tests for when only 1 axis needs to be displayed

* Address code review comments:
Move styles to computed properties
Refactor yAxes rendering in MctPlot to also include main y axis.

* Address review comments. Refactor plot configuration code for additiona y axes and reset the chart only for series that belong to the yaxis that has changed.

* Fix bug where only the first series for a given yAxis was rendering

* Refactor code to reuse method logic, move const and use cap case

* Only draw highlights for the given yAxis

* Only draw rectangles for the yAxis the series belongs to

Co-authored-by: Khalid Adil <khalidadil29@gmail.com>
Co-authored-by: Jesse Mazzella <jesse.d.mazzella@nasa.gov>
Co-authored-by: Jesse Mazzella <ozyx@users.noreply.github.com>
2022-12-27 16:02:48 -08:00
Khalid Adil
4847cc8931 [Plot] Set yKey on each plot series (#5790)
* Set yKey on each model

* Add a test
2022-12-27 16:02:48 -08:00
177 changed files with 1651 additions and 6419 deletions

View File

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

View File

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

View File

@@ -1,27 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
// This should be used to install the Example User
document.addEventListener('DOMContentLoaded', () => {
const openmct = window.openmct;
openmct.install(openmct.plugins.example.ExampleUser());
});

View File

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

View File

@@ -1,27 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
// This should be used to install the Operator Status
document.addEventListener('DOMContentLoaded', () => {
const openmct = window.openmct;
openmct.install(openmct.plugins.OperatorStatus());
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View File

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

View File

@@ -30,8 +30,6 @@ const genUuid = require('uuid').v4;
const path = require('path');
const TEST_FOLDER = 'test folder';
const jsonFilePath = 'e2e/test-data/ExampleLayouts.json';
const imageFilePath = 'e2e/test-data/rick.jpg';
test.describe('Form Validation Behavior', () => {
test('Required Field indicators appear if title is empty and can be corrected', async ({ page }) => {
@@ -70,41 +68,6 @@ test.describe('Form Validation Behavior', () => {
});
});
test.describe('Form File Input Behavior', () => {
test.beforeEach(async ({ page }) => {
// eslint-disable-next-line no-undef
await page.addInitScript({ path: path.join(__dirname, '../../helper', 'addInitFileInputObject.js') });
});
test('Can select a JSON file type', async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
await page.getByRole('button', { name: ' Create ' }).click();
await page.getByRole('menuitem', { name: 'JSON File Input Object' }).click();
await page.setInputFiles('#fileElem', jsonFilePath);
await page.getByRole('button', { name: 'Save' }).click();
const type = await page.locator('#file-input-type').textContent();
await expect(type).toBe(`"string"`);
});
test('Can select an image file type', async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
await page.getByRole('button', { name: ' Create ' }).click();
await page.getByRole('menuitem', { name: 'Image File Input Object' }).click();
await page.setInputFiles('#fileElem', imageFilePath);
await page.getByRole('button', { name: 'Save' }).click();
const type = await page.locator('#file-input-type').textContent();
await expect(type).toBe(`"object"`);
});
});
test.describe('Persistence operations @addInit', () => {
// add non persistable root item
test.beforeEach(async ({ page }) => {

View File

@@ -43,76 +43,48 @@ test.describe('Move & link item tests', () => {
name: 'Child Folder',
parent: parentFolder.uuid
});
const grandchildFolder = await createDomainObjectWithDefaults(page, {
await createDomainObjectWithDefaults(page, {
type: 'Folder',
name: 'Grandchild Folder',
parent: childFolder.uuid
});
// Attempt to move parent to its own grandparent
await page.locator('button[title="Show selected item in tree"]').click();
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
await page.locator('.c-disclosure-triangle >> nth=0').click();
const treePane = page.locator('#tree-pane');
await treePane.getByRole('treeitem', {
name: 'Parent Folder'
}).click({
await page.locator(`a:has-text("Parent Folder") >> nth=0`).click({
button: 'right'
});
await page.getByRole('menuitem', {
name: /Move/
}).click();
const locatorTree = page.locator('#locator-tree');
const myItemsLocatorTreeItem = locatorTree.getByRole('treeitem', {
name: myItemsFolderName
});
await myItemsLocatorTreeItem.locator('.c-disclosure-triangle').click();
await myItemsLocatorTreeItem.click();
const parentFolderLocatorTreeItem = locatorTree.getByRole('treeitem', {
name: parentFolder.name
});
await parentFolderLocatorTreeItem.locator('.c-disclosure-triangle').click();
await parentFolderLocatorTreeItem.click();
await page.locator('li.icon-move').click();
await page.locator('form[name="mctForm"] >> .c-disclosure-triangle >> nth=0').click();
await page.locator('form[name="mctForm"] >> text=Parent Folder').click();
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
const childFolderLocatorTreeItem = locatorTree.getByRole('treeitem', {
name: new RegExp(childFolder.name)
});
await childFolderLocatorTreeItem.locator('.c-disclosure-triangle').click();
await childFolderLocatorTreeItem.click();
await page.locator('form[name="mctForm"] >> .c-disclosure-triangle >> nth=1').click();
await page.locator('form[name="mctForm"] >> text=Child Folder').click();
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
const grandchildFolderLocatorTreeItem = locatorTree.getByRole('treeitem', {
name: grandchildFolder.name
});
await grandchildFolderLocatorTreeItem.locator('.c-disclosure-triangle').click();
await grandchildFolderLocatorTreeItem.click();
await page.locator('form[name="mctForm"] >> .c-disclosure-triangle >> nth=2').click();
await page.locator('form[name="mctForm"] >> text=Grandchild Folder').click();
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
await parentFolderLocatorTreeItem.click();
await page.locator('form[name="mctForm"] >> text=Parent Folder').click();
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
await page.locator('[aria-label="Cancel"]').click();
// Move Child Folder from Parent Folder to My Items
await treePane.getByRole('treeitem', {
name: new RegExp(childFolder.name)
}).click({
await page.locator('.c-disclosure-triangle >> nth=0').click();
await page.locator('.c-disclosure-triangle >> nth=1').click();
await page.locator(`a:has-text("Child Folder") >> nth=0`).click({
button: 'right'
});
await page.getByRole('menuitem', {
name: /Move/
}).click();
await myItemsLocatorTreeItem.click();
await page.locator('li.icon-move').click();
await page.locator(`form[name="mctForm"] >> text=${myItemsFolderName}`).click();
await page.locator('[aria-label="Save"]').click();
const myItemsPaneTreeItem = treePane.getByRole('treeitem', {
name: myItemsFolderName
});
await page.locator('button:has-text("OK")').click();
// Expect that Child Folder is in My Items, the root folder
expect(myItemsPaneTreeItem.locator('nth=0:has(text=Child Folder)')).toBeTruthy();
expect(page.locator(`text=${myItemsFolderName} >> nth=0:has(text=Child Folder)`)).toBeTruthy();
});
test('Create a basic object and verify that it cannot be moved to telemetry object without Composition Provider', async ({ page, openmctConfig }) => {
const { myItemsFolderName } = openmctConfig;
@@ -142,7 +114,7 @@ test.describe('Move & link item tests', () => {
// See if it's possible to put the folder in the Telemetry object during creation (Soft Assert)
await page.locator(`form[name="mctForm"] >> text=${telemetryTable}`).click();
let okButton = page.locator('button.c-button.c-button--major:has-text("OK")');
let okButton = await page.locator('button.c-button.c-button--major:has-text("OK")');
let okButtonStateDisabled = await okButton.isDisabled();
expect.soft(okButtonStateDisabled).toBeTruthy();
@@ -166,7 +138,7 @@ test.describe('Move & link item tests', () => {
// See if it's possible to put the folder in the Telemetry object after creation
await page.locator(`text=Location Open MCT ${myItemsFolderName} >> span`).nth(3).click();
await page.locator(`form[name="mctForm"] >> text=${telemetryTable}`).click();
let okButton2 = page.locator('button.c-button.c-button--major:has-text("OK")');
let okButton2 = await page.locator('button.c-button.c-button--major:has-text("OK")');
let okButtonStateDisabled2 = await okButton2.isDisabled();
expect(okButtonStateDisabled2).toBeTruthy();
});
@@ -186,76 +158,48 @@ test.describe('Move & link item tests', () => {
name: 'Child Folder',
parent: parentFolder.uuid
});
const grandchildFolder = await createDomainObjectWithDefaults(page, {
await createDomainObjectWithDefaults(page, {
type: 'Folder',
name: 'Grandchild Folder',
parent: childFolder.uuid
});
// Attempt to move parent to its own grandparent
await page.locator('button[title="Show selected item in tree"]').click();
// Attempt to link parent to its own grandparent
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
await page.locator('.c-disclosure-triangle >> nth=0').click();
const treePane = page.locator('#tree-pane');
await treePane.getByRole('treeitem', {
name: 'Parent Folder'
}).click({
await page.locator(`a:has-text("Parent Folder") >> nth=0`).click({
button: 'right'
});
await page.getByRole('menuitem', {
name: /Move/
}).click();
const locatorTree = page.locator('#locator-tree');
const myItemsLocatorTreeItem = locatorTree.getByRole('treeitem', {
name: myItemsFolderName
});
await myItemsLocatorTreeItem.locator('.c-disclosure-triangle').click();
await myItemsLocatorTreeItem.click();
const parentFolderLocatorTreeItem = locatorTree.getByRole('treeitem', {
name: parentFolder.name
});
await parentFolderLocatorTreeItem.locator('.c-disclosure-triangle').click();
await parentFolderLocatorTreeItem.click();
await page.locator('li.icon-link').click();
await page.locator('form[name="mctForm"] >> .c-disclosure-triangle >> nth=0').click();
await page.locator('form[name="mctForm"] >> text=Parent Folder').click();
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
const childFolderLocatorTreeItem = locatorTree.getByRole('treeitem', {
name: new RegExp(childFolder.name)
});
await childFolderLocatorTreeItem.locator('.c-disclosure-triangle').click();
await childFolderLocatorTreeItem.click();
await page.locator('form[name="mctForm"] >> .c-disclosure-triangle >> nth=1').click();
await page.locator('form[name="mctForm"] >> text=Child Folder').click();
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
const grandchildFolderLocatorTreeItem = locatorTree.getByRole('treeitem', {
name: grandchildFolder.name
});
await grandchildFolderLocatorTreeItem.locator('.c-disclosure-triangle').click();
await grandchildFolderLocatorTreeItem.click();
await page.locator('form[name="mctForm"] >> .c-disclosure-triangle >> nth=2').click();
await page.locator('form[name="mctForm"] >> text=Grandchild Folder').click();
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
await parentFolderLocatorTreeItem.click();
await page.locator('form[name="mctForm"] >> text=Parent Folder').click();
await expect(page.locator('[aria-label="Save"]')).toBeDisabled();
await page.locator('[aria-label="Cancel"]').click();
// Move Child Folder from Parent Folder to My Items
await treePane.getByRole('treeitem', {
name: new RegExp(childFolder.name)
}).click({
// Link Child Folder from Parent Folder to My Items
await page.locator('.c-disclosure-triangle >> nth=0').click();
await page.locator('.c-disclosure-triangle >> nth=1').click();
await page.locator(`a:has-text("Child Folder") >> nth=0`).click({
button: 'right'
});
await page.getByRole('menuitem', {
name: /Link/
}).click();
await myItemsLocatorTreeItem.click();
await page.locator('li.icon-link').click();
await page.locator(`form[name="mctForm"] >> text=${myItemsFolderName}`).click();
await page.locator('[aria-label="Save"]').click();
const myItemsPaneTreeItem = treePane.getByRole('treeitem', {
name: myItemsFolderName
});
await page.locator('button:has-text("OK")').click();
// Expect that Child Folder is in My Items, the root folder
expect(myItemsPaneTreeItem.locator('nth=0:has(text=Child Folder)')).toBeTruthy();
expect(page.locator(`text=${myItemsFolderName} >> nth=0:has(text=Child Folder)`)).toBeTruthy();
});
});

View File

@@ -1,79 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
This test suite is dedicated to tests which verify Open MCT's Notification functionality
*/
// FIXME: Remove this eslint exception once tests are implemented
// eslint-disable-next-line no-unused-vars
const { createDomainObjectWithDefaults } = require('../../appActions');
const { test, expect } = require('../../pluginFixtures');
test.describe('Notifications List', () => {
test.fixme('Notifications can be dismissed individually', async ({ page }) => {
// Create some persistent notifications
// Verify that they are present in the notifications list
// Dismiss one of the notifications
// Verify that it is no longer present in the notifications list
// Verify that the other notifications are still present in the notifications list
});
});
test.describe('Notification Overlay', () => {
test('Closing notification list after notification banner disappeared does not cause it to open automatically', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/6130'
});
// Go to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
// Create a new Display Layout object
await createDomainObjectWithDefaults(page, { type: 'Display Layout' });
// Click on the button "Review 1 Notification"
await page.click('button[aria-label="Review 1 Notification"]');
// Verify that Notification List is open
expect(await page.locator('div[role="dialog"]').isVisible()).toBe(true);
// Wait until there is no Notification Banner
await page.waitForSelector('div[role="alert"]', { state: 'detached'});
// Click on the "Close" button of the Notification List
await page.click('button[aria-label="Close"]');
// On the Display Layout object, click on the "Edit" button
await page.click('button[title="Edit"]');
// Click on the "Save" button
await page.click('button[title="Save"]');
// Click on the "Save and Finish Editing" option
await page.click('li[title="Save and Finish Editing"]');
// Verify that Notification List is NOT open
expect(await page.locator('div[role="dialog"]').isVisible()).toBe(false);
});
});

View File

@@ -98,8 +98,8 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
await page.locator('text=Conditions View Snapshot >> button').nth(3).click();
//Edit Condition Set Name from main view
await page.locator('.l-browse-bar__object-name').filter({ hasText: 'Unnamed Condition Set' }).first().fill('Renamed Condition Set');
await page.locator('.l-browse-bar__object-name').filter({ hasText: 'Renamed Condition Set' }).first().press('Enter');
await page.locator('text=Unnamed Condition Set').first().fill('Renamed Condition Set');
await page.locator('text=Renamed Condition Set').first().press('Enter');
// Click Save Button
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
// Click Save and Finish Editing Option

View File

@@ -24,7 +24,6 @@ const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults, setStartOffset, setFixedTimeMode, setRealTimeMode } = require('../../../../appActions');
test.describe('Display Layout', () => {
/** @type {import('../../../../appActions').CreatedObjectInfo} */
let sineWaveObject;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
@@ -32,7 +31,8 @@ test.describe('Display Layout', () => {
// Create Sine Wave Generator
sineWaveObject = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator'
type: 'Sine Wave Generator',
name: "Test Sine Wave Generator"
});
});
test('alpha-numeric widget telemetry value exactly matches latest telemetry value received in real time', async ({ page }) => {
@@ -47,12 +47,7 @@ test.describe('Display Layout', () => {
// Expand the 'My Items' folder in the left tree
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
// Add the Sine Wave Generator to the Display Layout and save changes
const treePane = page.locator('#tree-pane');
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(sineWaveObject.name)
});
const layoutGridHolder = page.locator('.l-layout__grid-holder');
await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder);
await page.dragAndDrop('text=Test Sine Wave Generator', '.l-layout__grid-holder');
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
@@ -79,12 +74,7 @@ test.describe('Display Layout', () => {
// Expand the 'My Items' folder in the left tree
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
// Add the Sine Wave Generator to the Display Layout and save changes
const treePane = page.locator('#tree-pane');
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(sineWaveObject.name)
});
const layoutGridHolder = page.locator('.l-layout__grid-holder');
await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder);
await page.dragAndDrop('text=Test Sine Wave Generator', '.l-layout__grid-holder');
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
@@ -115,12 +105,7 @@ test.describe('Display Layout', () => {
// Expand the 'My Items' folder in the left tree
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
// Add the Sine Wave Generator to the Display Layout and save changes
const treePane = page.locator('#tree-pane');
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(sineWaveObject.name)
});
const layoutGridHolder = page.locator('.l-layout__grid-holder');
await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder);
await page.dragAndDrop('text=Test Sine Wave Generator', '.l-layout__grid-holder');
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
@@ -130,7 +115,7 @@ test.describe('Display Layout', () => {
await page.locator('.c-tree__item.is-navigated-object .c-disclosure-triangle').click();
// Bring up context menu and remove
await sineWaveGeneratorTreeItem.nth(1).click({ button: 'right' });
await page.locator('.c-tree__item.is-alias .c-tree__item__name:text("Test Sine Wave Generator")').first().click({ button: 'right' });
await page.locator('li[role="menuitem"]:has-text("Remove")').click();
await page.locator('button:has-text("OK")').click();
@@ -145,7 +130,8 @@ test.describe('Display Layout', () => {
});
// Create a Display Layout
const displayLayout = await createDomainObjectWithDefaults(page, {
type: 'Display Layout'
type: 'Display Layout',
name: "Test Display Layout"
});
// Edit Display Layout
await page.locator('[title="Edit"]').click();
@@ -153,12 +139,7 @@ test.describe('Display Layout', () => {
// Expand the 'My Items' folder in the left tree
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
// Add the Sine Wave Generator to the Display Layout and save changes
const treePane = page.locator('#tree-pane');
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(sineWaveObject.name)
});
const layoutGridHolder = page.locator('.l-layout__grid-holder');
await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder);
await page.dragAndDrop('text=Test Sine Wave Generator', '.l-layout__grid-holder');
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
@@ -171,7 +152,7 @@ test.describe('Display Layout', () => {
await page.goto(sineWaveObject.url);
// Bring up context menu and remove
await sineWaveGeneratorTreeItem.first().click({ button: 'right' });
await page.locator('.c-tree__item.is-alias .c-tree__item__name:text("Test Sine Wave Generator")').click({ button: 'right' });
await page.locator('li[role="menuitem"]:has-text("Remove")').click();
await page.locator('button:has-text("OK")').click();

View File

@@ -25,31 +25,26 @@ const { createDomainObjectWithDefaults } = require('../../../../appActions');
test.describe('Flexible Layout', () => {
let sineWaveObject;
let clockObject;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
// Create Sine Wave Generator
sineWaveObject = await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator'
type: 'Sine Wave Generator',
name: "Test Sine Wave Generator"
});
// Create Clock Object
clockObject = await createDomainObjectWithDefaults(page, {
type: 'Clock'
await createDomainObjectWithDefaults(page, {
type: 'Clock',
name: "Test Clock"
});
});
test('panes have the appropriate draggable attribute while in Edit and Browse modes', async ({ page }) => {
const treePane = page.locator('#tree-pane');
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(sineWaveObject.name)
});
const clockTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(clockObject.name)
});
// Create a Flexible Layout
await createDomainObjectWithDefaults(page, {
type: 'Flexible Layout'
type: 'Flexible Layout',
name: "Test Flexible Layout"
});
// Edit Flexible Layout
await page.locator('[title="Edit"]').click();
@@ -57,8 +52,8 @@ test.describe('Flexible Layout', () => {
// Expand the 'My Items' folder in the left tree
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').first().click();
// Add the Sine Wave Generator and Clock to the Flexible Layout
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first());
await clockTreeItem.dragTo(page.locator('.c-fl__container.is-empty'));
await page.dragAndDrop('text=Test Sine Wave Generator', '.c-fl__container.is-empty');
await page.dragAndDrop('text=Test Clock', '.c-fl__container.is-empty');
// Check that panes can be dragged while Flexible Layout is in Edit mode
let dragWrapper = page.locator('.c-fl-container__frames-holder .c-fl-frame__drag-wrapper').first();
await expect(dragWrapper).toHaveAttribute('draggable', 'true');
@@ -70,13 +65,10 @@ test.describe('Flexible Layout', () => {
await expect(dragWrapper).toHaveAttribute('draggable', 'false');
});
test('items in a flexible layout can be removed with object tree context menu when viewing the flexible layout', async ({ page }) => {
const treePane = page.locator('#tree-pane');
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(sineWaveObject.name)
});
// Create a Display Layout
await createDomainObjectWithDefaults(page, {
type: 'Flexible Layout'
type: 'Flexible Layout',
name: "Test Flexible Layout"
});
// Edit Flexible Layout
await page.locator('[title="Edit"]').click();
@@ -84,7 +76,7 @@ test.describe('Flexible Layout', () => {
// Expand the 'My Items' folder in the left tree
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').first().click();
// Add the Sine Wave Generator to the Flexible Layout and save changes
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first());
await page.dragAndDrop('text=Test Sine Wave Generator', '.c-fl__container.is-empty');
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
@@ -94,7 +86,7 @@ test.describe('Flexible Layout', () => {
await page.locator('.c-tree__item.is-navigated-object .c-disclosure-triangle').click();
// Bring up context menu and remove
await sineWaveGeneratorTreeItem.first().click({ button: 'right' });
await page.locator('.c-tree__item.is-alias .c-tree__item__name:text("Test Sine Wave Generator")').first().click({ button: 'right' });
await page.locator('li[role="menuitem"]:has-text("Remove")').click();
await page.locator('button:has-text("OK")').click();
@@ -106,14 +98,10 @@ test.describe('Flexible Layout', () => {
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/3117'
});
const treePane = page.locator('#tree-pane');
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
name: new RegExp(sineWaveObject.name)
});
// Create a Flexible Layout
const flexibleLayout = await createDomainObjectWithDefaults(page, {
type: 'Flexible Layout'
type: 'Flexible Layout',
name: "Test Flexible Layout"
});
// Edit Flexible Layout
await page.locator('[title="Edit"]').click();
@@ -121,7 +109,7 @@ test.describe('Flexible Layout', () => {
// Expand the 'My Items' folder in the left tree
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
// Add the Sine Wave Generator to the Flexible Layout and save changes
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first());
await page.dragAndDrop('text=Test Sine Wave Generator', '.c-fl__container.is-empty');
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
@@ -134,7 +122,7 @@ test.describe('Flexible Layout', () => {
await page.goto(sineWaveObject.url);
// Bring up context menu and remove
await sineWaveGeneratorTreeItem.first().click({ button: 'right' });
await page.locator('.c-tree__item.is-alias .c-tree__item__name:text("Test Sine Wave Generator")').click({ button: 'right' });
await page.locator('li[role="menuitem"]:has-text("Remove")').click();
await page.locator('button:has-text("OK")').click();

View File

@@ -25,13 +25,13 @@ This test suite is dedicated to tests which verify the basic operations surround
but only assume that example imagery is present.
*/
/* globals process */
const { v4: uuid } = require('uuid');
const { waitForAnimations } = require('../../../../baseFixtures');
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
const backgroundImageSelector = '.c-imagery__main-image__background-image';
const panHotkey = process.platform === 'linux' ? ['Control', 'Alt'] : ['Alt'];
const expectedAltText = process.platform === 'linux' ? 'Ctrl+Alt drag to pan' : 'Alt drag to pan';
const thumbnailUrlParamsRegexp = /\?w=100&h=100/;
//The following block of tests verifies the basic functionality of example imagery and serves as a template for Imagery objects embedded in other objects.
test.describe('Example Imagery Object', () => {
@@ -207,58 +207,6 @@ test.describe('Example Imagery in Display Layout', () => {
await page.goto(displayLayout.url);
});
test('View Large action pauses imagery when in realtime and returns to realtime', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/3647'
});
// Click time conductor mode button
await page.locator('.c-mode-button').click();
// set realtime mode
await page.locator('[data-testid="conductor-modeOption-realtime"]').click();
// pause/play button
const pausePlayButton = await page.locator('.c-button.pause-play');
await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/);
// Open context menu and click view large menu item
await page.locator('button[title="View menu items"]').click();
await page.locator('li[title="View Large"]').click();
await expect(pausePlayButton).toHaveClass(/is-paused/);
await page.locator('[aria-label="Close"]').click();
await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/);
});
test('View Large action leaves keeps realtime mode paused', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/3647'
});
// Click time conductor mode button
await page.locator('.c-mode-button').click();
// set realtime mode
await page.locator('[data-testid="conductor-modeOption-realtime"]').click();
// pause/play button
const pausePlayButton = await page.locator('.c-button.pause-play');
await pausePlayButton.click();
await expect.soft(pausePlayButton).toHaveClass(/is-paused/);
// Open context menu and click view large menu item
await page.locator('button[title="View menu items"]').click();
await page.locator('li[title="View Large"]').click();
await expect(pausePlayButton).toHaveClass(/is-paused/);
await page.locator('[aria-label="Close"]').click();
await expect.soft(pausePlayButton).toHaveClass(/is-paused/);
});
test('Imagery View operations @unstable', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
@@ -397,11 +345,13 @@ test.describe('Example Imagery in Time Strip', () => {
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
timeStripObject = await createDomainObjectWithDefaults(page, {
type: 'Time Strip'
type: 'Time Strip',
name: 'Time Strip'.concat(' ', uuid())
});
await createDomainObjectWithDefaults(page, {
type: 'Example Imagery',
name: 'Example Imagery'.concat(' ', uuid()),
parent: timeStripObject.uuid
});
// Navigate to timestrip
@@ -412,28 +362,17 @@ test.describe('Example Imagery in Time Strip', () => {
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5632'
});
// Hover over the timestrip to reveal a thumbnail image
await page.locator('.c-imagery-tsv-container').hover();
// Get the img src of the hovered image thumbnail
const hoveredThumbnailImg = page.locator('.c-imagery-tsv div.c-imagery-tsv__image-wrapper:hover img');
const hoveredThumbnailImgSrc = await hoveredThumbnailImg.getAttribute('src');
// Verify that imagery timestrip view uses the thumbnailUrl as img src for thumbnails
expect(hoveredThumbnailImgSrc).toBeTruthy();
expect(hoveredThumbnailImgSrc).toMatch(thumbnailUrlParamsRegexp);
// Click on the hovered thumbnail to open "View Large" view
// get url of the hovered image
const hoveredImg = page.locator('.c-imagery-tsv div.c-imagery-tsv__image-wrapper:hover img');
const hoveredImgSrc = await hoveredImg.getAttribute('src');
expect(hoveredImgSrc).toBeTruthy();
await page.locator('.c-imagery-tsv-container').click();
// Get the img src of the large view image
// get image of view large container
const viewLargeImg = page.locator('img.c-imagery__main-image__image');
const viewLargeImgSrc = await viewLargeImg.getAttribute('src');
expect(viewLargeImgSrc).toBeTruthy();
// Verify that the image in the large view is the same as the hovered thumbnail
expect(viewLargeImgSrc).toEqual(hoveredThumbnailImgSrc.split('?')[0]);
expect(viewLargeImgSrc).toEqual(hoveredImgSrc);
});
});
@@ -450,12 +389,6 @@ test.describe('Example Imagery in Time Strip', () => {
* @param {import('@playwright/test').Page} page
*/
async function performImageryViewOperationsAndAssert(page) {
// Verify that imagery thumbnails use a thumbnail url
const thumbnailImages = page.locator('.c-thumb__image');
const mainImage = page.locator('.c-imagery__main-image__image');
await expect(thumbnailImages.first()).toHaveAttribute('src', thumbnailUrlParamsRegexp);
await expect(mainImage).not.toHaveAttribute('src', thumbnailUrlParamsRegexp);
// Click previous image button
const previousImageButton = page.locator('.c-nav--prev');
await previousImageButton.click();

View File

@@ -24,6 +24,8 @@
This test suite is dedicated to tests which verify the basic operations surrounding Notebooks.
*/
// FIXME: Remove this eslint exception once tests are implemented
// eslint-disable-next-line no-unused-vars
const { test, expect } = require('../../../../pluginFixtures');
const { expandTreePaneItemByName, createDomainObjectWithDefaults } = require('../../../../appActions');
const nbUtils = require('../../../../helper/notebookUtils');
@@ -263,77 +265,71 @@ test.describe('Notebook entry tests', () => {
});
test.fixme('new entries persist through navigation events without save', async ({ page }) => {});
test.fixme('previous and new entries can be deleted', async ({ page }) => {});
test.fixme('when a valid link is entered into a notebook entry, it becomes clickable when viewing', async ({ page }) => {
const TEST_LINK = 'http://www.google.com';
await page.goto('./#/browse/mine', { waitUntil: 'networkidle' });
});
// Create Notebook
const notebook = await createDomainObjectWithDefaults(page, {
type: 'Notebook',
name: "Entry Link Test"
});
await expandTreePaneItemByName(page, 'My Items');
await page.goto(notebook.url);
await nbUtils.enterTextEntry(page, `This should be a link: ${TEST_LINK} is it?`);
const validLink = page.locator(`a[href="${TEST_LINK}"]`);
// Start waiting for popup before clicking. Note no await.
const popupPromise = page.waitForEvent('popup');
await validLink.click();
const popup = await popupPromise;
// Wait for the popup to load.
await popup.waitForLoadState();
expect.soft(popup.url()).toContain('www.google.com');
expect(await validLink.count()).toBe(1);
test.describe('Snapshot Menu tests', () => {
test.fixme('When no default notebook is selected, Snapshot Menu dropdown should only have a single option', async ({ page }) => {
// There should be no default notebook
// Clear default notebook if exists using `localStorage.setItem('notebook-storage', null);`
// refresh page
// Click on 'Notebook Snaphot Menu'
// 'save to Notebook Snapshots' should be only option there
});
test.fixme('when an invalid link is entered into a notebook entry, it does not become clickable when viewing', async ({ page }) => {
const TEST_LINK = 'www.google.com';
await page.goto('./#/browse/mine', { waitUntil: 'networkidle' });
// Create Notebook
const notebook = await createDomainObjectWithDefaults(page, {
type: 'Notebook',
name: "Entry Link Test"
});
await expandTreePaneItemByName(page, 'My Items');
await page.goto(notebook.url);
await nbUtils.enterTextEntry(page, `This should NOT be a link: ${TEST_LINK} is it?`);
const invalidLink = page.locator(`a[href="${TEST_LINK}"]`);
expect(await invalidLink.count()).toBe(0);
test.fixme('When default notebook is updated selected, Snapshot Menu dropdown should list it as the newest option', async ({ page }) => {
// Create 2a notebooks
// Set Notebook A as Default
// Open Snapshot Menu and note that Notebook A is listed
// Close Snapshot Menu
// Set Default Notebook to Notebook B
// Open Snapshot Notebook and note that Notebook B is listed
// Select Default Notebook Option and verify that Snapshot is added to Notebook B
});
test.fixme('when a nefarious link is entered into a notebook entry, it is sanitized when viewing', async ({ page }) => {
const TEST_LINK = 'http://www.google.com?bad=';
const TEST_LINK_BAD = `http://www.google.com?bad=<script>alert('gimme your cookies')</script>`;
await page.goto('./#/browse/mine', { waitUntil: 'networkidle' });
// Create Notebook
const notebook = await createDomainObjectWithDefaults(page, {
type: 'Notebook',
name: "Entry Link Test"
});
await expandTreePaneItemByName(page, 'My Items');
await page.goto(notebook.url);
await nbUtils.enterTextEntry(page, `This should be a link, BUT not a bad link: ${TEST_LINK_BAD} is it?`);
const sanitizedLink = page.locator(`a[href="${TEST_LINK}"]`);
const unsanitizedLink = page.locator(`a[href="${TEST_LINK_BAD}"]`);
expect.soft(await sanitizedLink.count()).toBe(1);
expect(await unsanitizedLink.count()).toBe(0);
test.fixme('Can add Snapshots via Snapshot Menu and details are correct', async ({ page }) => {
//Note this should be a visual test, too
// Create Telemetry object
// Create A notebook with many pages and sections.
// Set page and section defaults to be between first and last of many. i.e. 3 of 5
// Navigate to Telemetry object
// Select Default Notebook Option and verify that Snapshot is added to Notebook A
// Verify Snapshot Details appear correctly
});
test.fixme('Snapshots adjust time conductor', async ({ page }) => {
// Create Telemetry object
// Set Telemetry object's timeconductor to Fixed time with Start and Endtimes are recorded
// Embed Telemetry object into notebook
// Set Time Conductor to Local clock
// Click into embedded telemetry object and verify object appears with same fixed time from record
});
});
test.describe('Snapshot Container tests', () => {
test.fixme('5 Snapshots can be added to a container', async ({ page }) => {});
test.fixme('5 Snapshots can be added to a container and Deleted with Delete All action', async ({ page }) => {});
test.fixme('A snapshot can be Deleted from Container', async ({ page }) => {});
test.fixme('A snapshot can be Previewed from Container', async ({ page }) => {});
test.fixme('A snapshot Container can be open and closed', async ({ page }) => {});
test.fixme('Can add object to Snapshot container and pull into notebook and create a new entry', async ({ page }) => {
//Create Notebook
//Create Telemetry Object
//From Telemetry Object, use 'save to Notebook Snapshots'
//Snapshots indicator should blink, click on it to view snapshots
//Navigate to Notebook
//Drag and Drop onto droppable area for new entry
//New Entry created with given snapshot added
//Snapshot removed from container?
});
test.fixme('Can add object to Snapshot container and pull into notebook and existing entry', async ({ page }) => {
//Create Notebook
//Create Telemetry Object
//From Telemetry Object, use 'save to Notebook Snapshots'
//Snapshots indicator should blink, click on it to view snapshots
//Navigate to Notebook
//Drag and Drop into exiting entry
//Existing Entry updated with given snapshot
//Snapshot removed from container?
});
test.fixme('Verify Embedded options for PNG, JPG, and Annotate work correctly', async ({ page }) => {
//Add snapshot to container
//Verify PNG, JPG, and Annotate buttons work correctly
});
});

View File

@@ -1,134 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
This test suite is dedicated to tests which verify the basic operations surrounding Notebooks.
*/
const { test, expect } = require('../../../../pluginFixtures');
// const { expandTreePaneItemByName, createDomainObjectWithDefaults } = require('../../../../appActions');
// const nbUtils = require('../../../../helper/notebookUtils');
test.describe('Snapshot Menu tests', () => {
test.fixme('When no default notebook is selected, Snapshot Menu dropdown should only have a single option', async ({ page }) => {
// There should be no default notebook
// Clear default notebook if exists using `localStorage.setItem('notebook-storage', null);`
// refresh page
// Click on 'Notebook Snaphot Menu'
// 'save to Notebook Snapshots' should be only option there
});
test.fixme('When default notebook is updated selected, Snapshot Menu dropdown should list it as the newest option', async ({ page }) => {
// Create 2a notebooks
// Set Notebook A as Default
// Open Snapshot Menu and note that Notebook A is listed
// Close Snapshot Menu
// Set Default Notebook to Notebook B
// Open Snapshot Notebook and note that Notebook B is listed
// Select Default Notebook Option and verify that Snapshot is added to Notebook B
});
test.fixme('Can add Snapshots via Snapshot Menu and details are correct', async ({ page }) => {
//Note this should be a visual test, too
// Create Telemetry object
// Create A notebook with many pages and sections.
// Set page and section defaults to be between first and last of many. i.e. 3 of 5
// Navigate to Telemetry object
// Select Default Notebook Option and verify that Snapshot is added to Notebook A
// Verify Snapshot Details appear correctly
});
test.fixme('Snapshots adjust time conductor', async ({ page }) => {
// Create Telemetry object
// Set Telemetry object's timeconductor to Fixed time with Start and Endtimes are recorded
// Embed Telemetry object into notebook
// Set Time Conductor to Local clock
// Click into embedded telemetry object and verify object appears with same fixed time from record
});
});
test.describe('Snapshot Container tests', () => {
test.beforeEach(async ({ page }) => {
//Navigate to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
// Create Notebook
// const notebook = await createDomainObjectWithDefaults(page, {
// type: 'Notebook',
// name: "Test Notebook"
// });
// // Create Overlay Plot
// const snapShotObject = await createDomainObjectWithDefaults(page, {
// type: 'Overlay Plot',
// name: "Dropped Overlay Plot"
// });
await page.getByRole('button', { name: ' Snapshot ' }).click();
await page.getByRole('menuitem', { name: ' Save to Notebook Snapshots' }).click();
await page.getByRole('button', { name: 'Show' }).click();
});
test.fixme('5 Snapshots can be added to a container', async ({ page }) => {});
test.fixme('5 Snapshots can be added to a container and Deleted with Delete All action', async ({ page }) => {});
test.fixme('A snapshot can be Deleted from Container with 3 dot action menu', async ({ page }) => {});
test.fixme('A snapshot can be Viewed, Annotated, display deleted, and saved from Container with 3 dot action menu', async ({ page }) => {
await page.locator('.c-snapshot.c-ne__embed').first().getByTitle('More options').click();
await page.getByRole('menuitem', { name: ' View Snapshot' }).click();
await expect(page.locator('.c-overlay__outer')).toBeVisible();
await page.getByTitle('Annotate').click();
await expect(page.locator('#snap-annotation-canvas')).toBeVisible();
await page.getByRole('button', { name: '' }).click();
// await expect(page.locator('#snap-annotation-canvas')).not.toBeVisible();
await page.getByRole('button', { name: 'Save' }).click();
await page.getByRole('button', { name: 'Done' }).click();
//await expect(await page.locator)
});
test('A snapshot can be Quick Viewed from Container with 3 dot action menu', async ({ page }) => {
await page.locator('.c-snapshot.c-ne__embed').first().getByTitle('More options').click();
await page.getByRole('menuitem', { name: 'Quick View' }).click();
await expect(page.locator('.c-overlay__outer')).toBeVisible();
});
test.fixme('A snapshot can be Navigated To from Container with 3 dot action menu', async ({ page }) => {});
test.fixme('A snapshot can be Navigated To Item in Time from Container with 3 dot action menu', async ({ page }) => {});
test.fixme('A snapshot Container can be open and closed', async ({ page }) => {});
test.fixme('Can add object to Snapshot container and pull into notebook and create a new entry', async ({ page }) => {
//Create Notebook
//Create Telemetry Object
//From Telemetry Object, use 'save to Notebook Snapshots'
//Snapshots indicator should blink, click on it to view snapshots
//Navigate to Notebook
//Drag and Drop onto droppable area for new entry
//New Entry created with given snapshot added
//Snapshot removed from container?
});
test.fixme('Can add object to Snapshot container and pull into notebook and existing entry', async ({ page }) => {
//Create Notebook
//Create Telemetry Object
//From Telemetry Object, use 'save to Notebook Snapshots'
//Snapshots indicator should blink, click on it to view snapshots
//Navigate to Notebook
//Drag and Drop into exiting entry
//Existing Entry updated with given snapshot
//Snapshot removed from container?
});
test.fixme('Verify Embedded options for PNG, JPG, and Annotate work correctly', async ({ page }) => {
//Add snapshot to container
//Verify PNG, JPG, and Annotate buttons work correctly
});
});

View File

@@ -76,7 +76,6 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => {
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
await page.locator('[aria-label="Notebook Entry Input"]').click();
await page.locator('[aria-label="Notebook Entry Input"]').fill(`First Entry`);
await page.locator('[aria-label="Notebook Entry Input"]').press('Enter');
await page.waitForLoadState('networkidle');
expect(addingNotebookElementsRequests.length).toBeLessThanOrEqual(2);
@@ -149,17 +148,14 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => {
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
await page.locator('[aria-label="Notebook Entry Input"]').click();
await page.locator('[aria-label="Notebook Entry Input"]').fill(`First Entry`);
await page.locator('[aria-label="Notebook Entry Input"]').press('Enter');
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
await page.locator('[aria-label="Notebook Entry Input"] >> nth=1').click();
await page.locator('[aria-label="Notebook Entry Input"] >> nth=1').fill(`Second Entry`);
await page.locator('[aria-label="Notebook Entry Input"] >> nth=1').press('Enter');
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
await page.locator('[aria-label="Notebook Entry Input"] >> nth=2').click();
await page.locator('[aria-label="Notebook Entry Input"] >> nth=2').fill(`Third Entry`);
await page.locator('[aria-label="Notebook Entry Input"] >> nth=2').press('Enter');
// Add three tags
await page.hover(`button:has-text("Add Tag") >> nth=2`);

View File

@@ -152,7 +152,7 @@ test.describe('Restricted Notebook with a page locked and with an embed @addInit
test('Allows embeds to be deleted if page unlocked @addInit', async ({ page }) => {
// Click .c-ne__embed__name .c-popup-menu-button
await page.locator('.c-ne__embed__name .c-icon-button').click(); // embed popup menu
await page.locator('.c-ne__embed__name .c-popup-menu-button').click(); // embed popup menu
const embedMenu = page.locator('body >> .c-menu');
await expect(embedMenu).toContainText('Remove This Embed');
@@ -161,7 +161,7 @@ test.describe('Restricted Notebook with a page locked and with an embed @addInit
test('Disallows embeds to be deleted if page locked @addInit', async ({ page }) => {
await lockPage(page);
// Click .c-ne__embed__name .c-popup-menu-button
await page.locator('.c-ne__embed__name .c-icon-button').click(); // embed popup menu
await page.locator('.c-ne__embed__name .c-popup-menu-button').click(); // embed popup menu
const embedMenu = page.locator('body >> .c-menu');
await expect(embedMenu).not.toContainText('Remove This Embed');

View File

@@ -44,7 +44,6 @@ async function createNotebookAndEntry(page, iterations = 1) {
const entryLocator = `[aria-label="Notebook Entry Input"] >> nth = ${iteration}`;
await page.locator(entryLocator).click();
await page.locator(entryLocator).fill(`Entry ${iteration}`);
await page.locator(entryLocator).press('Enter');
}
return notebook;
@@ -57,14 +56,12 @@ async function createNotebookAndEntry(page, iterations = 1) {
*/
async function createNotebookEntryAndTags(page, iterations = 1) {
const notebook = await createNotebookAndEntry(page, iterations);
await page.locator('text=Annotations').click();
for (let iteration = 0; iteration < iterations; iteration++) {
// Hover and click "Add Tag" button
// Hover is needed here to "slow down" the actions while running in headless mode
await page.locator(`[aria-label="Notebook Entry"] >> nth = ${iteration}`).click();
await page.hover(`button:has-text("Add Tag")`);
await page.locator(`button:has-text("Add Tag")`).click();
await page.hover(`button:has-text("Add Tag") >> nth = ${iteration}`);
await page.locator(`button:has-text("Add Tag") >> nth = ${iteration}`).click();
// Click inside the tag search input
await page.locator('[placeholder="Type to select tag"]').click();
@@ -73,8 +70,8 @@ async function createNotebookEntryAndTags(page, iterations = 1) {
// Hover and click "Add Tag" button
// Hover is needed here to "slow down" the actions while running in headless mode
await page.hover(`button:has-text("Add Tag")`);
await page.locator(`button:has-text("Add Tag")`).click();
await page.hover(`button:has-text("Add Tag") >> nth = ${iteration}`);
await page.locator(`button:has-text("Add Tag") >> nth = ${iteration}`).click();
// Click inside the tag search input
await page.locator('[placeholder="Type to select tag"]').click();
// Select the "Science" tag
@@ -86,10 +83,8 @@ async function createNotebookEntryAndTags(page, iterations = 1) {
test.describe('Tagging in Notebooks @addInit', () => {
test('Can load tags', async ({ page }) => {
await createNotebookAndEntry(page);
await page.locator('text=Annotations').click();
await page.locator('button:has-text("Add Tag")').click();
await page.locator('[placeholder="Type to select tag"]').click();
@@ -130,12 +125,13 @@ test.describe('Tagging in Notebooks @addInit', () => {
test('Can delete tags', async ({ page }) => {
await createNotebookEntryAndTags(page);
await page.locator('[aria-label="Notebook Entries"]').click();
// Delete Driving
await page.hover('[aria-label="Tag"]:has-text("Driving")');
await page.locator('[aria-label="Tag"]:has-text("Driving") ~ .c-completed-tag-deletion').click();
await expect(page.locator('[aria-label="Tags Inspector"]')).toContainText("Science");
await expect(page.locator('[aria-label="Tags Inspector"]')).not.toContainText("Driving");
await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Science");
await expect(page.locator('[aria-label="Notebook Entry"]')).not.toContainText("Driving");
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
await expect(page.locator('[aria-label="Search Result"]')).not.toContainText("Driving");
@@ -197,16 +193,11 @@ test.describe('Tagging in Notebooks @addInit', () => {
page.goto('./#/browse/mine?hideTree=false'),
page.click('.c-disclosure-triangle')
]);
const treePane = page.locator('#tree-pane');
// Click Clock
await treePane.getByRole('treeitem', {
name: clock.name
}).click();
await page.click(`text=${clock.name}`);
// Click Notebook
await page.getByRole('treeitem', {
name: notebook.name
}).click();
await page.click(`text=${notebook.name}`);
for (let iteration = 0; iteration < ITERATIONS; iteration++) {
const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`;

View File

@@ -1,156 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
* This test suite is dedicated to testing the operator status plugin.
*/
const path = require('path');
const { test, expect } = require('../../../../pluginFixtures');
/*
Precondition: Inject Example User, Operator Status Plugins
Verify that user 1 sees updates from user/role 2 (Not possible without openmct-yamcs implementation)
Clear Role Status of single user test
STUB (test.fixme) Rolling through each
*/
test.describe('Operator Status', () => {
test.beforeEach(async ({ page }) => {
// FIXME: determine if plugins will be added to index.html or need to be injected
// eslint-disable-next-line no-undef
await page.addInitScript({ path: path.join(__dirname, '../../../../helper/', 'addInitExampleUser.js')});
// eslint-disable-next-line no-undef
await page.addInitScript({ path: path.join(__dirname, '../../../../helper/', 'addInitOperatorStatus.js')});
await page.goto('./', { waitUntil: 'networkidle' });
});
// verify that operator status is visible
test('operator status is visible and expands when clicked', async ({ page }) => {
await expect(page.locator('div[title="Set my operator status"]')).toBeVisible();
await page.locator('div[title="Set my operator status"]').click();
// expect default status to be 'GO'
await expect(page.locator('.c-status-poll-panel')).toBeVisible();
});
test('poll question indicator remains when blank poll set', async ({ page }) => {
await expect(page.locator('div[title="Set the current poll question"]')).toBeVisible();
await page.locator('div[title="Set the current poll question"]').click();
// set to blank
await page.getByRole('button', { name: 'Update' }).click();
// should still be visible
await expect(page.locator('div[title="Set the current poll question"]')).toBeVisible();
});
// Verify that user 1 sees updates from user/role 2 (Not possible without openmct-yamcs implementation)
test('operator status table reflects answered values', async ({ page }) => {
// user navigates to operator status poll
const statusPollIndicator = page.locator('div[title="Set my operator status"]');
await statusPollIndicator.click();
// get user role value
const userRole = page.locator('.c-status-poll-panel__user-role');
const userRoleText = await userRole.innerText();
// get selected status value
const selectStatus = page.locator('select[name="setStatus"]');
await selectStatus.selectOption({ index: 1});
const initialStatusValue = await selectStatus.inputValue();
// open manage status poll
const manageStatusPollIndicator = page.locator('div[title="Set the current poll question"]');
await manageStatusPollIndicator.click();
// parse the table row values
const row = page.locator(`tr:has-text("${userRoleText}")`);
const rowValues = await row.innerText();
const rowValuesArr = rowValues.split('\t');
const COLUMN_STATUS_INDEX = 1;
// check initial set value matches status table
expect(rowValuesArr[COLUMN_STATUS_INDEX].toLowerCase())
.toEqual(initialStatusValue.toLowerCase());
// change user status
await statusPollIndicator.click();
// FIXME: might want to grab a dynamic option instead of arbitrary
await page.locator('select[name="setStatus"]').selectOption({ index: 2});
const updatedStatusValue = await selectStatus.inputValue();
// verify user status is reflected in table
await manageStatusPollIndicator.click();
const updatedRow = page.locator(`tr:has-text("${userRoleText}")`);
const updatedRowValues = await updatedRow.innerText();
const updatedRowValuesArr = updatedRowValues.split('\t');
expect(updatedRowValuesArr[COLUMN_STATUS_INDEX].toLowerCase())
.toEqual(updatedStatusValue.toLowerCase());
});
test('clear poll button removes poll responses', async ({ page }) => {
// user navigates to operator status poll
const statusPollIndicator = page.locator('div[title="Set my operator status"]');
await statusPollIndicator.click();
// get user role value
const userRole = page.locator('.c-status-poll-panel__user-role');
const userRoleText = await userRole.innerText();
// get selected status value
const selectStatus = page.locator('select[name="setStatus"]');
// FIXME: might want to grab a dynamic option instead of arbitrary
await selectStatus.selectOption({ index: 1});
const initialStatusValue = await selectStatus.inputValue();
// open manage status poll
const manageStatusPollIndicator = page.locator('div[title="Set the current poll question"]');
await manageStatusPollIndicator.click();
// parse the table row values
const row = page.locator(`tr:has-text("${userRoleText}")`);
const rowValues = await row.innerText();
const rowValuesArr = rowValues.split('\t');
const COLUMN_STATUS_INDEX = 1;
// check initial set value matches status table
expect(rowValuesArr[COLUMN_STATUS_INDEX].toLowerCase())
.toEqual(initialStatusValue.toLowerCase());
// clear the poll
await page.locator('button[title="Clear the previous poll question"]').click();
const updatedRow = page.locator(`tr:has-text("${userRoleText}")`);
const updatedRowValues = await updatedRow.innerText();
const updatedRowValuesArr = updatedRowValues.split('\t');
const UNSET_VALUE_LABEL = 'Not set';
expect(updatedRowValuesArr[COLUMN_STATUS_INDEX])
.toEqual(UNSET_VALUE_LABEL);
});
test.fixme('iterate through all possible response values', async ({ page }) => {
// test all possible respone values for the poll
});
});

View File

@@ -32,7 +32,7 @@ test.use({
}
});
test.fixme('ExportAsJSON', () => {
test.describe('ExportAsJSON', () => {
test('User can set autoscale with a valid range @snapshot', async ({ page, openmctConfig }) => {
const { myItemsFolderName } = openmctConfig;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -97,28 +97,20 @@ test.describe('Overlay Plot', () => {
await page.mouse.move(0, 100);
await page.mouse.up();
// Drag swg a, c, e into Y Axis 2
await page.locator('#inspector-elements-tree >> text=swg a').dragTo(page.locator('[aria-label="Element Item Group Y Axis 2"]'));
await page.locator('#inspector-elements-tree >> text=swg c').dragTo(page.locator('[aria-label="Element Item Group Y Axis 2"]'));
await page.locator('#inspector-elements-tree >> text=swg e').dragTo(page.locator('[aria-label="Element Item Group Y Axis 2"]'));
// Drag swg b into Y Axis 3
await page.locator('#inspector-elements-tree >> text=swg b').dragTo(page.locator('[aria-label="Element Item Group Y Axis 3"]'));
const yAxis1Group = page.getByLabel("Y Axis 1");
const yAxis2Group = page.getByLabel("Y Axis 2");
const yAxis3Group = page.getByLabel("Y Axis 3");
// Verify that the elements are in the correct buckets and in the correct order
expect(yAxis1Group.getByRole('listitem', { name: 'swg d' })).toBeTruthy();
expect(yAxis1Group.getByRole('listitem').nth(0).getByText('swg d')).toBeTruthy();
expect(yAxis2Group.getByRole('listitem', { name: 'swg e' })).toBeTruthy();
expect(yAxis2Group.getByRole('listitem').nth(0).getByText('swg e')).toBeTruthy();
expect(yAxis2Group.getByRole('listitem', { name: 'swg c' })).toBeTruthy();
expect(yAxis2Group.getByRole('listitem').nth(1).getByText('swg c')).toBeTruthy();
expect(yAxis2Group.getByRole('listitem', { name: 'swg a' })).toBeTruthy();
expect(yAxis2Group.getByRole('listitem').nth(2).getByText('swg a')).toBeTruthy();
expect(yAxis3Group.getByRole('listitem', { name: 'swg b' })).toBeTruthy();
expect(yAxis3Group.getByRole('listitem').nth(0).getByText('swg b')).toBeTruthy();
await page.locator('#inspector-elements-tree >> text=swg a').dragTo(page.locator('[aria-label="Element Item Group"]').nth(1));
await page.locator('#inspector-elements-tree >> text=swg c').dragTo(page.locator('[aria-label="Element Item Group"]').nth(1));
await page.locator('#inspector-elements-tree >> text=swg e').dragTo(page.locator('[aria-label="Element Item Group"]').nth(1));
await page.locator('#inspector-elements-tree >> text=swg b').dragTo(page.locator('[aria-label="Element Item Group"]').nth(2));
const elementsTree = await page.locator('#inspector-elements-tree').allInnerTexts();
expect(elementsTree.join('').split('\n')).toEqual([
"Y Axis 1",
"swg d",
"Y Axis 2",
"swg e",
"swg c",
"swg a",
"Y Axis 3",
"swg b"
]);
});
});

View File

@@ -1,85 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
const { test, expect } = require('../../pluginFixtures.js');
const { createDomainObjectWithDefaults } = require('../../appActions.js');
test.describe('Recent Objects', () => {
test('Recent Objects CRUD operations', async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
// Create a folder and nest a Clock within it
const recentObjectsList = page.locator('[aria-label="Recent Objects"]');
const folderA = await createDomainObjectWithDefaults(page, {
type: 'Folder'
});
const clock = await createDomainObjectWithDefaults(page, {
type: 'Clock',
parent: folderA.uuid
});
// Drag the Recent Objects panel up a bit
await page.locator('div:nth-child(2) > .l-pane__handle').hover();
await page.mouse.down();
await page.mouse.move(0, 100);
await page.mouse.up();
// Verify that both created objects appear in the list and are in the correct order
expect(recentObjectsList.getByRole('listitem', { name: clock.name })).toBeTruthy();
expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeTruthy();
expect(recentObjectsList.getByRole('listitem', { name: clock.name }).locator('a').getByText(folderA.name)).toBeTruthy();
expect(recentObjectsList.getByRole('listitem').nth(0).getByText(clock.name)).toBeTruthy();
expect(recentObjectsList.getByRole('listitem', { name: clock.name }).locator('a').getByText(folderA.name)).toBeTruthy();
expect(recentObjectsList.getByRole('listitem').nth(1).getByText(folderA.name)).toBeTruthy();
// Navigate to the folder by clicking on the main object name in the recent objects list item
await recentObjectsList.getByRole('listitem', { name: folderA.name }).getByText(folderA.name).click();
await page.waitForURL(`**/${folderA.uuid}?*`);
expect(recentObjectsList.getByRole('listitem').nth(0).getByText(folderA.name)).toBeTruthy();
// Rename
folderA.name = `${folderA.name}-NEW!`;
await page.locator('.l-browse-bar__object-name').fill("");
await page.locator('.l-browse-bar__object-name').fill(folderA.name);
await page.keyboard.press('Enter');
// Verify rename has been applied in recent objects list item and objects paths
expect(page.getByRole('listitem', { name: clock.name }).locator('a').getByText(folderA.name)).toBeTruthy();
expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeTruthy();
// Delete
await page.click('button[title="Show selected item in tree"]');
// Delete the folder via the left tree pane treeitem context menu
await page.getByRole('treeitem', { name: new RegExp(folderA.name) }).locator('a').click({
button: 'right'
});
await page.getByRole('menuitem', { name: /Remove/ }).click();
await page.getByRole('button', { name: 'OK' }).click();
// Verify that the folder and clock are no longer in the recent objects list
await expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeHidden();
await expect(recentObjectsList.getByRole('listitem', { name: clock.name })).toBeHidden();
});
test.fixme("Clicking on the 'target button' scrolls the object into view in the tree and highlights it");
test.fixme("Clicking on an object in the path of a recent object navigates to the object");
test.fixme("Tests for context menu actions from recent objects");
});

View File

@@ -72,7 +72,7 @@ test.describe('Grand Search', () => {
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').fill('Cl');
await Promise.all([
page.waitForNavigation(),
page.locator('[aria-label="Clock A clock result"] >> text=Clock A').click()
page.locator('text=Clock A').click()
]);
await expect(page.locator('.is-object-type-clock')).toBeVisible();

View File

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

View File

@@ -65,7 +65,7 @@ export default class ExampleUserProvider extends EventEmitter {
this.user = undefined;
this.loggedIn = false;
this.autoLoginUser = undefined;
this.status = STATUSES[0];
this.status = STATUSES[1];
this.pollQuestion = undefined;
this.defaultStatusRole = defaultStatusRole;
@@ -124,7 +124,6 @@ export default class ExampleUserProvider extends EventEmitter {
}
setStatusForRole(role, status) {
status.timestamp = Date.now();
this.status = status;
this.emit('statusChange', {
role,
@@ -134,23 +133,14 @@ export default class ExampleUserProvider extends EventEmitter {
return true;
}
// eslint-disable-next-line require-await
async getPollQuestion() {
if (this.pollQuestion) {
return this.pollQuestion;
} else {
return undefined;
}
getPollQuestion() {
return Promise.resolve({
question: 'Set "GO" if your position is ready for a boarding action on the Klingon cruiser',
timestamp: Date.now()
});
}
setPollQuestion(pollQuestion) {
if (!pollQuestion) {
// If the poll question is undefined, set it to a blank string.
// This behavior better reflects how other telemetry systems
// deal with undefined poll questions.
pollQuestion = '';
}
this.pollQuestion = {
question: pollQuestion,
timestamp: Date.now()

View File

@@ -37,9 +37,8 @@ define([
infinityValues: false
};
function GeneratorProvider(openmct, StalenessProvider) {
this.openmct = openmct;
this.workerInterface = new WorkerInterface(openmct, StalenessProvider);
function GeneratorProvider(openmct) {
this.workerInterface = new WorkerInterface(openmct);
}
GeneratorProvider.prototype.canProvideTelemetry = function (domainObject) {
@@ -82,7 +81,6 @@ define([
workerRequest[prop] = Number(workerRequest[prop]);
});
workerRequest.id = this.openmct.objects.makeKeyString(domainObject.identifier);
workerRequest.name = domainObject.name;
return workerRequest;

View File

@@ -1,151 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import EventEmitter from 'EventEmitter';
export default class SinewaveLimitProvider extends EventEmitter {
constructor(openmct) {
super();
this.openmct = openmct;
this.observingStaleness = {};
this.watchingTheClock = false;
this.isRealTime = undefined;
}
supportsStaleness(domainObject) {
return domainObject.type === 'generator';
}
isStale(domainObject, options) {
if (!this.providingStaleness(domainObject)) {
return Promise.resolve({
isStale: false,
utc: 0
});
}
const id = this.getObjectKeyString(domainObject);
if (!this.observerExists(id)) {
this.createObserver(id);
}
return Promise.resolve(this.observingStaleness[id].isStale);
}
subscribeToStaleness(domainObject, callback) {
const id = this.getObjectKeyString(domainObject);
if (this.isRealTime === undefined) {
this.updateRealTime(this.openmct.time.clock());
}
this.handleClockUpdate();
if (this.observerExists(id)) {
this.addCallbackToObserver(id, callback);
} else {
this.createObserver(id, callback);
}
const intervalId = setInterval(() => {
if (this.providingStaleness(domainObject)) {
this.updateStaleness(id, !this.observingStaleness[id].isStale);
}
}, 10000);
return () => {
clearInterval(intervalId);
this.updateStaleness(id, false);
this.handleClockUpdate();
this.destroyObserver(id);
};
}
handleClockUpdate() {
let observers = Object.values(this.observingStaleness).length > 0;
if (observers && !this.watchingTheClock) {
this.watchingTheClock = true;
this.openmct.time.on('clock', this.updateRealTime, this);
} else if (!observers && this.watchingTheClock) {
this.watchingTheClock = false;
this.openmct.time.off('clock', this.updateRealTime, this);
}
}
updateRealTime(clock) {
this.isRealTime = clock !== undefined;
if (!this.isRealTime) {
Object.keys(this.observingStaleness).forEach((id) => {
this.updateStaleness(id, false);
});
}
}
updateStaleness(id, isStale) {
this.observingStaleness[id].isStale = isStale;
this.observingStaleness[id].utc = Date.now();
this.observingStaleness[id].callback({
isStale: this.observingStaleness[id].isStale,
utc: this.observingStaleness[id].utc
});
this.emit('stalenessEvent', {
id,
isStale: this.observingStaleness[id].isStale
});
}
createObserver(id, callback) {
this.observingStaleness[id] = {
isStale: false,
utc: Date.now()
};
if (typeof callback === 'function') {
this.addCallbackToObserver(id, callback);
}
}
destroyObserver(id) {
delete this.observingStaleness[id];
}
providingStaleness(domainObject) {
return domainObject.telemetry?.staleness === true && this.isRealTime;
}
getObjectKeyString(object) {
return this.openmct.objects.makeKeyString(object.identifier);
}
addCallbackToObserver(id, callback) {
this.observingStaleness[id].callback = callback;
}
observerExists(id) {
return this.observingStaleness?.[id];
}
}

View File

@@ -25,24 +25,14 @@ define([
], function (
{ v4: uuid }
) {
function WorkerInterface(openmct, StalenessProvider) {
function WorkerInterface(openmct) {
// eslint-disable-next-line no-undef
const workerUrl = `${openmct.getAssetPath()}${__OPENMCT_ROOT_RELATIVE__}generatorWorker.js`;
this.StalenessProvider = StalenessProvider;
this.worker = new Worker(workerUrl);
this.worker.onmessage = this.onMessage.bind(this);
this.callbacks = {};
this.staleTelemetryIds = {};
this.watchStaleness();
}
WorkerInterface.prototype.watchStaleness = function () {
this.StalenessProvider.on('stalenessEvent', ({ id, isStale}) => {
this.staleTelemetryIds[id] = isStale;
});
};
WorkerInterface.prototype.onMessage = function (message) {
message = message.data;
var callback = this.callbacks[message.id];
@@ -93,12 +83,11 @@ define([
};
WorkerInterface.prototype.subscribe = function (request, cb) {
const id = request.id;
const messageId = this.dispatch('subscribe', request, (message) => {
if (!this.staleTelemetryIds[id]) {
cb(message.data);
}
});
function callback(message) {
cb(message.data);
}
var messageId = this.dispatch('subscribe', request, callback);
return function () {
this.dispatch('unsubscribe', {

View File

@@ -20,163 +20,158 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import GeneratorProvider from "./GeneratorProvider";
import SinewaveLimitProvider from "./SinewaveLimitProvider";
import SinewaveStalenessProvider from "./SinewaveStalenessProvider";
import StateGeneratorProvider from "./StateGeneratorProvider";
import GeneratorMetadataProvider from "./GeneratorMetadataProvider";
define([
"./GeneratorProvider",
"./SinewaveLimitProvider",
"./StateGeneratorProvider",
"./GeneratorMetadataProvider"
], function (
GeneratorProvider,
SinewaveLimitProvider,
StateGeneratorProvider,
GeneratorMetadataProvider
) {
export default function (openmct) {
return function (openmct) {
openmct.types.addType("example.state-generator", {
name: "State Generator",
description: "For development use. Generates example enumerated telemetry by cycling through a given set of states.",
cssClass: "icon-generator-telemetry",
creatable: true,
form: [
{
name: "State Duration (seconds)",
control: "numberfield",
cssClass: "l-input-sm l-numeric",
key: "duration",
required: true,
property: [
"telemetry",
"duration"
]
openmct.types.addType("example.state-generator", {
name: "State Generator",
description: "For development use. Generates example enumerated telemetry by cycling through a given set of states.",
cssClass: "icon-generator-telemetry",
creatable: true,
form: [
{
name: "State Duration (seconds)",
control: "numberfield",
cssClass: "l-input-sm l-numeric",
key: "duration",
required: true,
property: [
"telemetry",
"duration"
]
}
],
initialize: function (object) {
object.telemetry = {
duration: 5
};
}
],
initialize: function (object) {
object.telemetry = {
duration: 5
};
}
});
});
openmct.telemetry.addProvider(new StateGeneratorProvider());
openmct.telemetry.addProvider(new StateGeneratorProvider());
openmct.types.addType("generator", {
name: "Sine Wave Generator",
description: "For development use. Generates example streaming telemetry data using a simple sine wave algorithm.",
cssClass: "icon-generator-telemetry",
creatable: true,
form: [
{
name: "Period",
control: "numberfield",
cssClass: "l-input-sm l-numeric",
key: "period",
required: true,
property: [
"telemetry",
"period"
]
},
{
name: "Amplitude",
control: "numberfield",
cssClass: "l-numeric",
key: "amplitude",
required: true,
property: [
"telemetry",
"amplitude"
]
},
{
name: "Offset",
control: "numberfield",
cssClass: "l-numeric",
key: "offset",
required: true,
property: [
"telemetry",
"offset"
]
},
{
name: "Data Rate (hz)",
control: "numberfield",
cssClass: "l-input-sm l-numeric",
key: "dataRateInHz",
required: true,
property: [
"telemetry",
"dataRateInHz"
]
},
{
name: "Phase (radians)",
control: "numberfield",
cssClass: "l-input-sm l-numeric",
key: "phase",
required: true,
property: [
"telemetry",
"phase"
]
},
{
name: "Randomness",
control: "numberfield",
cssClass: "l-input-sm l-numeric",
key: "randomness",
required: true,
property: [
"telemetry",
"randomness"
]
},
{
name: "Loading Delay (ms)",
control: "numberfield",
cssClass: "l-input-sm l-numeric",
key: "loadDelay",
required: true,
property: [
"telemetry",
"loadDelay"
]
},
{
name: "Include Infinity Values",
control: "toggleSwitch",
cssClass: "l-input",
key: "infinityValues",
property: [
"telemetry",
"infinityValues"
]
},
{
name: "Provide Staleness Updates",
control: "toggleSwitch",
cssClass: "l-input",
key: "staleness",
property: [
"telemetry",
"staleness"
]
openmct.types.addType("generator", {
name: "Sine Wave Generator",
description: "For development use. Generates example streaming telemetry data using a simple sine wave algorithm.",
cssClass: "icon-generator-telemetry",
creatable: true,
form: [
{
name: "Period",
control: "numberfield",
cssClass: "l-input-sm l-numeric",
key: "period",
required: true,
property: [
"telemetry",
"period"
]
},
{
name: "Amplitude",
control: "numberfield",
cssClass: "l-numeric",
key: "amplitude",
required: true,
property: [
"telemetry",
"amplitude"
]
},
{
name: "Offset",
control: "numberfield",
cssClass: "l-numeric",
key: "offset",
required: true,
property: [
"telemetry",
"offset"
]
},
{
name: "Data Rate (hz)",
control: "numberfield",
cssClass: "l-input-sm l-numeric",
key: "dataRateInHz",
required: true,
property: [
"telemetry",
"dataRateInHz"
]
},
{
name: "Phase (radians)",
control: "numberfield",
cssClass: "l-input-sm l-numeric",
key: "phase",
required: true,
property: [
"telemetry",
"phase"
]
},
{
name: "Randomness",
control: "numberfield",
cssClass: "l-input-sm l-numeric",
key: "randomness",
required: true,
property: [
"telemetry",
"randomness"
]
},
{
name: "Loading Delay (ms)",
control: "numberfield",
cssClass: "l-input-sm l-numeric",
key: "loadDelay",
required: true,
property: [
"telemetry",
"loadDelay"
]
},
{
name: "Include Infinity Values",
control: "toggleSwitch",
cssClass: "l-input",
key: "infinityValues",
property: [
"telemetry",
"infinityValues"
]
}
],
initialize: function (object) {
object.telemetry = {
period: 10,
amplitude: 1,
offset: 0,
dataRateInHz: 1,
phase: 0,
randomness: 0,
loadDelay: 0,
infinityValues: false
};
}
],
initialize: function (object) {
object.telemetry = {
period: 10,
amplitude: 1,
offset: 0,
dataRateInHz: 1,
phase: 0,
randomness: 0,
loadDelay: 0,
infinityValues: false,
staleness: false
};
}
});
const stalenessProvider = new SinewaveStalenessProvider(openmct);
});
openmct.telemetry.addProvider(new GeneratorProvider(openmct, stalenessProvider));
openmct.telemetry.addProvider(new GeneratorMetadataProvider());
openmct.telemetry.addProvider(new SinewaveLimitProvider());
openmct.telemetry.addProvider(stalenessProvider);
}
openmct.telemetry.addProvider(new GeneratorProvider(openmct));
openmct.telemetry.addProvider(new GeneratorMetadataProvider());
openmct.telemetry.addProvider(new SinewaveLimitProvider());
};
});

View File

@@ -107,15 +107,6 @@ export default function () {
}
]
},
{
name: 'Image Thumbnail',
key: 'thumbnail-url',
format: 'thumbnail',
hints: {
thumbnail: 1
},
source: 'url'
},
{
name: 'Image Download Name',
key: 'imageDownloadName',
@@ -152,16 +143,6 @@ export default function () {
]
});
const formatThumbnail = {
format: function (url) {
return `${url}?w=100&h=100`;
}
};
openmct.telemetry.addFormat({
key: 'thumbnail',
...formatThumbnail
});
openmct.telemetry.addProvider(getRealtimeProvider());
openmct.telemetry.addProvider(getHistoricalProvider());
openmct.telemetry.addProvider(getLadProvider());
@@ -261,13 +242,6 @@ function pointForTimestamp(timestamp, name, imageSamples, delay) {
const url = imageSamples[Math.floor(timestamp / delay) % imageSamples.length];
const urlItems = url.split('/');
const imageDownloadName = `example.imagery.${urlItems[urlItems.length - 1]}`;
const navCamTransformations = {
"translateX": 0,
"translateY": 18,
"rotation": 0,
"scale": 0.3,
"cameraAngleOfView": 70
};
return {
name,
@@ -277,7 +251,6 @@ function pointForTimestamp(timestamp, name, imageSamples, delay) {
sunOrientation: getCompassValues(0, 360),
cameraPan: getCompassValues(0, 360),
heading: getCompassValues(0, 360),
transformations: navCamTransformations,
imageDownloadName
};
}

View File

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

View File

@@ -1,14 +1,13 @@
{
"name": "openmct",
"version": "2.1.6-SNAPSHOT",
"version": "2.1.5-SNAPSHOT",
"description": "The Open MCT core platform",
"devDependencies": {
"@babel/eslint-parser": "7.18.9",
"@braintree/sanitize-url": "6.0.2",
"@percy/cli": "1.17.0",
"@percy/cli": "1.16.0",
"@percy/playwright": "1.0.4",
"@playwright/test": "1.29.0",
"@types/eventemitter3": "1.2.0",
"@types/jasmine": "4.3.1",
"@types/lodash": "4.14.191",
"babel-loader": "9.1.0",
@@ -16,14 +15,14 @@
"codecov": "3.8.3",
"comma-separated-values": "3.6.4",
"copy-webpack-plugin": "11.0.0",
"css-loader": "6.7.3",
"css-loader": "6.7.1",
"d3-axis": "3.0.0",
"d3-scale": "3.3.0",
"d3-selection": "3.0.0",
"eslint": "8.32.0",
"eslint": "8.30.0",
"eslint-plugin-compat": "4.0.2",
"eslint-plugin-playwright": "0.12.0",
"eslint-plugin-vue": "9.9.0",
"eslint-plugin-playwright": "0.11.2",
"eslint-plugin-vue": "9.8.0",
"eslint-plugin-you-dont-need-lodash-underscore": "6.12.0",
"eventemitter3": "1.2.0",
"file-saver": "2.0.5",
@@ -39,9 +38,8 @@
"karma-jasmine": "5.1.0",
"karma-junit-reporter": "2.0.1",
"karma-sourcemap-loader": "0.3.8",
"karma-spec-reporter": "0.0.36",
"karma-spec-reporter": "0.0.34",
"karma-webpack": "5.0.0",
"kdbush": "^3.0.0",
"location-bar": "3.0.1",
"lodash": "4.17.21",
"mini-css-extract-plugin": "2.7.2",
@@ -51,13 +49,12 @@
"nyc": "15.1.0",
"painterro": "1.2.78",
"playwright-core": "1.29.0",
"plotly.js-basic-dist": "2.17.0",
"plotly.js-gl2d-dist": "2.17.1",
"plotly.js-basic-dist": "2.14.0",
"plotly.js-gl2d-dist": "2.14.0",
"printj": "1.3.1",
"resolve-url-loader": "5.0.0",
"sanitize-html": "2.8.1",
"sass": "1.57.1",
"sass-loader": "13.2.0",
"sass": "1.56.1",
"sass-loader": "13.0.2",
"sinon": "15.0.1",
"style-loader": "^3.3.1",
"typescript": "4.9.4",
@@ -74,14 +71,14 @@
"scripts": {
"clean": "rm -rf ./dist ./node_modules ./package-lock.json",
"clean-test-lint": "npm run clean; npm install; npm run test; npm run lint",
"start": "npx webpack serve --config ./.webpack/webpack.dev.js",
"start:coverage": "npx webpack serve --config ./.webpack/webpack.coverage.js",
"start": "npx webpack serve --config ./webpack.dev.js",
"start:coverage": "npx webpack serve --config ./webpack.coverage.js",
"lint": "eslint example src e2e --ext .js,.vue openmct.js --max-warnings=0",
"lint:fix": "eslint example src e2e --ext .js,.vue openmct.js --fix",
"build:prod": "webpack --config ./.webpack/webpack.prod.js",
"build:dev": "webpack --config ./.webpack/webpack.dev.js",
"build:coverage": "webpack --config ./.webpack/webpack.coverage.js",
"build:watch": "webpack --config ./.webpack/webpack.dev.js --watch",
"build:prod": "webpack --config webpack.prod.js",
"build:dev": "webpack --config webpack.dev.js",
"build:coverage": "webpack --config webpack.coverage.js",
"build:watch": "webpack --config webpack.dev.js --watch",
"info": "npx envinfo --system --browsers --npmPackages --binaries --languages --markdown",
"test": "karma start",
"test:debug": "KARMA_DEBUG=true karma start",

View File

@@ -256,15 +256,6 @@ define([
});
});
/**
* MCT's annotation API that enables
* human-created comments and categorization linked to data products
* @type {module:openmct.AnnotationAPI}
* @memberof module:openmct.MCT#
* @name annotation
*/
this.annotation = new api.AnnotationAPI(this);
// Plugins that are installed by default
this.install(this.plugins.Plot());
this.install(this.plugins.TelemetryTable.default());

View File

@@ -52,29 +52,6 @@ const ANNOTATION_LAST_CREATED = 'annotationLastCreated';
* @property {String} foregroundColor eg. "#ffffff"
*/
/**
* @typedef {import('../objects/ObjectAPI').DomainObject} DomainObject
*/
/**
* @typedef {import('../objects/ObjectAPI').Identifier} Identifier
*/
/**
* @typedef {import('../../../openmct').OpenMCT} OpenMCT
*/
/**
* An interface for interacting with annotations of domain objects.
* An annotation of a domain object is an operator created object for the purposes
* of further describing data in plots, notebooks, maps, etc. For example, an annotation
* could be a tag on a plot notating an interesting set of points labeled SCIENCE. It could
* also be set of notebook entries the operator has tagged DRIVING when a robot monitored by OpenMCT
* about rationals behind why the robot has taken a certain path.
* Annotations are discoverable using search, and are typically rendered in OpenMCT views to bring attention
* to other users.
* @constructor
*/
export default class AnnotationAPI extends EventEmitter {
/**
@@ -104,26 +81,24 @@ export default class AnnotationAPI extends EventEmitter {
}
});
}
/**
* Creates an annotation on a given domain object (e.g., a plot) and a set of targets (e.g., telemetry objects)
* Create the a generic annotation
* @typedef {Object} CreateAnnotationOptions
* @property {String} name a name for the new annotation (e.g., "Plot annnotation")
* @property {DomainObject} domainObject the domain object this annotation was created with
* @property {ANNOTATION_TYPES} annotationType the type of annotation to create (e.g., PLOT_SPATIAL)
* @property {Tag[]} tags tags to add to the annotation, e.g., SCIENCE for science related annotations
* @property {String} contentText Some text to add to the annotation, e.g. ("This annotation is about science")
* @property {Object<string, Object>} targets The targets ID keystrings and their specific properties.
* For plots, this will be a bounding box, e.g.: {maxY: 100, minY: 0, maxX: 100, minX: 0}
* For notebooks, this will be an entry ID, e.g.: {entryId: "entry-ecb158f5-d23c-45e1-a704-649b382622ba"}
* @property {DomainObject>} targetDomainObjects the targets ID keystrings and the domain objects this annotation points to (e.g., telemetry objects for a plot)
* @property {String} name a name for the new parameter
* @property {import('../objects/ObjectAPI').DomainObject} domainObject the domain object to create
* @property {ANNOTATION_TYPES} annotationType the type of annotation to create
* @property {Tag[]} tags
* @property {String} contentText
* @property {import('../objects/ObjectAPI').Identifier[]} targets
*/
/**
* @method create
* @param {CreateAnnotationOptions} options
* @returns {Promise<DomainObject>} a promise which will resolve when the domain object
* @returns {Promise<import('../objects/ObjectAPI').DomainObject>} a promise which will resolve when the domain object
* has been created, or be rejected if it cannot be saved
*/
async create({name, domainObject, annotationType, tags, contentText, targets, targetDomainObjects}) {
async create({name, domainObject, annotationType, tags, contentText, targets}) {
if (!Object.keys(this.ANNOTATION_TYPES).includes(annotationType)) {
throw new Error(`Unknown annotation type: ${annotationType}`);
}
@@ -132,10 +107,6 @@ export default class AnnotationAPI extends EventEmitter {
throw new Error(`At least one target is required to create an annotation`);
}
if (!Object.keys(targetDomainObjects).length) {
throw new Error(`At least one targetDomainObject is required to create an annotation`);
}
const domainObjectKeyString = this.openmct.objects.makeKeyString(domainObject.identifier);
const originalPathObjects = await this.openmct.objects.getOriginalPath(domainObjectKeyString);
const originalContextPath = this.openmct.objects.getRelativePath(originalPathObjects);
@@ -168,9 +139,7 @@ export default class AnnotationAPI extends EventEmitter {
const success = await this.openmct.objects.save(createdObject);
if (success) {
this.emit('annotationCreated', createdObject);
Object.values(targetDomainObjects).forEach(targetDomainObject => {
this.#updateAnnotationModified(targetDomainObject);
});
this.#updateAnnotationModified(domainObject);
return createdObject;
} else {
@@ -178,15 +147,8 @@ export default class AnnotationAPI extends EventEmitter {
}
}
#updateAnnotationModified(targetDomainObject) {
// As certain telemetry objects are immutable, we'll need to check here first
// to see if we can add the annotation last created property.
// TODO: This should be removed once we have a better way to handle immutable telemetry objects
if (targetDomainObject.isMutable) {
this.openmct.objects.mutate(targetDomainObject, this.ANNOTATION_LAST_CREATED, Date.now());
} else {
this.emit('targetDomainObjectAnnotated', targetDomainObject);
}
#updateAnnotationModified(domainObject) {
this.openmct.objects.mutate(domainObject, this.ANNOTATION_LAST_CREATED, Date.now());
}
/**
@@ -200,7 +162,7 @@ export default class AnnotationAPI extends EventEmitter {
/**
* @method isAnnotation
* @param {DomainObject} domainObject the domainObject in question
* @param {import('../objects/ObjectAPI').DomainObject} domainObject domainObject the domainObject in question
* @returns {Boolean} Returns true if the domain object is an annotation
*/
isAnnotation(domainObject) {
@@ -228,19 +190,56 @@ export default class AnnotationAPI extends EventEmitter {
/**
* @method getAnnotations
* @param {Identifier} domainObjectIdentifier - The domain object identifier to use to search for annotations. For example, a telemetry object identifier.
* @returns {DomainObject[]} Returns an array of annotations that match the search query
* @param {String} query - The keystring of the domain object to search for annotations for
* @returns {import('../objects/ObjectAPI').DomainObject[]} Returns an array of domain objects that match the search query
*/
async getAnnotations(domainObjectIdentifier) {
const keyStringQuery = this.openmct.objects.makeKeyString(domainObjectIdentifier);
const searchResults = (await Promise.all(this.openmct.objects.search(keyStringQuery, null, this.openmct.objects.SEARCH_TYPES.ANNOTATIONS))).flat();
async getAnnotations(query) {
const searchResults = (await Promise.all(this.openmct.objects.search(query, null, this.openmct.objects.SEARCH_TYPES.ANNOTATIONS))).flat();
return searchResults;
}
/**
* @method addSingleAnnotationTag
* @param {import('../objects/ObjectAPI').DomainObject=} existingAnnotation - An optional annotation to add the tag to. If not specified, we will create an annotation.
* @param {import('../objects/ObjectAPI').DomainObject} targetDomainObject - The domain object the annotation will point to.
* @param {Object=} targetSpecificDetails - Optional object to add to the target object. E.g., for notebooks this would be an entryID
* @param {AnnotationType} annotationType - The type of annotation this is for.
* @returns {import('../objects/ObjectAPI').DomainObject[]} Returns the annotation that was either created or passed as an existingAnnotation
*/
async addSingleAnnotationTag(existingAnnotation, targetDomainObject, targetSpecificDetails, annotationType, tag) {
if (!existingAnnotation) {
const targets = {};
const targetKeyString = this.openmct.objects.makeKeyString(targetDomainObject.identifier);
targets[targetKeyString] = targetSpecificDetails;
const contentText = `${annotationType} tag`;
const annotationCreationArguments = {
name: contentText,
domainObject: targetDomainObject,
annotationType,
tags: [tag],
contentText,
targets
};
const newAnnotation = await this.create(annotationCreationArguments);
return newAnnotation;
} else {
if (!existingAnnotation.tags.includes(tag)) {
throw new Error(`Existing annotation did not contain tag ${tag}`);
}
if (existingAnnotation._deleted) {
this.unDeleteAnnotation(existingAnnotation);
}
return existingAnnotation;
}
}
/**
* @method deleteAnnotations
* @param {DomainObject[]} existingAnnotation - An array of annotations to delete (set _deleted to true)
* @param {import('../objects/ObjectAPI').DomainObject[]} existingAnnotation - An array of annotations to delete (set _deleted to true)
*/
deleteAnnotations(annotations) {
if (!annotations) {
@@ -256,7 +255,7 @@ export default class AnnotationAPI extends EventEmitter {
/**
* @method deleteAnnotations
* @param {DomainObject} annotation - An annotation to undelete (set _deleted to false)
* @param {import('../objects/ObjectAPI').DomainObject} existingAnnotation - An annotations to undelete (set _deleted to false)
*/
unDeleteAnnotation(annotation) {
if (!annotation) {
@@ -266,39 +265,6 @@ export default class AnnotationAPI extends EventEmitter {
this.openmct.objects.mutate(annotation, '_deleted', false);
}
getTagsFromAnnotations(annotations, filterDuplicates = true) {
if (!annotations) {
return [];
}
let tagsFromAnnotations = annotations.flatMap((annotation) => {
if (annotation._deleted) {
return [];
} else {
return annotation.tags;
}
});
if (filterDuplicates) {
tagsFromAnnotations = tagsFromAnnotations.filter((tag, index, tagArray) => {
return tagArray.indexOf(tag) === index;
});
}
const fullTagModels = this.#addTagMetaInformationToTags(tagsFromAnnotations);
return fullTagModels;
}
#addTagMetaInformationToTags(tags) {
return tags.map(tagKey => {
const tagModel = this.availableTags[tagKey];
tagModel.tagID = tagKey;
return tagModel;
});
}
#getMatchingTags(query) {
if (!query) {
return [];
@@ -317,7 +283,12 @@ export default class AnnotationAPI extends EventEmitter {
#addTagMetaInformationToResults(results, matchingTagKeys) {
const tagsAddedToResults = results.map(result => {
const fullTagModels = this.#addTagMetaInformationToTags(result.tags);
const fullTagModels = result.tags.map(tagKey => {
const tagModel = this.availableTags[tagKey];
tagModel.tagID = tagKey;
return tagModel;
});
return {
fullTagModels,
@@ -367,33 +338,6 @@ export default class AnnotationAPI extends EventEmitter {
return combinedResults;
}
/**
* @method #breakApartSeparateTargets
* @param {Array} results A set of search results that could have the multiple targets for the same result
* @returns {Array} The same set of results, but with each target separated out into its own result
*/
#breakApartSeparateTargets(results) {
const separateResults = [];
results.forEach(result => {
Object.keys(result.targets).forEach(targetID => {
const separatedResult = {
...result
};
separatedResult.targets = {
[targetID]: result.targets[targetID]
};
separatedResult.targetModels = result.targetModels.filter(targetModel => {
const targetKeyString = this.openmct.objects.makeKeyString(targetModel.identifier);
return targetKeyString === targetID;
});
separateResults.push(separatedResult);
});
});
return separateResults;
}
/**
* @method searchForTags
* @param {String} query A query to match against tags. E.g., "dr" will match the tags "drilling" and "driving"
@@ -416,8 +360,7 @@ export default class AnnotationAPI extends EventEmitter {
const resultsWithValidPath = appliedTargetsModels.filter(result => {
return this.openmct.objects.isReachable(result.targetModels?.[0]?.originalPath);
});
const breakApartSeparateTargets = this.#breakApartSeparateTargets(resultsWithValidPath);
return breakApartSeparateTargets;
return resultsWithValidPath;
}
}

View File

@@ -108,7 +108,6 @@ describe("The Annotation API", () => {
annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
tags: ['sometag'],
contentText: "fooContext",
targetDomainObjects: [mockDomainObject],
targets: {'fooTarget': {}}
};
const annotationObject = await openmct.annotation.create(annotationCreationArguments);
@@ -125,39 +124,27 @@ describe("The Annotation API", () => {
});
describe("Tagging", () => {
let tagCreationArguments;
beforeEach(() => {
tagCreationArguments = {
name: 'Test Annotation',
domainObject: mockDomainObject,
annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
tags: ['aWonderfulTag'],
contentText: 'fooContext',
targets: {'fooNameSpace:some-object': {entryId: 'fooBarEntry'}},
targetDomainObjects: [mockDomainObject]
};
});
it("can create a tag", async () => {
const annotationObject = await openmct.annotation.create(tagCreationArguments);
const annotationObject = await openmct.annotation.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
expect(annotationObject).toBeDefined();
expect(annotationObject.type).toEqual('annotation');
expect(annotationObject.tags).toContain('aWonderfulTag');
});
it("can delete a tag", async () => {
const annotationObject = await openmct.annotation.create(tagCreationArguments);
const annotationObject = await openmct.annotation.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
expect(annotationObject).toBeDefined();
openmct.annotation.deleteAnnotations([annotationObject]);
expect(annotationObject._deleted).toBeTrue();
});
it("throws an error if deleting non-existent tag", async () => {
const annotationObject = await openmct.annotation.create(tagCreationArguments);
const annotationObject = await openmct.annotation.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
expect(annotationObject).toBeDefined();
expect(() => {
openmct.annotation.removeAnnotationTag(annotationObject, 'ThisTagShouldNotExist');
}).toThrow();
});
it("can remove all tags", async () => {
const annotationObject = await openmct.annotation.create(tagCreationArguments);
const annotationObject = await openmct.annotation.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
expect(annotationObject).toBeDefined();
expect(() => {
openmct.annotation.deleteAnnotations([annotationObject]);
@@ -165,13 +152,13 @@ describe("The Annotation API", () => {
expect(annotationObject._deleted).toBeTrue();
});
it("can add/delete/add a tag", async () => {
let annotationObject = await openmct.annotation.create(tagCreationArguments);
let annotationObject = await openmct.annotation.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
expect(annotationObject).toBeDefined();
expect(annotationObject.type).toEqual('annotation');
expect(annotationObject.tags).toContain('aWonderfulTag');
openmct.annotation.deleteAnnotations([annotationObject]);
expect(annotationObject._deleted).toBeTrue();
annotationObject = await openmct.annotation.create(tagCreationArguments);
annotationObject = await openmct.annotation.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
expect(annotationObject).toBeDefined();
expect(annotationObject.type).toEqual('annotation');
expect(annotationObject.tags).toContain('aWonderfulTag');

View File

@@ -20,7 +20,6 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import EventEmitter from 'EventEmitter';
/**
* @typedef {import('../objects/ObjectAPI').DomainObject} DomainObject
*/
@@ -61,10 +60,6 @@ export default class CompositionCollection {
#publicAPI;
#listeners;
#mutables;
#onGlobalAdd;
#onGlobalRemove;
static #globalEvents = new EventEmitter();
/**
* @constructor
* @param {DomainObject} domainObject the domain object
@@ -100,21 +95,6 @@ export default class CompositionCollection {
unobserve();
});
}
const keyString = publicAPI.objects.makeKeyString(domainObject.identifier);
this.#onGlobalAdd = this._onGlobalAdd.bind(this);
this.#onGlobalRemove = this._onGlobalRemove.bind(this);
CompositionCollection.#globalEvents.on(`add:${keyString}`, this.#onGlobalAdd);
CompositionCollection.#globalEvents.on(`remove:${keyString}`, this.#onGlobalRemove);
}
_onGlobalAdd(object) {
this.#emit('add', object);
}
_onGlobalRemove(identifier) {
this.#emit('remove', identifier);
}
/**
* Listen for changes to this composition. Supports 'add', 'remove', and
@@ -229,21 +209,23 @@ export default class CompositionCollection {
* **Intended for internal use ONLY.**
* true if the underlying provider should not be updated.
*/
add(child) {
if (!this.#publicAPI.composition.checkPolicy(this.domainObject, child)) {
throw `Object of type ${child.type} cannot be added to object of type ${this.domainObject.type}`;
add(child, skipMutate) {
if (!skipMutate) {
if (!this.#publicAPI.composition.checkPolicy(this.domainObject, child)) {
throw `Object of type ${child.type} cannot be added to object of type ${this.domainObject.type}`;
}
this.#provider.add(this.domainObject, child.identifier);
} else {
if (this.returnMutables && this.#publicAPI.objects.supportsMutation(child.identifier)) {
let keyString = this.#publicAPI.objects.makeKeyString(child.identifier);
child = this.#publicAPI.objects.toMutable(child);
this.#mutables[keyString] = child;
}
this.#emit('add', child);
}
this.#provider.add(this.domainObject, child.identifier);
if (this.returnMutables && this.#publicAPI.objects.supportsMutation(child.identifier)) {
let keyString = this.#publicAPI.objects.makeKeyString(child.identifier);
child = this.#publicAPI.objects.toMutable(child);
this.#mutables[keyString] = child;
}
// const keyString = this.#publicAPI.objects.makeKeyString(this.domainObject.identifier);
// CompositionCollection.#globalEvents.emit(`add:${keyString}`, child);
}
/**
* Load the domain objects in this composition.
@@ -258,12 +240,7 @@ export default class CompositionCollection {
this.#cleanUpMutables();
const children = await this.#provider.load(this.domainObject);
const childObjects = await Promise.all(children.map((c) => this.#publicAPI.objects.get(c, abortSignal)));
childObjects.forEach(c => {
this.add(c);
const keyString = this.#publicAPI.objects.makeKeyString(this.domainObject.identifier);
CompositionCollection.#globalEvents.emit(`add:${keyString}`, c);
});
childObjects.forEach(c => this.add(c, true));
this.#emit('load');
return childObjects;
@@ -282,18 +259,20 @@ export default class CompositionCollection {
* true if the underlying provider should not be updated.
* @name remove
*/
remove(child) {
this.#provider.remove(this.domainObject, child.identifier);
if (this.returnMutables) {
let keyString = this.#publicAPI.objects.makeKeyString(child);
if (this.#mutables[keyString] !== undefined && this.#mutables[keyString].isMutable) {
this.#publicAPI.objects.destroyMutable(this.#mutables[keyString]);
delete this.#mutables[keyString];
remove(child, skipMutate) {
if (!skipMutate) {
this.#provider.remove(this.domainObject, child.identifier);
} else {
if (this.returnMutables) {
let keyString = this.#publicAPI.objects.makeKeyString(child);
if (this.#mutables[keyString] !== undefined && this.#mutables[keyString].isMutable) {
this.#publicAPI.objects.destroyMutable(this.#mutables[keyString]);
delete this.#mutables[keyString];
}
}
}
// const keyString = this.#publicAPI.objects.makeKeyString(this.domainObject.identifier);
// CompositionCollection.#globalEvents.emit(`remove:${keyString}`, child.identifier);
this.#emit('remove', child);
}
}
/**
* Reorder the domain objects in this composition.
@@ -316,10 +295,6 @@ export default class CompositionCollection {
this.mutationListener();
delete this.mutationListener;
}
const keyString = this.#publicAPI.objects.makeKeyString(this.domainObject.identifier);
CompositionCollection.#globalEvents.off(`add:${keyString}`, this.#onGlobalAdd);
CompositionCollection.#globalEvents.off(`remove:${keyString}`, this.#onGlobalRemove);
}
/**
* Handle reorder from provider.

View File

@@ -71,6 +71,10 @@ export default class CompositionProvider {
return this.#listeningTo;
}
get establishTopicListener() {
return this.#establishTopicListener.bind(this);
}
get publicAPI() {
return this.#publicAPI;
}
@@ -177,6 +181,22 @@ export default class CompositionProvider {
throw new Error("This method must be implemented by a subclass.");
}
/**
* Listens on general mutation topic, using injector to fetch to avoid
* circular dependencies.
* @private
*/
#establishTopicListener() {
if (this.topicListener) {
return;
}
this.#publicAPI.objects.eventEmitter.on('mutation', this.#onMutation.bind(this));
this.topicListener = () => {
this.#publicAPI.objects.eventEmitter.off('mutation', this.#onMutation.bind(this));
};
}
/**
* @private
* @param {DomainObject} parent
@@ -196,5 +216,47 @@ export default class CompositionProvider {
#supportsComposition(parent, _child) {
return this.#publicAPI.composition.supportsComposition(parent);
}
/**
* Handles mutation events. If there are active listeners for the mutated
* object, detects changes to composition and triggers necessary events.
*
* @private
* @param {DomainObject} oldDomainObject
*/
#onMutation(oldDomainObject) {
const id = objectUtils.makeKeyString(oldDomainObject.identifier);
const listeners = this.#listeningTo[id];
if (!listeners) {
return;
}
const oldComposition = listeners.composition.map(objectUtils.makeKeyString);
const newComposition = oldDomainObject.composition.map(objectUtils.makeKeyString);
const added = _.difference(newComposition, oldComposition).map(objectUtils.parseKeyString);
const removed = _.difference(oldComposition, newComposition).map(objectUtils.parseKeyString);
function notify(value) {
return function (listener) {
if (listener.context) {
listener.callback.call(listener.context, value);
} else {
listener.callback(value);
}
};
}
listeners.composition = newComposition.map(objectUtils.parseKeyString);
added.forEach(function (addedChild) {
listeners.add.forEach(notify(addedChild));
});
removed.forEach(function (removedChild) {
listeners.remove.forEach(notify(removedChild));
});
}
}

View File

@@ -89,7 +89,7 @@ export default class DefaultCompositionProvider extends CompositionProvider {
event,
callback,
context) {
//this.establishTopicListener();
this.establishTopicListener();
/** @type {string} */
const keyString = objectUtils.makeKeyString(domainObject.identifier);
@@ -157,8 +157,6 @@ export default class DefaultCompositionProvider extends CompositionProvider {
});
this.publicAPI.objects.mutate(domainObject, 'composition', composition);
this.objectListeners.remove?.forEach(listener => listener.callback.apply(listener.context, childId));
}
/**
* Add a domain object to another domain object's composition.
@@ -176,8 +174,6 @@ export default class DefaultCompositionProvider extends CompositionProvider {
if (!this.includes(parent, childId)) {
parent.composition.push(childId);
this.publicAPI.objects.mutate(parent, 'composition', parent.composition);
this.objectListeners.add?.forEach(listener => listener.callback.apply(listener.context, childId));
}
}

View File

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

View File

@@ -22,7 +22,6 @@
<template>
<mct-tree
id="locator-tree"
:is-selector-tree="true"
:initial-selection="model.parent"
@tree-item-selection="handleItemSelection"

View File

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

View File

@@ -189,36 +189,34 @@ export default class ObjectAPI {
/**
* Get a domain object.
*
* @method get
* @memberof module:openmct.ObjectProvider#
* @param {string} key the key for the domain object to load
* @param {AbortSignal} abortSignal (optional) signal to abort fetch requests
* @param {boolean} [forceRemote=false] defaults to false. If true, will skip cached and
* dirty/in-transaction objects use and the provider.get method
* @returns {Promise<DomainObject>} a promise which will resolve when the domain object
* @param {AbortController.signal} abortSignal (optional) signal to abort fetch requests
* @returns {Promise} a promise which will resolve when the domain object
* has been saved, or be rejected if it cannot be saved
*/
get(identifier, abortSignal, forceRemote = false) {
get(identifier, abortSignal) {
let keystring = this.makeKeyString(identifier);
if (!forceRemote) {
if (this.cache[keystring] !== undefined) {
return this.cache[keystring];
}
if (this.cache[keystring] !== undefined) {
return this.cache[keystring];
}
identifier = utils.parseKeyString(identifier);
identifier = utils.parseKeyString(identifier);
if (this.isTransactionActive()) {
let dirtyObject = this.transaction.getDirtyObject(identifier);
if (this.isTransactionActive()) {
let dirtyObject = this.transaction.getDirtyObject(identifier);
if (dirtyObject) {
return Promise.resolve(dirtyObject);
}
if (dirtyObject) {
return Promise.resolve(dirtyObject);
}
}
const provider = this.getProvider(identifier);
if (!provider) {
throw new Error(`No Provider Matched for keyString "${this.makeKeyString(identifier)}}"`);
throw new Error('No Provider Matched');
}
if (!provider.get) {
@@ -393,6 +391,7 @@ export default class ObjectAPI {
lastPersistedTime = domainObject.persisted;
const persistedTime = Date.now();
this.#mutate(domainObject, 'persisted', persistedTime);
savedObjectPromise = provider.update(domainObject);
}
@@ -400,7 +399,7 @@ export default class ObjectAPI {
savedObjectPromise.then(response => {
savedResolve(response);
}).catch((error) => {
if (!isNewObject) {
if (lastPersistedTime !== undefined) {
this.#mutate(domainObject, 'persisted', lastPersistedTime);
}
@@ -413,12 +412,11 @@ export default class ObjectAPI {
return result.catch(async (error) => {
if (error instanceof this.errors.Conflict) {
// Synchronized objects will resolve their own conflicts
if (this.SYNCHRONIZED_OBJECT_TYPES.includes(domainObject.type)) {
this.openmct.notifications.info(`Conflict detected while saving "${this.makeKeyString(domainObject.name)}", attempting to resolve`);
} else {
this.openmct.notifications.error(`Conflict detected while saving ${this.makeKeyString(domainObject.identifier)}`);
this.openmct.notifications.error(`Conflict detected while saving ${this.makeKeyString(domainObject.identifier)}`);
// Synchronized objects will resolve their own conflicts, so
// bypass the refresh here and throw the error.
if (!this.SYNCHRONIZED_OBJECT_TYPES.includes(domainObject.type)) {
if (this.isTransactionActive()) {
this.endTransaction();
}
@@ -738,46 +736,6 @@ export default class ObjectAPI {
}
}
/**
* Parse and construct an `objectPath` from a `navigationPath`.
*
* A `navigationPath` is a string of the form `"/browse/<keyString>/<keyString>/..."` that is used
* by the Open MCT router to navigate to a specific object.
*
* Throws an error if the `navigationPath` is malformed.
*
* @param {string} navigationPath
* @returns {DomainObject[]} objectPath
*/
async getRelativeObjectPath(navigationPath) {
if (!navigationPath.startsWith('/browse/')) {
throw new Error(`Malformed navigation path: "${navigationPath}"`);
}
navigationPath = navigationPath.replace('/browse/', '');
if (!navigationPath || navigationPath === '/') {
return [];
}
// Remove any query params and split on '/'
const keyStrings = navigationPath.split('?')?.[0].split('/');
if (keyStrings[0] !== 'ROOT') {
keyStrings.unshift('ROOT');
}
const objectPath = (await Promise.all(
keyStrings.map(
keyString => this.supportsMutation(keyString)
? this.getMutable(utils.parseKeyString(keyString))
: this.get(utils.parseKeyString(keyString))
)
)).reverse();
return objectPath;
}
isObjectPathToALink(domainObject, objectPath) {
return objectPath !== undefined
&& objectPath.length > 1

View File

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

View File

@@ -36,7 +36,6 @@ export default class TelemetryAPI {
this.formatMapCache = new WeakMap();
this.formatters = new Map();
this.limitProviders = [];
this.stalenessProviders = [];
this.metadataCache = new WeakMap();
this.metadataProviders = [new DefaultMetadataProvider(this.openmct)];
this.noRequestProviderForAllObjects = false;
@@ -115,10 +114,6 @@ export default class TelemetryAPI {
if (provider.supportsLimits) {
this.limitProviders.unshift(provider);
}
if (provider.supportsStaleness) {
this.stalenessProviders.unshift(provider);
}
}
/**
@@ -130,7 +125,7 @@ export default class TelemetryAPI {
return provider.supportsSubscribe.apply(provider, args);
}
return this.subscriptionProviders.find(supportsDomainObject);
return this.subscriptionProviders.filter(supportsDomainObject)[0];
}
/**
@@ -143,25 +138,25 @@ export default class TelemetryAPI {
return provider.supportsRequest.apply(provider, args);
}
return this.requestProviders.find(supportsDomainObject);
return this.requestProviders.filter(supportsDomainObject)[0];
}
/**
* @private
*/
#findMetadataProvider(domainObject) {
return this.metadataProviders.find((provider) => {
return provider.supportsMetadata(domainObject);
});
return this.metadataProviders.filter(function (p) {
return p.supportsMetadata(domainObject);
})[0];
}
/**
* @private
*/
#findLimitEvaluator(domainObject) {
return this.limitProviders.find((provider) => {
return provider.supportsLimits(domainObject);
});
return this.limitProviders.filter(function (p) {
return p.supportsLimits(domainObject);
})[0];
}
/**
@@ -356,101 +351,6 @@ export default class TelemetryAPI {
}.bind(this);
}
/**
* Subscribe to staleness updates for a specific domain object.
* The callback will be called whenever staleness changes.
*
* @method subscribeToStaleness
* @memberof module:openmct.TelemetryAPI~StalenessProvider#
* @param {module:openmct.DomainObject} domainObject the object
* to watch for staleness updates
* @param {Function} callback the callback to invoke with staleness data,
* as it is received: ex.
* {
* isStale: <Boolean>,
* timestamp: <timestamp>
* }
* @returns {Function} a function which may be called to terminate
* the subscription to staleness updates
*/
subscribeToStaleness(domainObject, callback) {
const provider = this.#findStalenessProvider(domainObject);
if (!this.stalenessSubscriberCache) {
this.stalenessSubscriberCache = {};
}
const keyString = objectUtils.makeKeyString(domainObject.identifier);
let stalenessSubscriber = this.stalenessSubscriberCache[keyString];
if (!stalenessSubscriber) {
stalenessSubscriber = this.stalenessSubscriberCache[keyString] = {
callbacks: [callback]
};
if (provider) {
stalenessSubscriber.unsubscribe = provider
.subscribeToStaleness(domainObject, (stalenessResponse) => {
stalenessSubscriber.callbacks.forEach((cb) => {
cb(stalenessResponse);
});
});
} else {
stalenessSubscriber.unsubscribe = () => {};
}
} else {
stalenessSubscriber.callbacks.push(callback);
}
return function unsubscribe() {
stalenessSubscriber.callbacks = stalenessSubscriber.callbacks.filter((cb) => {
return cb !== callback;
});
if (stalenessSubscriber.callbacks.length === 0) {
stalenessSubscriber.unsubscribe();
delete this.stalenessSubscriberCache[keyString];
}
}.bind(this);
}
/**
* Request telemetry staleness for a domain object.
*
* @method isStale
* @memberof module:openmct.TelemetryAPI~StalenessProvider#
* @param {module:openmct.DomainObject} domainObject the object
* which has associated telemetry staleness
* @returns {Promise.<StalenessResponseObject>} a promise for a StalenessResponseObject
* or undefined if no provider exists
*/
async isStale(domainObject) {
const provider = this.#findStalenessProvider(domainObject);
if (!provider) {
return;
}
const abortController = new AbortController();
const options = { signal: abortController.signal };
this.requestAbortControllers.add(abortController);
try {
const staleness = await provider.isStale(domainObject, options);
return staleness;
} finally {
this.requestAbortControllers.delete(abortController);
}
}
/**
* @private
*/
#findStalenessProvider(domainObject) {
return this.stalenessProviders.find((provider) => {
return provider.supportsStaleness(domainObject);
});
}
/**
* Get telemetry metadata for a given domain object. Returns a telemetry
* metadata manager which provides methods for interrogating telemetry
@@ -761,29 +661,6 @@ export default class TelemetryAPI {
* @memberof module:openmct.TelemetryAPI~
*/
/**
* Provides telemetry staleness data. To subscribe to telemetry stalenes,
* new StalenessProvider implementations should be
* [registered]{@link module:openmct.TelemetryAPI#addProvider}.
*
* @interface StalenessProvider
* @property {function} supportsStaleness receieves a domainObject and
* returns a boolean to indicate it will provide staleness
* @property {function} subscribeToStaleness receieves a domainObject to
* be subscribed to and a callback to invoke with a StalenessResponseObject
* @property {function} isStale an asynchronous method called with a domainObject
* and an options object which currently has an abort signal, ex.
* { signal: <AbortController.signal> }
* this method should return a current StalenessResponseObject
* @memberof module:openmct.TelemetryAPI~
*/
/**
* @typedef {object} StalenessResponseObject
* @property {Boolean} isStale boolean representing the staleness state
* @property {Number} timestamp Unix timestamp in milliseconds
*/
/**
* An interface for retrieving telemetry data associated with a domain
* object.

View File

@@ -291,6 +291,5 @@ export default class StatusAPI extends EventEmitter {
* The Status type
* @typedef {Object} Status
* @property {String} key - A unique identifier for this status
* @property {String} label - A human readable label for this status
* @property {Number} timestamp - The time that the status was set.
* @property {Number} label - A human readable label for this status
*/

View File

@@ -29,7 +29,7 @@
<td class="js-second-data">{{ formattedTimestamp }}</td>
<td
class="js-third-data"
:class="valueClasses"
:class="valueClass"
>{{ value }}</td>
<td
v-if="hasUnits"
@@ -63,12 +63,6 @@ export default {
hasUnits: {
type: Boolean,
requred: true
},
isStale: {
type: Boolean,
default() {
return false;
}
}
},
data() {
@@ -87,22 +81,14 @@ export default {
return this.formats[this.valueKey].format(this.datum);
},
valueClasses() {
let classes = [];
if (this.isStale) {
classes.push('is-stale');
valueClass() {
if (!this.datum) {
return '';
}
if (this.datum) {
const limit = this.limitEvaluator.evaluate(this.datum, this.valueMetadata);
const limit = this.limitEvaluator.evaluate(this.datum, this.valueMetadata);
if (limit) {
classes.push(limit.cssClass);
}
}
return classes;
return limit ? limit.cssClass : '';
},
formattedTimestamp() {

View File

@@ -21,10 +21,7 @@
*****************************************************************************/
<template>
<div
class="c-lad-table-wrapper u-style-receiver js-style-receiver"
:class="staleClass"
>
<div class="c-lad-table-wrapper u-style-receiver js-style-receiver">
<table class="c-table c-lad-table">
<thead>
<tr>
@@ -41,7 +38,6 @@
:domain-object="ladRow.domainObject"
:path-to-table="objectPath"
:has-units="hasUnits"
:is-stale="staleObjects.includes(ladRow.key)"
@rowContextClick="updateViewContext"
/>
</tbody>
@@ -50,9 +46,7 @@
</template>
<script>
import LadRow from './LADRow.vue';
import StalenessUtils from '@/utils/staleness';
export default {
components: {
@@ -72,8 +66,7 @@ export default {
data() {
return {
items: [],
viewContext: {},
staleObjects: []
viewContext: {}
};
},
computed: {
@@ -87,13 +80,6 @@ export default {
});
return itemsWithUnits.length !== 0;
},
staleClass() {
if (this.staleObjects.length !== 0) {
return 'is-stale';
}
return '';
}
},
mounted() {
@@ -102,17 +88,11 @@ export default {
this.composition.on('remove', this.removeItem);
this.composition.on('reorder', this.reorder);
this.composition.load();
this.stalenessSubscription = {};
},
destroyed() {
this.composition.off('add', this.addItem);
this.composition.off('remove', this.removeItem);
this.composition.off('reorder', this.reorder);
Object.values(this.stalenessSubscription).forEach(stalenessSubscription => {
stalenessSubscription.unsubscribe();
stalenessSubscription.stalenessUtils.destroy();
});
},
methods: {
addItem(domainObject) {
@@ -121,55 +101,23 @@ export default {
item.key = this.openmct.objects.makeKeyString(domainObject.identifier);
this.items.push(item);
this.stalenessSubscription[item.key] = {};
this.stalenessSubscription[item.key].stalenessUtils = new StalenessUtils(this.openmct, domainObject);
this.openmct.telemetry.isStale(domainObject).then((stalenessResponse) => {
if (stalenessResponse !== undefined) {
this.handleStaleness(item.key, stalenessResponse);
}
});
const stalenessSubscription = this.openmct.telemetry.subscribeToStaleness(domainObject, (stalenessResponse) => {
this.handleStaleness(item.key, stalenessResponse);
});
this.stalenessSubscription[item.key].unsubscribe = stalenessSubscription;
},
removeItem(identifier) {
const SKIP_CHECK = true;
const keystring = this.openmct.objects.makeKeyString(identifier);
const index = this.items.findIndex(item => keystring === item.key);
let index = this.items.findIndex(item => this.openmct.objects.makeKeyString(identifier) === item.key);
this.items.splice(index, 1);
this.stalenessSubscription[keystring].unsubscribe();
this.handleStaleness(keystring, { isStale: false }, SKIP_CHECK);
},
reorder(reorderPlan) {
const oldItems = this.items.slice();
let oldItems = this.items.slice();
reorderPlan.forEach((reorderEvent) => {
this.$set(this.items, reorderEvent.newIndex, oldItems[reorderEvent.oldIndex]);
});
},
metadataHasUnits(valueMetadatas) {
const metadataWithUnits = valueMetadatas.filter(metadatum => metadatum.unit);
let metadataWithUnits = valueMetadatas.filter(metadatum => metadatum.unit);
return metadataWithUnits.length > 0;
},
handleStaleness(id, stalenessResponse, skipCheck = false) {
if (skipCheck || this.stalenessSubscription[id].stalenessUtils.shouldUpdateStaleness(stalenessResponse)) {
const index = this.staleObjects.indexOf(id);
if (stalenessResponse.isStale) {
if (index === -1) {
this.staleObjects.push(id);
}
} else {
if (index !== -1) {
this.staleObjects.splice(index, 1);
}
}
}
},
updateViewContext(rowContext) {
this.viewContext.row = rowContext;
},

View File

@@ -21,50 +21,42 @@
*****************************************************************************/
<template>
<div
class="c-lad-table-wrapper u-style-receiver js-style-receiver"
:class="staleClass"
>
<table class="c-table c-lad-table">
<thead>
<tr>
<th>Name</th>
<th>Timestamp</th>
<th>Value</th>
<th v-if="hasUnits">Unit</th>
</tr>
</thead>
<tbody>
<template
v-for="ladTable in ladTableObjects"
<table class="c-table c-lad-table">
<thead>
<tr>
<th>Name</th>
<th>Timestamp</th>
<th>Value</th>
<th v-if="hasUnits">Unit</th>
</tr>
</thead>
<tbody>
<template
v-for="ladTable in ladTableObjects"
>
<tr
:key="ladTable.key"
class="c-table__group-header js-lad-table-set__table-headers"
>
<tr
:key="ladTable.key"
class="c-table__group-header js-lad-table-set__table-headers"
>
<td colspan="10">
{{ ladTable.domainObject.name }}
</td>
</tr>
<lad-row
v-for="ladRow in ladTelemetryObjects[ladTable.key]"
:key="ladRow.key"
:domain-object="ladRow.domainObject"
:path-to-table="ladTable.objectPath"
:has-units="hasUnits"
:is-stale="staleObjects.includes(ladRow.key)"
@rowContextClick="updateViewContext"
/>
</template>
</tbody>
</table>
</div>
<td colspan="10">
{{ ladTable.domainObject.name }}
</td>
</tr>
<lad-row
v-for="ladRow in ladTelemetryObjects[ladTable.key]"
:key="ladRow.key"
:domain-object="ladRow.domainObject"
:path-to-table="ladTable.objectPath"
:has-units="hasUnits"
@rowContextClick="updateViewContext"
/>
</template>
</tbody>
</table>
</template>
<script>
import LadRow from './LADRow.vue';
import StalenessUtils from '@/utils/staleness';
export default {
components: {
@@ -82,8 +74,7 @@ export default {
ladTableObjects: [],
ladTelemetryObjects: {},
compositions: [],
viewContext: {},
staleObjects: []
viewContext: {}
};
},
computed: {
@@ -104,13 +95,6 @@ export default {
}
return false;
},
staleClass() {
if (this.staleObjects.length !== 0) {
return 'is-stale';
}
return '';
}
},
mounted() {
@@ -119,8 +103,6 @@ export default {
this.composition.on('remove', this.removeLadTable);
this.composition.on('reorder', this.reorderLadTables);
this.composition.load();
this.stalenessSubscription = {};
},
destroyed() {
this.composition.off('add', this.addLadTable);
@@ -130,11 +112,6 @@ export default {
c.composition.off('add', c.addCallback);
c.composition.off('remove', c.removeCallback);
});
Object.values(this.stalenessSubscription).forEach(stalenessSubscription => {
stalenessSubscription.unsubscribe();
stalenessSubscription.stalenessUtils.destroy();
});
},
methods: {
addLadTable(domainObject) {
@@ -183,57 +160,18 @@ export default {
telemetryObjects.push(telemetryObject);
this.$set(this.ladTelemetryObjects, ladTable.key, telemetryObjects);
// if tracking already, possibly in another table, return
if (this.stalenessSubscription[telemetryObject.key]) {
return;
} else {
this.stalenessSubscription[telemetryObject.key] = {};
this.stalenessSubscription[telemetryObject.key].stalenessUtils = new StalenessUtils(this.openmct, domainObject);
}
this.openmct.telemetry.isStale(domainObject).then((stalenessResponse) => {
if (stalenessResponse !== undefined) {
this.handleStaleness(telemetryObject.key, stalenessResponse);
}
});
const stalenessSubscription = this.openmct.telemetry.subscribeToStaleness(domainObject, (stalenessResponse) => {
this.handleStaleness(telemetryObject.key, stalenessResponse);
});
this.stalenessSubscription[telemetryObject.key].unsubscribe = stalenessSubscription;
};
},
removeTelemetryObject(ladTable) {
return (identifier) => {
const SKIP_CHECK = true;
const keystring = this.openmct.objects.makeKeyString(identifier);
let telemetryObjects = this.ladTelemetryObjects[ladTable.key];
let index = telemetryObjects.findIndex(telemetryObject => keystring === telemetryObject.key);
let index = telemetryObjects.findIndex(telemetryObject => this.openmct.objects.makeKeyString(identifier) === telemetryObject.key);
telemetryObjects.splice(index, 1);
this.$set(this.ladTelemetryObjects, ladTable.key, telemetryObjects);
this.stalenessSubscription[keystring].unsubscribe();
this.stalenessSubscription[keystring].stalenessUtils.destroy();
this.handleStaleness(keystring, { isStale: false }, SKIP_CHECK);
};
},
handleStaleness(id, stalenessResponse, skipCheck = false) {
if (skipCheck || this.stalenessSubscription[id].stalenessUtils.shouldUpdateStaleness(stalenessResponse)) {
const index = this.staleObjects.indexOf(id);
if (stalenessResponse.isStale) {
if (index === -1) {
this.staleObjects.push(id);
}
} else {
if (index !== -1) {
this.staleObjects.splice(index, 1);
}
}
}
},
updateViewContext(rowContext) {
this.viewContext.row = rowContext;
},

View File

@@ -26,7 +26,7 @@ import TelemetryCriterion from "./criterion/TelemetryCriterion";
import { evaluateResults } from './utils/evaluator';
import { getLatestTimestamp } from './utils/time';
import AllTelemetryCriterion from "./criterion/AllTelemetryCriterion";
import { TRIGGER_CONJUNCTION, TRIGGER_LABEL } from "./utils/constants";
import {TRIGGER_CONJUNCTION, TRIGGER_LABEL} from "./utils/constants";
/*
* conditionConfiguration = {
@@ -160,8 +160,7 @@ export default class Condition extends EventEmitter {
}
criterion.on('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
criterion.on('telemetryIsOld', (obj) => this.handleOldTelemetryCriterion(obj));
criterion.on('telemetryStaleness', () => this.handleTelemetryStaleness());
criterion.on('telemetryIsStale', (obj) => this.handleStaleCriterion(obj));
if (!this.criteria) {
this.criteria = [];
}
@@ -192,14 +191,12 @@ export default class Condition extends EventEmitter {
const newCriterionConfiguration = this.generateCriterion(criterionConfiguration);
let newCriterion = new TelemetryCriterion(newCriterionConfiguration, this.openmct);
newCriterion.on('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
newCriterion.on('telemetryIsOld', (obj) => this.handleOldTelemetryCriterion(obj));
newCriterion.on('telemetryStaleness', () => this.handleTelemetryStaleness());
newCriterion.on('telemetryIsStale', (obj) => this.handleStaleCriterion(obj));
let criterion = found.item;
criterion.unsubscribe();
criterion.off('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
criterion.off('telemetryIsOld', (obj) => this.handleOldTelemetryCriterion(obj));
newCriterion.off('telemetryStaleness', () => this.handleTelemetryStaleness());
criterion.off('telemetryIsStale', (obj) => this.handleStaleCriterion(obj));
this.criteria.splice(found.index, 1, newCriterion);
}
}
@@ -208,9 +205,12 @@ export default class Condition extends EventEmitter {
let found = this.findCriterion(id);
if (found) {
let criterion = found.item;
criterion.off('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
criterion.off('telemetryIsOld', (obj) => this.handleOldTelemetryCriterion(obj));
criterion.off('telemetryStaleness', () => this.handleTelemetryStaleness());
criterion.off('criterionUpdated', (obj) => {
this.handleCriterionUpdated(obj);
});
criterion.off('telemetryIsStale', (obj) => {
this.handleStaleCriterion(obj);
});
criterion.destroy();
this.criteria.splice(found.index, 1);
@@ -227,7 +227,7 @@ export default class Condition extends EventEmitter {
}
}
handleOldTelemetryCriterion(updatedCriterion) {
handleStaleCriterion(updatedCriterion) {
this.result = evaluateResults(this.criteria.map(criterion => criterion.result), this.trigger);
let latestTimestamp = {};
latestTimestamp = getLatestTimestamp(
@@ -239,11 +239,6 @@ export default class Condition extends EventEmitter {
this.conditionManager.updateCurrentCondition(latestTimestamp);
}
handleTelemetryStaleness() {
this.result = evaluateResults(this.criteria.map(criterion => criterion.result), this.trigger);
this.conditionManager.updateCurrentCondition();
}
updateDescription() {
const triggerDescription = this.getTriggerDescription();
let description = '';

View File

@@ -82,10 +82,8 @@
</template>
<script>
import Condition from './Condition.vue';
import ConditionManager from '../ConditionManager';
import StalenessUtils from '@/utils/staleness';
export default {
components: {
@@ -141,13 +139,6 @@ export default {
if (this.stopObservingForChanges) {
this.stopObservingForChanges();
}
if (this.stalenessSubscription) {
Object.values(this.stalenessSubscription).forEach(stalenessSubscription => {
stalenessSubscription.unsubscribe();
stalenessSubscription.stalenessUtils.destroy();
});
}
},
mounted() {
this.composition = this.openmct.composition.get(this.domainObject);
@@ -159,7 +150,6 @@ export default {
this.conditionManager = new ConditionManager(this.domainObject, this.openmct);
this.conditionManager.on('conditionSetResultUpdated', this.handleConditionSetResultUpdated);
this.updateDefaultCondition();
this.stalenessSubscription = {};
},
methods: {
handleConditionSetResultUpdated(data) {
@@ -220,57 +210,19 @@ export default {
return arr;
},
addTelemetryObject(domainObject) {
const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
this.telemetryObjs.push(domainObject);
this.$emit('telemetryUpdated', this.telemetryObjs);
if (!this.stalenessSubscription[keyString]) {
this.stalenessSubscription[keyString] = {};
}
this.stalenessSubscription[keyString].stalenessUtils = new StalenessUtils(this.openmct, domainObject);
this.openmct.telemetry.isStale(domainObject).then((stalenessResponse) => {
this.hanldeStaleness(keyString, stalenessResponse);
});
const stalenessSubscription = this.openmct.telemetry.subscribeToStaleness(domainObject, (stalenessResponse) => {
this.hanldeStaleness(keyString, stalenessResponse);
});
this.stalenessSubscription[keyString].unsubscribe = stalenessSubscription;
},
removeTelemetryObject(identifier) {
const keyString = this.openmct.objects.makeKeyString(identifier);
const index = this.telemetryObjs.findIndex(obj => {
let index = this.telemetryObjs.findIndex(obj => {
let objId = this.openmct.objects.makeKeyString(obj.identifier);
let id = this.openmct.objects.makeKeyString(identifier);
return objId === keyString;
return objId === id;
});
if (index > -1) {
this.telemetryObjs.splice(index, 1);
}
if (this.stalenessSubscription[keyString]) {
this.stalenessSubscription[keyString].unsubscribe();
this.stalenessSubscription[keyString].stalenessUtils.destroy();
this.emitStaleness({
keyString,
isStale: false
});
}
},
hanldeStaleness(keyString, stalenessResponse) {
if (this.stalenessSubscription[keyString].stalenessUtils.shouldUpdateStaleness(stalenessResponse)) {
this.emitStaleness({
keyString,
isStale: stalenessResponse.isStale
});
}
},
emitStaleness(stalenessObject) {
this.$emit('telemetryStaleness', stalenessObject);
},
addCondition() {
this.conditionManager.addCondition();

View File

@@ -21,10 +21,7 @@
*****************************************************************************/
<template>
<div
class="c-cs"
:class="{'is-stale': isStale }"
>
<div class="c-cs">
<section class="c-cs__current-output c-section">
<div class="c-cs__content c-cs__current-output-value">
<span class="c-cs__current-output-value__label">Current Output</span>
@@ -53,7 +50,6 @@
@conditionSetResultUpdated="updateCurrentOutput"
@updateDefaultOutput="updateDefaultOutput"
@telemetryUpdated="updateTelemetry"
@telemetryStaleness="handleStaleness"
/>
</div>
</div>
@@ -77,15 +73,9 @@ export default {
currentConditionOutput: '',
defaultConditionOutput: '',
telemetryObjs: [],
testData: {},
staleObjects: []
testData: {}
};
},
computed: {
isStale() {
return this.staleObjects.length !== 0;
}
},
mounted() {
this.conditionSetIdentifier = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.testData = {
@@ -105,18 +95,6 @@ export default {
},
updateTestData(testData) {
this.testData = testData;
},
handleStaleness({ keyString, isStale }) {
const index = this.staleObjects.indexOf(keyString);
if (isStale) {
if (index === -1) {
this.staleObjects.push(keyString);
}
} else {
if (index !== -1) {
this.staleObjects.splice(index, 1);
}
}
}
}
};

View File

@@ -94,7 +94,7 @@
>
<span v-if="inputIndex < inputCount-1">and</span>
</span>
<span v-if="criterion.metadata === 'dataReceived' && criterion.operation.name === IS_OLD_KEY">seconds</span>
<span v-if="criterion.metadata === 'dataReceived'">seconds</span>
</template>
<span v-else>
<span
@@ -122,7 +122,7 @@
<script>
import { OPERATIONS, INPUT_TYPES } from '../utils/operations';
import { TRIGGER_CONJUNCTION, IS_OLD_KEY, IS_STALE_KEY } from "../utils/constants";
import {TRIGGER_CONJUNCTION} from "../utils/constants";
export default {
inject: ['openmct'],
@@ -153,8 +153,7 @@ export default {
rowLabel: '',
operationFormat: '',
enumerations: [],
inputTypes: INPUT_TYPES,
IS_OLD_KEY
inputTypes: INPUT_TYPES
};
},
computed: {
@@ -165,7 +164,7 @@ export default {
},
filteredOps: function () {
if (this.criterion.metadata === 'dataReceived') {
return this.operations.filter(op => op.name === IS_OLD_KEY || op.name === IS_STALE_KEY);
return this.operations.filter(op => op.name === 'isStale');
} else {
return this.operations.filter(op => op.appliesTo.indexOf(this.operationFormat) !== -1);
}

View File

@@ -54,10 +54,6 @@
height: 100%;
overflow: hidden;
&.is-stale {
@include isStaleHolder();
}
/************************** CONDITION SET LAYOUT */
&__current-output {
flex: 0 0 auto;

View File

@@ -21,9 +21,8 @@
*****************************************************************************/
import TelemetryCriterion from './TelemetryCriterion';
import StalenessUtils from '@/utils/staleness';
import { evaluateResults } from "../utils/evaluator";
import { getLatestTimestamp, checkIfOld } from '../utils/time';
import {getLatestTimestamp, subscribeForStaleness} from '../utils/time';
import { getOperatorText } from "@/plugins/condition/utils/operations";
export default class AllTelemetryCriterion extends TelemetryCriterion {
@@ -39,41 +38,13 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
initialize() {
this.telemetryObjects = { ...this.telemetryDomainObjectDefinition.telemetryObjects };
this.telemetryDataCache = {};
if (this.isValid() && this.isOldCheck() && this.isValidInput()) {
this.checkForOldData(this.telemetryObjects || {});
}
if (this.isValid() && this.isStalenessCheck()) {
this.subscribeToStaleness(this.telemetryObjects || {});
if (this.isValid() && this.isStalenessCheck() && this.isValidInput()) {
this.subscribeForStaleData(this.telemetryObjects || {});
}
}
checkForOldData(telemetryObjects) {
if (!this.ageCheck) {
this.ageCheck = {};
}
subscribeForStaleData(telemetryObjects) {
Object.values(telemetryObjects).forEach((telemetryObject) => {
const id = this.openmct.objects.makeKeyString(telemetryObject.identifier);
if (!this.ageCheck[id]) {
this.ageCheck[id] = checkIfOld((data) => {
this.handleOldTelemetry(id, data);
}, this.input[0] * 1000);
}
});
}
handleOldTelemetry(id, data) {
if (this.telemetryDataCache) {
this.telemetryDataCache[id] = true;
this.result = evaluateResults(Object.values(this.telemetryDataCache), this.telemetry);
}
this.emitEvent('telemetryIsOld', data);
}
subscribeToStaleness(telemetryObjects) {
if (!this.stalenessSubscription) {
this.stalenessSubscription = {};
}
@@ -81,27 +52,20 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
Object.values(telemetryObjects).forEach((telemetryObject) => {
const id = this.openmct.objects.makeKeyString(telemetryObject.identifier);
if (!this.stalenessSubscription[id]) {
this.stalenessSubscription[id] = {};
this.stalenessSubscription[id].stalenessUtils = new StalenessUtils(this.openmct, telemetryObject);
this.stalenessSubscription[id].unsubscribe = this.openmct.telemetry.subscribeToStaleness(
telemetryObject,
(stalenessResponse) => {
this.handleStaleTelemetry(id, stalenessResponse);
}
);
this.stalenessSubscription[id] = subscribeForStaleness((data) => {
this.handleStaleTelemetry(id, data);
}, this.input[0] * 1000);
}
});
}
handleStaleTelemetry(id, stalenessResponse) {
handleStaleTelemetry(id, data) {
if (this.telemetryDataCache) {
if (this.stalenessSubscription[id].stalenessUtils.shouldUpdateStaleness(stalenessResponse)) {
this.telemetryDataCache[id] = stalenessResponse.isStale;
this.result = evaluateResults(Object.values(this.telemetryDataCache), this.telemetry);
this.emitEvent('telemetryStaleness');
}
this.telemetryDataCache[id] = true;
this.result = evaluateResults(Object.values(this.telemetryDataCache), this.telemetry);
}
this.emitEvent('telemetryIsStale', data);
}
isValid() {
@@ -111,13 +75,8 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
updateTelemetryObjects(telemetryObjects) {
this.telemetryObjects = { ...telemetryObjects };
this.removeTelemetryDataCache();
if (this.isValid() && this.isOldCheck() && this.isValidInput()) {
this.checkForOldData(this.telemetryObjects || {});
}
if (this.isValid() && this.isStalenessCheck()) {
this.subscribeToStaleness(this.telemetryObjects || {});
if (this.isValid() && this.isStalenessCheck() && this.isValidInput()) {
this.subscribeForStaleData(this.telemetryObjects || {});
}
}
@@ -132,9 +91,6 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
});
telemetryCacheIds.forEach(id => {
delete (this.telemetryDataCache[id]);
delete (this.ageCheck[id]);
this.stalenessSubscription[id].unsubscribe();
this.stalenessSubscription[id].stalenessUtils.destroy();
delete (this.stalenessSubscription[id]);
});
}
@@ -169,10 +125,10 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
updateResult(data, telemetryObjects) {
const validatedData = this.isValid() ? data : {};
if (validatedData && !this.isStalenessCheck()) {
if (this.isOldCheck()) {
if (this.ageCheck?.[validatedData.id]) {
this.ageCheck[validatedData.id].update(validatedData);
if (validatedData) {
if (this.isStalenessCheck()) {
if (this.stalenessSubscription && this.stalenessSubscription[validatedData.id]) {
this.stalenessSubscription[validatedData.id].update(validatedData);
}
this.telemetryDataCache[validatedData.id] = false;
@@ -270,17 +226,9 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
destroy() {
delete this.telemetryObjects;
delete this.telemetryDataCache;
if (this.ageCheck) {
Object.values(this.ageCheck).forEach((subscription) => subscription.clear);
delete this.ageCheck;
}
if (this.stalenessSubscription) {
Object.values(this.stalenessSubscription).forEach(subscription => {
subscription.unsubscribe();
subscription.stalenessUtils.destroy();
});
Object.values(this.stalenessSubscription).forEach((subscription) => subscription.clear);
delete this.stalenessSubscription;
}
}
}

View File

@@ -21,10 +21,8 @@
*****************************************************************************/
import EventEmitter from 'EventEmitter';
import StalenessUtils from '@/utils/staleness';
import { IS_OLD_KEY, IS_STALE_KEY } from '../utils/constants';
import { OPERATIONS, getOperatorText } from '../utils/operations';
import { checkIfOld } from "../utils/time";
import { subscribeForStaleness } from "../utils/time";
export default class TelemetryCriterion extends EventEmitter {
@@ -46,8 +44,7 @@ export default class TelemetryCriterion extends EventEmitter {
this.input = telemetryDomainObjectDefinition.input;
this.metadata = telemetryDomainObjectDefinition.metadata;
this.result = undefined;
this.ageCheck = undefined;
this.unsubscribeFromStaleness = undefined;
this.stalenessSubscription = undefined;
this.initialize();
this.emitEvent('criterionUpdated', this);
@@ -60,13 +57,8 @@ export default class TelemetryCriterion extends EventEmitter {
}
this.updateTelemetryObjects(this.telemetryDomainObjectDefinition.telemetryObjects);
if (this.isValid() && this.isOldCheck() && this.isValidInput()) {
this.checkForOldData();
}
if (this.isValid() && this.isStalenessCheck()) {
this.subscribeToStaleness();
if (this.isValid() && this.isStalenessCheck() && this.isValidInput()) {
this.subscribeForStaleData();
}
}
@@ -74,52 +66,25 @@ export default class TelemetryCriterion extends EventEmitter {
return this.telemetryObjectIdAsString && (this.telemetryObjectIdAsString === id);
}
checkForOldData() {
if (this.ageCheck) {
this.ageCheck.clear();
subscribeForStaleData() {
if (this.stalenessSubscription) {
this.stalenessSubscription.clear();
}
this.ageCheck = checkIfOld(this.handleOldTelemetry.bind(this), this.input[0] * 1000);
this.stalenessSubscription = subscribeForStaleness(this.handleStaleTelemetry.bind(this), this.input[0] * 1000);
}
handleOldTelemetry(data) {
handleStaleTelemetry(data) {
this.result = true;
this.emitEvent('telemetryIsOld', data);
}
subscribeToStaleness() {
if (this.unsubscribeFromStaleness) {
this.unsubscribeFromStaleness();
}
if (!this.stalenessUtils) {
this.stalenessUtils = new StalenessUtils(this.openmct, this.telemetryObject);
}
this.openmct.telemetry.isStale(this.telemetryObject).then(this.handleStaleTelemetry.bind(this));
this.unsubscribeFromStaleness = this.openmct.telemetry.subscribeToStaleness(
this.telemetryObject,
this.handleStaleTelemetry.bind(this)
);
}
handleStaleTelemetry(stalenessResponse) {
if (stalenessResponse !== undefined && this.stalenessUtils.shouldUpdateStaleness(stalenessResponse)) {
this.result = stalenessResponse.isStale;
this.emitEvent('telemetryStaleness');
}
this.emitEvent('telemetryIsStale', data);
}
isValid() {
return this.telemetryObject && this.metadata && this.operation;
}
isOldCheck() {
return this.metadata && this.metadata === 'dataReceived' && this.operation === IS_OLD_KEY;
}
isStalenessCheck() {
return this.metadata && this.metadata === 'dataReceived' && this.operation === IS_STALE_KEY;
return this.metadata && this.metadata === 'dataReceived';
}
isValidInput() {
@@ -128,13 +93,8 @@ export default class TelemetryCriterion extends EventEmitter {
updateTelemetryObjects(telemetryObjects) {
this.telemetryObject = telemetryObjects[this.telemetryObjectIdAsString];
if (this.isValid() && this.isOldCheck() && this.isValidInput()) {
this.checkForOldData();
}
if (this.isValid() && this.isStalenessCheck()) {
this.subscribeToStaleness();
if (this.isValid() && this.isStalenessCheck() && this.isValidInput()) {
this.subscribeForStaleData();
}
}
@@ -170,17 +130,14 @@ export default class TelemetryCriterion extends EventEmitter {
updateResult(data) {
const validatedData = this.isValid() ? data : {};
if (!this.isStalenessCheck()) {
if (this.isOldCheck()) {
if (this.ageCheck) {
this.ageCheck.update(validatedData);
}
this.result = false;
} else {
this.result = this.computeResult(validatedData);
if (this.isStalenessCheck()) {
if (this.stalenessSubscription) {
this.stalenessSubscription.update(validatedData);
}
this.result = false;
} else {
this.result = this.computeResult(validatedData);
}
}
@@ -311,17 +268,8 @@ export default class TelemetryCriterion extends EventEmitter {
destroy() {
delete this.telemetryObject;
delete this.telemetryObjectIdAsString;
if (this.ageCheck) {
delete this.ageCheck;
}
if (this.stalenessUtils) {
this.stalenessUtils.destroy();
}
if (this.unsubscribeFromStaleness) {
this.unsubscribeFromStaleness();
if (this.stalenessSubscription) {
delete this.stalenessSubscription;
}
}
}

View File

@@ -28,7 +28,6 @@ import Vue from 'vue';
import {getApplicableStylesForItem} from "./utils/styleUtils";
import ConditionManager from "@/plugins/condition/ConditionManager";
import StyleRuleManager from "./StyleRuleManager";
import { IS_OLD_KEY } from "./utils/constants";
describe('the plugin', function () {
let conditionSetDefinition;
@@ -643,7 +642,7 @@ describe('the plugin', function () {
});
describe('the condition check if old', () => {
describe('the condition check for staleness', () => {
let conditionSetDomainObject;
beforeEach(() => {
@@ -661,13 +660,13 @@ describe('the plugin', function () {
"id": "39584410-cbf9-499e-96dc-76f27e69885d",
"configuration": {
"name": "Unnamed Condition",
"output": "Any old telemetry",
"output": "Any stale telemetry",
"trigger": "all",
"criteria": [
{
"id": "35400132-63b0-425c-ac30-8197df7d5862",
"telemetry": "any",
"operation": IS_OLD_KEY,
"operation": "isStale",
"input": [
"0.2"
],
@@ -675,7 +674,7 @@ describe('the plugin', function () {
}
]
},
"summary": "Match if all criteria are met: Any telemetry is old after 5 seconds"
"summary": "Match if all criteria are met: Any telemetry is stale after 5 seconds"
},
{
"isDefault": true,
@@ -709,7 +708,7 @@ describe('the plugin', function () {
};
});
it('should evaluate as old when telemetry is not received in the allotted time', (done) => {
it('should evaluate as stale when telemetry is not received in the allotted time', (done) => {
let conditionMgr = new ConditionManager(conditionSetDomainObject, openmct);
conditionMgr.on('conditionSetResultUpdated', mockListener);
conditionMgr.telemetryObjects = {
@@ -718,7 +717,7 @@ describe('the plugin', function () {
conditionMgr.updateConditionTelemetryObjects();
setTimeout(() => {
expect(mockListener).toHaveBeenCalledWith({
output: 'Any old telemetry',
output: 'Any stale telemetry',
id: {
namespace: '',
key: 'cf4456a9-296a-4e6b-b182-62ed29cd15b9'
@@ -730,7 +729,7 @@ describe('the plugin', function () {
}, 400);
});
it('should not evaluate as old when telemetry is received in the allotted time', (done) => {
it('should not evaluate as stale when telemetry is received in the allotted time', (done) => {
const date = 1;
conditionSetDomainObject.configuration.conditionCollection[0].configuration.criteria[0].input = ["0.4"];
let conditionMgr = new ConditionManager(conditionSetDomainObject, openmct);

View File

@@ -59,6 +59,3 @@ export const ERROR = {
errorText: 'Condition not found'
}
};
export const IS_OLD_KEY = 'isStale';
export const IS_STALE_KEY = 'isStale.new';

View File

@@ -20,8 +20,6 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { IS_OLD_KEY, IS_STALE_KEY } from "./constants";
function convertToNumbers(input) {
let numberInputs = [];
input.forEach(inputValue => numberInputs.push(Number(inputValue)));
@@ -297,7 +295,7 @@ export const OPERATIONS = [
}
},
{
name: IS_OLD_KEY,
name: 'isStale',
operation: function () {
return false;
},
@@ -307,18 +305,6 @@ export const OPERATIONS = [
getDescription: function (values) {
return ` is older than ${values[0] || ''} seconds`;
}
},
{
name: IS_STALE_KEY,
operation: function () {
return false;
},
text: 'is stale',
appliesTo: ["number"],
inputCount: 0,
getDescription: function () {
return ' is stale';
}
}
];
@@ -330,5 +316,5 @@ export const INPUT_TYPES = {
export function getOperatorText(operationName, values) {
const found = OPERATIONS.find((operation) => operation.name === operationName);
return found?.getDescription(values) ?? '';
return found ? found.getDescription(values) : '';
}

View File

@@ -51,26 +51,26 @@ export function getLatestTimestamp(
return latest;
}
export function checkIfOld(callback, timeout) {
let oldCheckTimer = setTimeout(() => {
clearTimeout(oldCheckTimer);
export function subscribeForStaleness(callback, timeout) {
let stalenessTimer = setTimeout(() => {
clearTimeout(stalenessTimer);
callback();
}, timeout);
return {
update: (data) => {
if (oldCheckTimer) {
clearTimeout(oldCheckTimer);
if (stalenessTimer) {
clearTimeout(stalenessTimer);
}
oldCheckTimer = setTimeout(() => {
clearTimeout(oldCheckTimer);
stalenessTimer = setTimeout(() => {
clearTimeout(stalenessTimer);
callback(data);
}, timeout);
},
clear: () => {
if (oldCheckTimer) {
clearTimeout(oldCheckTimer);
if (stalenessTimer) {
clearTimeout(stalenessTimer);
}
}
};

View File

@@ -19,7 +19,7 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { checkIfOld } from "./time";
import { subscribeForStaleness } from "./time";
describe('time related utils', () => {
let subscription;
@@ -27,11 +27,11 @@ describe('time related utils', () => {
beforeEach(() => {
mockListener = jasmine.createSpy('listener');
subscription = checkIfOld(mockListener, 100);
subscription = subscribeForStaleness(mockListener, 100);
});
describe('check if old', () => {
it('should call listeners when old', (done) => {
describe('subscribe for staleness', () => {
it('should call listeners when stale', (done) => {
setTimeout(() => {
expect(mockListener).toHaveBeenCalled();
done();

View File

@@ -31,7 +31,7 @@
<div
v-if="domainObject"
class="c-telemetry-view u-style-receiver"
:class="[itemClasses]"
:class="[statusClass]"
:style="styleObject"
:data-font-size="item.fontSize"
:data-font="item.font"
@@ -73,7 +73,6 @@
<script>
import LayoutFrame from './LayoutFrame.vue';
import conditionalStylesMixin from "../mixins/objectStyles-mixin";
import stalenessMixin from '@/ui/mixins/staleness-mixin';
import { getDefaultNotebook, getNotebookSectionAndPage } from '@/plugins/notebook/utils/notebook-storage.js';
const DEFAULT_TELEMETRY_DIMENSIONS = [10, 5];
@@ -103,7 +102,7 @@ export default {
components: {
LayoutFrame
},
mixins: [conditionalStylesMixin, stalenessMixin],
mixins: [conditionalStylesMixin],
inject: ['openmct', 'objectPath', 'currentView'],
props: {
item: {
@@ -138,18 +137,8 @@ export default {
};
},
computed: {
itemClasses() {
let classes = [];
if (this.status) {
classes.push(`is-status--${this.status}`);
}
if (this.isStale) {
classes.push('is-stale');
}
return classes;
statusClass() {
return (this.status) ? `is-status--${this.status}` : '';
},
showLabel() {
let displayMode = this.item.displayMode;
@@ -321,7 +310,6 @@ export default {
this.removeSelectable = this.openmct.selection.selectable(
this.$el, this.context, this.immediatelySelect || this.initSelect);
delete this.immediatelySelect;
this.subscribeToStaleness(this.domainObject);
},
updateTelemetryFormat(format) {
this.customStringformatter.setFormat(format);

View File

@@ -25,12 +25,6 @@
margin-right: $interiorMargin;
}
&.is-stale {
.c-telemetry-view__value {
@include isStaleElement();
}
}
.c-frame & {
@include abs();
border: 1px solid transparent;

View File

@@ -31,7 +31,6 @@ export default class DuplicateAction {
this.priority = 7;
this.openmct = openmct;
this.transaction = null;
}
invoke(objectPath) {
@@ -46,9 +45,7 @@ export default class DuplicateAction {
.some(objectInPath => this.openmct.objects.areIdsEqual(objectInPath.identifier, this.object.identifier));
}
async onSave(changes) {
this.startTransaction();
onSave(changes) {
let inNavigationPath = this.inNavigationPath();
if (inNavigationPath && this.openmct.editor.isEditing()) {
this.openmct.editor.save();
@@ -62,9 +59,7 @@ export default class DuplicateAction {
const parentDomainObjectpath = changes.location || [this.parent];
const parent = parentDomainObjectpath[0];
await duplicationTask.duplicate(this.object, parent);
return this.saveTransaction();
return duplicationTask.duplicate(this.object, parent);
}
showForm(domainObject, parentDomainObject) {
@@ -147,20 +142,4 @@ export default class DuplicateAction {
&& parentType.definition.creatable
&& Array.isArray(parent.composition);
}
startTransaction() {
if (!this.openmct.objects.isTransactionActive()) {
this.transaction = this.openmct.objects.startTransaction();
}
}
async saveTransaction() {
if (!this.transaction) {
return;
}
await this.transaction.commit();
this.openmct.objects.endTransaction();
this.transaction = null;
}
}

View File

@@ -22,7 +22,7 @@
<template>
<div
class="c-gauge__wrapper js-gauge-wrapper"
:class="gaugeClasses"
:class="`c-gauge--${gaugeType}`"
:title="gaugeTitle"
>
<template v-if="typeDial">
@@ -347,14 +347,12 @@
<script>
import { DIAL_VALUE_DEG_OFFSET, getLimitDegree } from '../gauge-limit-util';
import stalenessMixin from '@/ui/mixins/staleness-mixin';
const LIMIT_PADDING_IN_PERCENT = 10;
const DEFAULT_CURRENT_VALUE = '--';
export default {
name: 'Gauge',
mixins: [stalenessMixin],
inject: ['openmct', 'domainObject', 'composition'],
data() {
let gaugeController = this.domainObject.configuration.gaugeController;
@@ -405,15 +403,6 @@ export default {
return VIEWBOX_STR.replace('X', this.digits * DIGITS_RATIO);
},
gaugeClasses() {
let classes = [`c-gauge--${this.gaugeType}`];
if (this.isStale) {
classes.push('is-stale');
}
return classes;
},
rangeFontSize() {
const CHAR_THRESHOLD = 3;
const START_PERC = 8.5;
@@ -564,8 +553,6 @@ export default {
this.telemetryObject = domainObject;
this.request();
this.subscribe();
this.subscribeToStaleness(domainObject);
},
addedToComposition(domainObject) {
if (this.telemetryObject) {
@@ -624,8 +611,6 @@ export default {
this.unsubscribe = null;
}
this.triggerUnsubscribeFromStaleness();
this.curVal = DEFAULT_CURRENT_VALUE;
this.formats = null;
this.limitHigh = '';

View File

@@ -32,15 +32,6 @@ $meterNeedleBorderRadius: 5px;
&__wrapper {
@include abs();
overflow: hidden;
&.is-stale {
@include isStaleHolder();
[class*=__current-value-text] {
fill: $colorTelemStale;
font-style: italic;
}
}
}
&__current-value-text-wrapper {

View File

@@ -45,35 +45,6 @@ export default class ImageryView {
});
}
getViewContext() {
if (!this.component) {
return {};
}
return this.component.$refs.ImageryContainer;
}
pause() {
const imageContext = this.getViewContext();
// persist previous pause value to return to after unpausing
this.previouslyPaused = imageContext.isPaused;
imageContext.thumbnailClicked(imageContext.focusedImageIndex);
}
unpause() {
const pausedStateBefore = this.previouslyPaused;
this.previouslyPaused = undefined; // clear value
const imageContext = this.getViewContext();
imageContext.paused(pausedStateBefore);
}
onPreviewModeChange({ isPreviewing } = {}) {
if (isPreviewing) {
this.pause();
} else {
this.unpause();
}
}
destroy() {
this.component.$destroy();
this.component = undefined;

View File

@@ -26,18 +26,19 @@
:style="`width: 100%; height: 100%`"
>
<CompassHUD
v-if="showCompassHUD"
v-if="hasCameraFieldOfView"
:sun-heading="sunHeading"
:camera-angle-of-view="cameraAngleOfView"
:camera-pan="cameraPan"
/>
<CompassRose
v-if="showCompassRose"
v-if="hasCameraFieldOfView"
:camera-angle-of-view="cameraAngleOfView"
:camera-pan="cameraPan"
:compass-rose-sizing-classes="compassRoseSizingClasses"
:heading="heading"
:sized-image-dimensions="sizedImageDimensions"
:sun-heading="sunHeading"
:transformations="transformations"
/>
</div>
</template>
@@ -46,12 +47,18 @@
import CompassHUD from './CompassHUD.vue';
import CompassRose from './CompassRose.vue';
const CAMERA_ANGLE_OF_VIEW = 70;
export default {
components: {
CompassHUD,
CompassRose
},
props: {
compassRoseSizingClasses: {
type: String,
required: true
},
image: {
type: Object,
required: true
@@ -62,19 +69,13 @@ export default {
}
},
computed: {
showCompassHUD() {
return this.hasCameraPan && this.cameraAngleOfView > 0;
},
showCompassRose() {
return (this.hasCameraPan || this.hasHeading) && this.cameraAngleOfView > 0;
hasCameraFieldOfView() {
return this.cameraPan !== undefined && this.cameraAngleOfView > 0;
},
// horizontal rotation from north in degrees
heading() {
return this.image.heading;
},
hasHeading() {
return this.heading !== undefined;
},
// horizontal rotation from north in degrees
sunHeading() {
return this.image.sunOrientation;
@@ -83,14 +84,8 @@ export default {
cameraPan() {
return this.image.cameraPan;
},
hasCameraPan() {
return this.cameraPan !== undefined;
},
cameraAngleOfView() {
return this.transformations?.cameraAngleOfView;
},
transformations() {
return this.image.transformations;
return CAMERA_ANGLE_OF_VIEW;
}
},
methods: {

View File

@@ -64,14 +64,14 @@
class="c-cr__edge"
width="100"
height="100"
fill="url(#gradient_edge)"
fill="url(#paint0_radial)"
/>
<rect
v-if="hasSunHeading"
class="c-cr__sun"
width="100"
height="100"
fill="url(#gradient_sun)"
fill="url(#paint1_radial)"
:style="sunHeadingStyle"
/>
@@ -107,26 +107,9 @@
height="100"
/>
</mask>
<!-- Equipment (spacecraft) body holder. Transforms relative to the camera position. -->
<g
v-if="hasHeading"
class="cr-vrover"
:style="camAngleAndPositionStyle"
>
<!-- Equipment body. Rotates relative to the camera gimbal value for cams that gimbal. -->
<path
class="cr-vrover__body"
:style="camGimbalAngleStyle"
fill-rule="evenodd"
clip-rule="evenodd"
d="M5 0C2.23858 0 0 2.23858 0 5V95C0 97.7614 2.23858 100 5 100H95C97.7614 100 100 97.7614 100 95V5C100 2.23858 97.7614 0 95 0H5ZM85 59L50 24L15 59H33V75H67.0455V59H85Z"
/>
</g>
<g
class="c-cr__cam-fov"
:style="cameraHeadingStyle"
:style="cameraPanStyle"
>
<g mask="url(#mask2)">
<rect
@@ -145,13 +128,19 @@
:style="cameraFOVStyleLeftHalf"
/>
</g>
<polygon
class="c-cr__cam"
points="0,0 100,0 70,40 70,100 30,100 30,40"
/>
</g>
</g>
<!-- Spacecraft body -->
<path
v-if="hasHeading"
class="c-cr__spacecraft-body"
fill-rule="evenodd"
clip-rule="evenodd"
d="M37 49C35.3431 49 34 50.3431 34 52V82C34 83.6569 35.3431 85 37 85H63C64.6569 85 66 83.6569 66 82V52C66 50.3431 64.6569 49 63 49H37ZM50 52L58 60H55V67H45V60H42L50 52Z"
:style="headingStyle"
/>
<!-- NSEW and ticks -->
<g
class="c-cr__nsew"
@@ -204,7 +193,7 @@
</g>
<defs>
<radialGradient
id="gradient_edge"
id="paint0_radial"
cx="0"
cy="0"
r="1"
@@ -212,7 +201,7 @@
gradientTransform="translate(50 50) rotate(90) scale(50)"
>
<stop
offset="0.6"
offset="0.751387"
stop-opacity="0"
/>
<stop
@@ -221,7 +210,7 @@
/>
</radialGradient>
<radialGradient
id="gradient_sun"
id="paint1_radial"
cx="0"
cy="0"
r="1"
@@ -229,17 +218,12 @@
gradientTransform="translate(50 -7) rotate(-90) scale(18.5)"
>
<stop
offset="0.7"
offset="0.716377"
stop-color="#FFCC00"
/>
<stop
offset="0.7"
stop-color="#FFCC00"
stop-opacity="0.6"
/>
<stop
offset="1"
stop-color="#FF6600"
stop-color="#FF9900"
stop-opacity="0"
/>
</radialGradient>
@@ -254,6 +238,10 @@ import { throttle } from 'lodash';
export default {
props: {
compassRoseSizingClasses: {
type: String,
required: true
},
heading: {
type: Number,
required: true,
@@ -265,13 +253,16 @@ export default {
type: Number,
default: undefined
},
cameraPan: {
cameraAngleOfView: {
type: Number,
default: undefined
},
transformations: {
type: Object,
default: undefined
cameraPan: {
type: Number,
required: true,
default() {
return 0;
}
},
sizedImageDimensions: {
type: Object,
@@ -284,38 +275,11 @@ export default {
};
},
computed: {
cameraHeading() {
return this.cameraPan ?? this.heading;
},
cameraAngleOfView() {
const cameraAngleOfView = this.transformations?.cameraAngleOfView;
if (!cameraAngleOfView) {
console.warn('No Camera Angle of View provided');
}
return cameraAngleOfView;
},
camAngleAndPositionStyle() {
const translateX = this.transformations?.translateX;
const translateY = this.transformations?.translateY;
const rotation = this.transformations?.rotation;
const scale = this.transformations?.scale;
return { transform: `translate(${translateX}%, ${translateY}%) rotate(${rotation}deg) scale(${scale})` };
},
camGimbalAngleStyle() {
const rotation = rotate(this.north, this.heading);
return {
transform: `rotate(${ rotation }deg)`
};
},
compassRoseStyle() {
return { transform: `rotate(${ this.north }deg)` };
},
north() {
return this.lockCompass ? rotate(-this.cameraHeading) : 0;
return this.lockCompass ? rotate(-this.cameraPan) : 0;
},
cardinalTextRotateN() {
return { transform: `translateY(-27%) rotate(${ -this.north }deg)` };
@@ -333,7 +297,6 @@ export default {
return this.heading !== undefined;
},
headingStyle() {
/* Replaced with computed camGimbalStyle, but left here just in case. */
const rotation = rotate(this.north, this.heading);
return {
@@ -350,8 +313,8 @@ export default {
transform: `rotate(${ rotation }deg)`
};
},
cameraHeadingStyle() {
const rotation = rotate(this.north, this.cameraHeading);
cameraPanStyle() {
const rotation = rotate(this.north, this.cameraPan);
return {
transform: `rotate(${ rotation }deg)`
@@ -370,24 +333,6 @@ export default {
return {
transform: `rotate(${ -this.cameraAngleOfView / 2 }deg)`
};
},
compassRoseSizingClasses() {
let compassRoseSizingClasses = '';
if (this.sizedImageWidth < 300) {
compassRoseSizingClasses = '--rose-small --rose-min';
} else if (this.sizedImageWidth < 500) {
compassRoseSizingClasses = '--rose-small';
} else if (this.sizedImageWidth > 1000) {
compassRoseSizingClasses = '--rose-max';
}
return compassRoseSizingClasses;
},
sizedImageWidth() {
return this.sizedImageDimensions.width;
},
sizedImageHeight() {
return this.sizedImageDimensions.height;
}
},
watch: {

View File

@@ -1,5 +1,5 @@
/***************************** THEME/UI CONSTANTS AND MIXINS */
$interfaceKeyColor: #fff;
$interfaceKeyColor: #00B9C5;
$elemBg: rgba(black, 0.7);
@mixin sun($position: 'circle closest-side') {
@@ -100,19 +100,13 @@ $elemBg: rgba(black, 0.7);
}
&__edge {
opacity: 0.2;
opacity: 0.1;
}
&__sun {
opacity: 0.7;
}
&__cam {
fill: $interfaceKeyColor;
transform-origin: center;
transform: scale(0.15);
}
&__cam-fov-l,
&__cam-fov-r {
// Cam FOV indication
@@ -121,6 +115,7 @@ $elemBg: rgba(black, 0.7);
}
&__nsew-text,
&__spacecraft-body,
&__ticks-major,
&__ticks-minor {
fill: $color;
@@ -171,15 +166,3 @@ $elemBg: rgba(black, 0.7);
padding-top: $s;
}
}
/************************** ROVER */
.cr-vrover {
$scale: 0.4;
transform-origin: center;
&__body {
fill: $interfaceKeyColor;
opacity: 0.3;
transform-origin: center 7% !important; // Places rotation center at mast position
}
}

View File

@@ -39,7 +39,7 @@
<img
ref="img"
class="c-thumb__image"
:src="`${image.thumbnailUrl || image.url}`"
:src="image.url"
fetchpriority="low"
@load="imageLoadCompleted"
>

View File

@@ -186,17 +186,17 @@ export default {
item.remove();
});
let imagery = this.$el.querySelectorAll(`.${IMAGE_WRAPPER_CLASS}`);
imagery.forEach(imageElm => {
imagery.forEach(item => {
if (clearAllImagery) {
imageElm.remove();
item.remove();
} else {
const id = imageElm.getAttributeNS(null, 'id');
const id = item.getAttributeNS(null, 'id');
if (id) {
const timestamp = id.replace(ID_PREFIX, '');
if (!this.isImageryInBounds({
time: timestamp
})) {
imageElm.remove();
item.remove();
}
}
}
@@ -343,25 +343,25 @@ export default {
imageElement.style.display = 'block';
}
},
updateExistingImageWrapper(existingImageWrapper, image, showImagePlaceholders) {
updateExistingImageWrapper(existingImageWrapper, item, showImagePlaceholders) {
//Update the x co-ordinates of the image wrapper and the url of image
//this is to avoid tearing down all elements completely and re-drawing them
this.setNSAttributesForElement(existingImageWrapper, {
'data-show-image-placeholders': showImagePlaceholders
});
existingImageWrapper.style.left = `${this.xScale(image.time)}px`;
existingImageWrapper.style.left = `${this.xScale(item.time)}px`;
let imageElement = existingImageWrapper.querySelector('img');
this.setNSAttributesForElement(imageElement, {
src: image.thumbnailUrl || image.url
src: item.url
});
this.setImageDisplay(imageElement, showImagePlaceholders);
},
createImageWrapper(index, image, showImagePlaceholders) {
const id = `${ID_PREFIX}${image.time}`;
createImageWrapper(index, item, showImagePlaceholders) {
const id = `${ID_PREFIX}${item.time}`;
let imageWrapper = document.createElement('div');
imageWrapper.classList.add(IMAGE_WRAPPER_CLASS);
imageWrapper.style.left = `${this.xScale(image.time)}px`;
imageWrapper.style.left = `${this.xScale(item.time)}px`;
this.setNSAttributesForElement(imageWrapper, {
id,
'data-show-image-placeholders': showImagePlaceholders
@@ -383,7 +383,7 @@ export default {
//create image element
let imageElement = document.createElement('img');
this.setNSAttributesForElement(imageElement, {
src: image.thumbnailUrl || image.url
src: item.url
});
imageElement.style.width = `${IMAGE_SIZE}px`;
imageElement.style.height = `${IMAGE_SIZE}px`;
@@ -392,7 +392,7 @@ export default {
//handle mousedown event to show the image in a large view
imageWrapper.addEventListener('mousedown', (e) => {
if (e.button === 0) {
this.expand(image.time);
this.expand(item.time);
}
});

View File

@@ -93,6 +93,7 @@
></div>
<Compass
v-if="shouldDisplayCompass"
:compass-rose-sizing-classes="compassRoseSizingClasses"
:image="focusedImage"
:natural-aspect-ratio="focusedImageNaturalAspectRatio"
:sized-image-dimensions="sizedImageDimensions"
@@ -171,7 +172,7 @@
>
<ImageThumbnail
v-for="(image, index) in imageHistory"
:key="`${image.thumbnailUrl || image.url}${image.time}`"
:key="image.url + image.time"
:image="image"
:active="focusedImageIndex === index"
:selected="focusedImageIndex === index && isPaused"
@@ -225,9 +226,6 @@ const SHOW_THUMBS_FULLSIZE_THRESHOLD_HEIGHT = 600;
const IMAGE_CONTAINER_BORDER_WIDTH = 1;
const DEFAULT_IMAGE_PAN_ALT_TEXT = "Alt drag to pan";
const LINUX_IMAGE_PAN_ALT_TEXT = `Ctrl+${DEFAULT_IMAGE_PAN_ALT_TEXT}`;
export default {
name: 'ImageryView',
components: {
@@ -300,6 +298,18 @@ export default {
};
},
computed: {
compassRoseSizingClasses() {
let compassRoseSizingClasses = '';
if (this.sizedImageWidth < 300) {
compassRoseSizingClasses = '--rose-small --rose-min';
} else if (this.sizedImageWidth < 500) {
compassRoseSizingClasses = '--rose-small';
} else if (this.sizedImageWidth > 1000) {
compassRoseSizingClasses = '--rose-max';
}
return compassRoseSizingClasses;
},
displayThumbnails() {
return (
this.forceShowThumbnails
@@ -311,8 +321,8 @@ export default {
},
focusImageStyles() {
return {
filter: `brightness(${this.filters.brightness}%) contrast(${this.filters.contrast}%)`,
backgroundImage:
'filter': `brightness(${this.filters.brightness}%) contrast(${this.filters.contrast}%)`,
'background-image':
`${this.imageUrl ? (
`url(${this.imageUrl}),
repeating-linear-gradient(
@@ -323,10 +333,10 @@ export default {
rgba(125,125,125,.2) 8px
)`
) : ''}`,
transform: `scale(${this.zoomFactor}) translate(${this.imageTranslateX}px, ${this.imageTranslateY}px)`,
transition: `${!this.pan && this.animateZoom ? 'transform 250ms ease-in' : 'initial'}`,
width: `${this.sizedImageWidth}px`,
height: `${this.sizedImageHeight}px`
'transform': `scale(${this.zoomFactor}) translate(${this.imageTranslateX}px, ${this.imageTranslateY}px)`,
'transition': `${!this.pan && this.animateZoom ? 'transform 250ms ease-in' : 'initial'}`,
'width': `${this.sizedImageWidth}px`,
'height': `${this.sizedImageHeight}px`
};
},
time() {
@@ -337,12 +347,11 @@ export default {
},
imageWrapperStyle() {
return {
cursorZoomIn: this.cursorStates.showCursorZoomIn,
cursorZoomOut: this.cursorStates.showCursorZoomOut,
pannable: this.cursorStates.isPannable,
paused: this.isPaused && !this.isFixed,
unsynced: this.isPaused && !this.isFixed,
stale: false
'cursor-zoom-in': this.cursorStates.showCursorZoomIn,
'cursor-zoom-out': this.cursorStates.showCursorZoomOut,
'pannable': this.cursorStates.isPannable,
'paused unnsynced': this.isPaused && !this.isFixed,
'stale': false
};
},
isImageNew() {
@@ -423,6 +432,7 @@ export default {
shouldDisplayCompass() {
const imageHeightAndWidth = this.sizedImageHeight !== 0
&& this.sizedImageWidth !== 0;
const display = this.focusedImage !== undefined
&& this.focusedImageNaturalAspectRatio !== undefined
&& this.imageContainerWidth !== undefined
@@ -430,9 +440,8 @@ export default {
&& imageHeightAndWidth
&& this.zoomFactor === 1
&& this.imagePanned !== true;
const hasCameraConfigurations = this.focusedImage?.transformations !== undefined;
return display && hasCameraConfigurations;
return display;
},
isSpacecraftPositionFresh() {
let isFresh = undefined;
@@ -519,10 +528,10 @@ export default {
const navigator = window.navigator.userAgent;
if (regexLinux.test(navigator)) {
return LINUX_IMAGE_PAN_ALT_TEXT;
return 'Ctrl+Alt drag to pan';
}
return DEFAULT_IMAGE_PAN_ALT_TEXT;
return 'Alt drag to pan';
},
viewableArea() {
if (this.zoomFactor === 1) {
@@ -617,7 +626,6 @@ export default {
this.spacecraftOrientationKeys = ['heading'];
this.cameraKeys = ['cameraPan', 'cameraTilt'];
this.sunKeys = ['sunOrientation'];
this.transformationsKeys = ['transformations'];
// related telemetry
await this.initializeRelatedTelemetry();
@@ -683,9 +691,9 @@ export default {
},
getVisibleLayerStyles(layer) {
return {
backgroundImage: `url(${layer.source})`,
transform: `scale(${this.zoomFactor}) translate(${this.imageTranslateX}px, ${this.imageTranslateY}px)`,
transition: `${!this.pan && this.animateZoom ? 'transform 250ms ease-in' : 'initial'}`
'background-image': `url(${layer.source})`,
'transform': `scale(${this.zoomFactor}) translate(${this.imageTranslateX}px, ${this.imageTranslateY}px)`,
'transition': `${!this.pan && this.animateZoom ? 'transform 250ms ease-in' : 'initial'}`
};
},
setTimeContext() {
@@ -713,20 +721,14 @@ export default {
&& visibleActions.find(action => action.key === 'large.view');
if (viewLargeAction && viewLargeAction.appliesTo(this.objectPath, this.currentView)) {
viewLargeAction.invoke(this.objectPath, this.currentView);
viewLargeAction.onItemClicked();
}
},
async initializeRelatedTelemetry() {
this.relatedTelemetry = new RelatedTelemetry(
this.openmct,
this.domainObject,
[
...this.spacecraftPositionKeys,
...this.spacecraftOrientationKeys,
...this.cameraKeys,
...this.sunKeys,
...this.transformationsKeys
]
[...this.spacecraftPositionKeys, ...this.spacecraftOrientationKeys, ...this.cameraKeys, ...this.sunKeys]
);
if (this.relatedTelemetry.hasRelatedTelemetry) {
@@ -763,30 +765,30 @@ export default {
return mostRecent[valueKey];
},
loadVisibleLayers() {
const layersMetadata = this.imageMetadataValue.layers;
if (!layersMetadata) {
return;
}
this.layers = layersMetadata;
if (this.domainObject.configuration) {
const persistedLayers = this.domainObject.configuration.layers;
layersMetadata.forEach((layer) => {
const persistedLayer = persistedLayers.find(object => object.name === layer.name);
if (persistedLayer) {
layer.visible = persistedLayer.visible === true;
}
});
this.visibleLayers = this.layers.filter(layer => layer.visible);
} else {
this.visibleLayers = [];
this.layers.forEach((layer) => {
layer.visible = false;
});
const metaDataValues = this.metadata.valuesForHints(['image'])[0];
this.imageFormat = this.openmct.telemetry.getValueFormatter(metaDataValues);
let layersMetadata = metaDataValues.layers;
if (layersMetadata) {
this.layers = layersMetadata;
if (this.domainObject.configuration) {
let persistedLayers = this.domainObject.configuration.layers;
layersMetadata.forEach((layer) => {
const persistedLayer = persistedLayers.find(object => object.name === layer.name);
if (persistedLayer) {
layer.visible = persistedLayer.visible === true;
}
});
this.visibleLayers = this.layers.filter(layer => layer.visible);
} else {
this.visibleLayers = [];
this.layers.forEach((layer) => {
layer.visible = false;
});
}
}
},
persistVisibleLayers() {
if (this.domainObject.configuration && this.openmct.objects.supportsMutation(this.domainObject.identifier)) {
if (this.domainObject.configuration) {
this.openmct.objects.mutate(this.domainObject, 'configuration.layers', this.layers);
}
@@ -835,15 +837,6 @@ export default {
this.$set(this.focusedImageRelatedTelemetry, key, value);
}
}
// set configuration for compass
this.transformationsKeys.forEach(key => {
const transformations = this.relatedTelemetry[key];
if (transformations !== undefined) {
this.$set(this.imageHistory[this.focusedImageIndex], key, transformations);
}
});
},
trackLatestRelatedTelemetry() {
[...this.spacecraftPositionKeys, ...this.spacecraftOrientationKeys, ...this.cameraKeys, ...this.sunKeys].forEach(key => {

View File

@@ -28,7 +28,6 @@ function copyRelatedMetadata(metadata) {
return copiedMetadata;
}
import IndependentTimeContext from "@/api/time/IndependentTimeContext";
export default class RelatedTelemetry {
constructor(openmct, domainObject, telemetryKeys) {
@@ -89,31 +88,9 @@ export default class RelatedTelemetry {
this[key].historicalDomainObject = await this._openmct.objects.get(this[key].historical.telemetryObjectId);
this[key].requestLatestFor = async (datum) => {
// We need to create a throwaway time context and pass it along
// as a request option. We do this to "trick" the Time API
// into thinking we are in fixed time mode in order to bypass this logic:
// https://github.com/akhenry/openmct-yamcs/blob/1060d42ebe43bf346dac0f6a8068cb288ade4ba4/src/providers/historical-telemetry-provider.js#L59
// Context: https://github.com/akhenry/openmct-yamcs/pull/217
const ephemeralContext = new IndependentTimeContext(
this._openmct,
this._openmct.time,
[this[key].historicalDomainObject]
);
// Stop following the global context, stop the clock,
// and set bounds.
ephemeralContext.resetContext();
const newBounds = {
start: this._openmct.time.bounds().start,
end: this._parseTime(datum)
};
ephemeralContext.stopClock();
ephemeralContext.bounds(newBounds);
const options = {
start: newBounds.start,
end: newBounds.end,
timeContext: ephemeralContext,
start: this._openmct.time.bounds().start,
end: this._parseTime(datum),
strategy: 'latest'
};
let results = await this._openmct.telemetry

View File

@@ -29,7 +29,7 @@
flex-direction: column;
flex: 1 1 auto;
&.unsynced{
&.unnsynced{
@include sUnsynced();
}

View File

@@ -21,9 +21,6 @@
*****************************************************************************/
const DEFAULT_DURATION_FORMATTER = 'duration';
const IMAGE_HINT_KEY = 'image';
const IMAGE_THUMBNAIL_HINT_KEY = 'thumbnail';
const IMAGE_DOWNLOAD_NAME_HINT_KEY = 'imageDownloadName';
export default {
inject: ['openmct', 'domainObject', 'objectPath'],
@@ -35,20 +32,13 @@ export default {
this.setDataTimeContext();
this.openmct.objectViews.on('clearData', this.dataCleared);
// Get metadata and formatters
// set
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
this.imageMetadataValue = { ...this.metadata.valuesForHints([IMAGE_HINT_KEY])[0] };
this.imageFormatter = this.getFormatter(this.imageMetadataValue.key);
this.imageThumbnailMetadataValue = { ...this.metadata.valuesForHints([IMAGE_THUMBNAIL_HINT_KEY])[0] };
this.imageThumbnailFormatter = this.imageThumbnailMetadataValue.key
? this.getFormatter(this.imageThumbnailMetadataValue.key)
: null;
this.imageHints = { ...this.metadata.valuesForHints(['image'])[0] };
this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
this.imageDownloadNameMetadataValue = { ...this.metadata.valuesForHints([IMAGE_DOWNLOAD_NAME_HINT_KEY])[0]};
this.imageFormatter = this.openmct.telemetry.getValueFormatter(this.imageHints);
this.imageDownloadNameHints = { ...this.metadata.valuesForHints(['imageDownloadName'])[0]};
// initialize
this.timeKey = this.timeSystem.key;
@@ -115,19 +105,12 @@ export default {
return this.imageFormatter.format(datum);
},
formatImageThumbnailUrl(datum) {
if (!datum || !this.imageThumbnailFormatter) {
return;
}
return this.imageThumbnailFormatter.format(datum);
},
formatTime(datum) {
if (!datum) {
return;
}
const dateTimeStr = this.timeFormatter.format(datum);
let dateTimeStr = this.timeFormatter.format(datum);
// Replace ISO "T" with a space to allow wrapping
return dateTimeStr.replace("T", " ");
@@ -135,7 +118,7 @@ export default {
getImageDownloadName(datum) {
let imageDownloadName = '';
if (datum) {
const key = this.imageDownloadNameMetadataValue.key;
const key = this.imageDownloadNameHints.key;
imageDownloadName = datum[key];
}
@@ -167,7 +150,6 @@ export default {
normalizeDatum(datum) {
const formattedTime = this.formatTime(datum);
const url = this.formatImageUrl(datum);
const thumbnailUrl = this.formatImageThumbnailUrl(datum);
const time = this.parseTime(formattedTime);
const imageDownloadName = this.getImageDownloadName(datum);
@@ -175,14 +157,13 @@ export default {
...datum,
formattedTime,
url,
thumbnailUrl,
time,
imageDownloadName
};
},
getFormatter(key) {
const metadataValue = this.metadata.value(key) || { format: key };
const valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
let metadataValue = this.metadata.value(key) || { format: key };
let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
return valueFormatter;
}

View File

@@ -35,10 +35,6 @@ const MAIN_IMAGE_CLASS = '.js-imageryView-image';
const NEW_IMAGE_CLASS = '.c-imagery__age.c-imagery--new';
const REFRESH_CSS_MS = 500;
function formatThumbnail(url) {
return url.replace('logo-openmct.svg', 'logo-nasa.svg');
}
function getImageInfo(doc) {
let imageElement = doc.querySelectorAll(MAIN_IMAGE_CLASS)[0];
let timestamp = imageElement.dataset.openmctImageTimestamp;
@@ -128,16 +124,6 @@ describe("The Imagery View Layouts", () => {
},
"source": "url"
},
{
"name": "Image Thumbnail",
"key": "thumbnail-url",
"format": "thumbnail",
"hints": {
"thumbnail": 1,
"priority": 3
},
"source": "url"
},
{
"name": "Name",
"key": "name",
@@ -214,11 +200,6 @@ describe("The Imagery View Layouts", () => {
originalRouterPath = openmct.router.path;
openmct.telemetry.addFormat({
key: 'thumbnail',
format: formatThumbnail
});
openmct.on('start', done);
openmct.startHeadless();
});
@@ -403,32 +384,15 @@ describe("The Imagery View Layouts", () => {
//Looks like we need Vue.nextTick here so that computed properties settle down
await Vue.nextTick();
const layerEls = parent.querySelectorAll('.js-layer-image');
console.log(layerEls);
expect(layerEls.length).toEqual(1);
});
it("should use the image thumbnailUrl for thumbnails", async () => {
await Vue.nextTick();
const fullSizeImageUrl = imageTelemetry[5].url;
const thumbnailUrl = formatThumbnail(imageTelemetry[5].url);
// Ensure thumbnails are shown w/ thumbnail Urls
const thumbnails = parent.querySelectorAll(`img[src='${thumbnailUrl}']`);
expect(thumbnails.length).toBeGreaterThan(0);
// Click a thumbnail
parent.querySelectorAll(`img[src='${thumbnailUrl}']`)[0].click();
await Vue.nextTick();
// Ensure full size image is shown w/ full size url
const fullSizeImages = parent.querySelectorAll(`img[src='${fullSizeImageUrl}']`);
expect(fullSizeImages.length).toBeGreaterThan(0);
});
it("should show the clicked thumbnail as the main image", async () => {
//Looks like we need Vue.nextTick here so that computed properties settle down
await Vue.nextTick();
const thumbnailUrl = formatThumbnail(imageTelemetry[5].url);
parent.querySelectorAll(`img[src='${thumbnailUrl}']`)[0].click();
const target = imageTelemetry[5].url;
parent.querySelectorAll(`img[src='${target}']`)[0].click();
await Vue.nextTick();
const imageInfo = getImageInfo(parent);
@@ -453,7 +417,7 @@ describe("The Imagery View Layouts", () => {
it("should show that an image is not new", async () => {
await Vue.nextTick();
const target = formatThumbnail(imageTelemetry[4].url);
const target = imageTelemetry[4].url;
parent.querySelectorAll(`img[src='${target}']`)[0].click();
await Vue.nextTick();

View File

@@ -30,7 +30,6 @@ export default class LinkAction {
this.priority = 7;
this.openmct = openmct;
this.transaction = null;
}
appliesTo(objectPath) {
@@ -49,9 +48,7 @@ export default class LinkAction {
}
onSave(changes) {
this.startTransaction();
const inNavigationPath = this.inNavigationPath();
let inNavigationPath = this.inNavigationPath();
if (inNavigationPath && this.openmct.editor.isEditing()) {
this.openmct.editor.save();
}
@@ -60,8 +57,6 @@ export default class LinkAction {
const parent = parentDomainObjectpath[0];
this.linkInNewParent(this.object, parent);
return this.saveTransaction();
}
linkInNewParent(child, newParent) {
@@ -133,19 +128,4 @@ export default class LinkAction {
return parentCandidate && this.openmct.composition.checkPolicy(parentCandidate, this.object);
};
}
startTransaction() {
if (!this.openmct.objects.isTransactionActive()) {
this.transaction = this.openmct.objects.startTransaction();
}
}
async saveTransaction() {
if (!this.transaction) {
return;
}
await this.transaction.commit();
this.openmct.objects.endTransaction();
this.transaction = null;
}
}

View File

@@ -29,7 +29,6 @@ export default class MoveAction {
this.priority = 7;
this.openmct = openmct;
this.transaction = null;
}
invoke(objectPath) {
@@ -61,8 +60,6 @@ export default class MoveAction {
}
async onSave(changes) {
this.startTransaction();
let inNavigationPath = this.inNavigationPath(this.object);
if (inNavigationPath && this.openmct.editor.isEditing()) {
this.openmct.editor.save();
@@ -102,8 +99,6 @@ export default class MoveAction {
}
}
await this.saveTransaction();
this.navigateTo(newObjectPath);
}
@@ -194,20 +189,4 @@ export default class MoveAction {
&& childType.definition.creatable
&& Array.isArray(parent.composition);
}
startTransaction() {
if (!this.openmct.objects.isTransactionActive()) {
this.transaction = this.openmct.objects.startTransaction();
}
}
async saveTransaction() {
if (!this.transaction) {
return;
}
await this.transaction.commit();
this.openmct.objects.endTransaction();
this.transaction = null;
}
}

View File

@@ -25,14 +25,13 @@ import Notebook from './components/Notebook.vue';
import Agent from '@/utils/agent/Agent';
export default class NotebookViewProvider {
constructor(openmct, name, key, type, cssClass, snapshotContainer, entryUrlWhitelist) {
constructor(openmct, name, key, type, cssClass, snapshotContainer) {
this.openmct = openmct;
this.key = key;
this.name = `${name} View`;
this.type = type;
this.cssClass = cssClass;
this.snapshotContainer = snapshotContainer;
this.entryUrlWhitelist = entryUrlWhitelist;
}
canView(domainObject) {
@@ -44,7 +43,6 @@ export default class NotebookViewProvider {
let openmct = this.openmct;
let snapshotContainer = this.snapshotContainer;
let agent = new Agent(window);
let entryUrlWhitelist = this.entryUrlWhitelist;
return {
show(container) {
@@ -56,8 +54,7 @@ export default class NotebookViewProvider {
provide: {
openmct,
snapshotContainer,
agent,
entryUrlWhitelist
agent
},
data() {
return {

View File

@@ -50,7 +50,7 @@
<Sidebar
ref="sidebar"
class="c-notebook__nav c-sidebar c-drawer c-drawer--align-left"
:class="sidebarClasses"
:class="[{'is-expanded': showNav}, {'c-drawer--push': !sidebarCoversEntries}, {'c-drawer--overlays': sidebarCoversEntries}]"
:default-page-id="defaultPageId"
:selected-page-id="getSelectedPageId()"
:default-section-id="defaultSectionId"
@@ -123,7 +123,6 @@
</div>
<div
v-if="selectedPage && !selectedPage.isLocked"
:class="{ 'disabled': activeTransaction }"
class="c-notebook__drag-area icon-plus"
@click="newEntry()"
@dragover="dragOver"
@@ -134,11 +133,6 @@
To start a new entry, click here or drag and drop any object
</span>
</div>
<progress-bar
v-if="savingTransaction"
class="c-telemetry-table__progress-bar"
:model="{ progressPerc: undefined }"
/>
<div
v-if="selectedPage && selectedPage.isLocked"
class="c-notebook__page-locked"
@@ -162,12 +156,10 @@
:selected-section="selectedSection"
:read-only="false"
:is-locked="selectedPage.isLocked"
:selected-entry-id="selectedEntryId"
@cancelEdit="cancelTransaction"
@editingEntry="startTransaction"
@deleteEntry="deleteEntry"
@updateEntry="updateEntry"
@entry-selection="entrySelection(entry)"
/>
</div>
<div
@@ -191,7 +183,6 @@ import NotebookEntry from './NotebookEntry.vue';
import Search from '@/ui/components/search.vue';
import SearchResults from './SearchResults.vue';
import Sidebar from './Sidebar.vue';
import ProgressBar from '../../../ui/components/ProgressBar.vue';
import { clearDefaultNotebook, getDefaultNotebook, setDefaultNotebook, setDefaultNotebookSectionId, setDefaultNotebookPageId } from '../utils/notebook-storage';
import { addNotebookEntry, createNewEmbed, getEntryPosById, getNotebookEntries, mutateObject } from '../utils/notebook-entries';
import { saveNotebookImageDomainObject, updateNamespaceOfDomainObject } from '../utils/notebook-image';
@@ -209,8 +200,7 @@ export default {
NotebookEntry,
Search,
SearchResults,
Sidebar,
ProgressBar
Sidebar
},
inject: ['agent', 'openmct', 'snapshotContainer'],
props: {
@@ -235,10 +225,7 @@ export default {
showNav: false,
sidebarCoversEntries: false,
filteredAndSortedEntries: [],
notebookAnnotations: {},
selectedEntryId: '',
activeTransaction: false,
savingTransaction: false
notebookAnnotations: {}
};
},
computed: {
@@ -283,20 +270,6 @@ export default {
return this.sections[0];
},
sidebarClasses() {
let sidebarClasses = [];
if (this.showNav) {
sidebarClasses.push('is-expanded');
}
if (this.sidebarCoversEntries) {
sidebarClasses.push('c-drawer--overlays');
} else {
sidebarClasses.push('c-drawer--push');
}
return sidebarClasses;
},
showLockButton() {
const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
@@ -324,9 +297,6 @@ export default {
this.formatSidebar();
this.setSectionAndPageFromUrl();
this.openmct.selection.on('change', this.updateSelection);
this.transaction = null;
window.addEventListener('orientationchange', this.formatSidebar);
window.addEventListener('hashchange', this.setSectionAndPageFromUrl);
this.filterAndSortEntries();
@@ -350,7 +320,6 @@ export default {
window.removeEventListener('orientationchange', this.formatSidebar);
window.removeEventListener('hashchange', this.setSectionAndPageFromUrl);
this.openmct.selection.off('change', this.updateSelection);
},
updated: function () {
this.$nextTick(() => {
@@ -380,20 +349,15 @@ export default {
}
});
},
updateSelection(selection) {
if (selection?.[0]?.[1]?.context?.targetDetails?.entryId === undefined) {
this.selectedEntryId = '';
}
},
async loadAnnotations() {
if (!this.openmct.annotation.getAvailableTags().length) {
// don't bother loading annotations if there are no tags
return;
}
this.lastLocalAnnotationCreation = this.domainObject.annotationLastCreated ?? 0;
const foundAnnotations = await this.openmct.annotation.getAnnotations(this.domainObject.identifier);
const query = this.openmct.objects.makeKeyString(this.domainObject.identifier);
const foundAnnotations = await this.openmct.annotation.getAnnotations(query);
foundAnnotations.forEach((foundAnnotation) => {
const targetId = Object.keys(foundAnnotation.targets)[0];
const entryId = foundAnnotation.targets[targetId].entryId;
@@ -785,7 +749,6 @@ export default {
return section.id;
},
async newEntry(embed = null) {
this.startTransaction();
this.resetSearch();
const notebookStorage = this.createNotebookStorageObject();
this.updateDefaultNotebook(notebookStorage);
@@ -928,37 +891,20 @@ export default {
},
startTransaction() {
if (!this.openmct.objects.isTransactionActive()) {
this.activeTransaction = true;
this.transaction = this.openmct.objects.startTransaction();
}
},
async saveTransaction() {
if (this.transaction !== null) {
this.savingTransaction = true;
try {
await this.transaction.commit();
} finally {
this.endTransaction();
}
if (this.transaction !== undefined) {
await this.transaction.commit();
this.openmct.objects.endTransaction();
}
},
async cancelTransaction() {
if (this.transaction !== null) {
try {
await this.transaction.cancel();
} finally {
this.endTransaction();
}
if (this.transaction !== undefined) {
await this.transaction.cancel();
this.openmct.objects.endTransaction();
}
},
entrySelection(entry) {
this.selectedEntryId = entry.id;
},
endTransaction() {
this.openmct.objects.endTransaction();
this.transaction = null;
this.savingTransaction = false;
this.activeTransaction = false;
}
}
};

View File

@@ -12,15 +12,14 @@
<a
class="c-ne__embed__link"
:class="embed.cssClass"
@click="navigateToItemInTime"
@click="changeLocation"
>{{ embed.name }}</a>
<button
class="c-ne__embed__actions c-icon-button icon-3-dots"
title="More options"
@click.prevent.stop="showMenuItems($event)"
></button>
<PopupMenu :popup-menu-items="popupMenuItems" />
</div>
<div class="c-ne__embed__time">
<div
v-if="embed.snapshot"
class="c-ne__embed__time"
>
{{ createdOn }}
</div>
</div>
@@ -33,14 +32,17 @@ import PreviewAction from '../../../ui/preview/PreviewAction';
import RemoveDialog from '../utils/removeDialog';
import PainterroInstance from '../utils/painterroInstance';
import SnapshotTemplate from './snapshot-template.html';
import objectPathToUrl from '@/tools/url';
import { updateNotebookImageDomainObject } from '../utils/notebook-image';
import ImageExporter from '../../../exporters/ImageExporter';
import PopupMenu from './PopupMenu.vue';
import Vue from 'vue';
export default {
components: {
PopupMenu
},
inject: ['openmct', 'snapshotContainer'],
props: {
embed: {
@@ -70,7 +72,7 @@ export default {
},
data() {
return {
menuActions: []
popupMenuItems: []
};
},
computed: {
@@ -86,89 +88,38 @@ export default {
watch: {
isLocked(value) {
if (value === true) {
let index = this.menuActions.findIndex((item) => item.id === 'removeEmbed');
let index = this.popupMenuItems.findIndex((item) => item.id === 'removeEmbed');
this.$delete(this.menuActions, index);
this.$delete(this.popupMenuItems, index);
}
}
},
async mounted() {
this.objectPath = [];
await this.setEmbedObjectPath();
this.addMenuActions();
mounted() {
this.addPopupMenuItems();
this.imageExporter = new ImageExporter(this.openmct);
},
methods: {
showMenuItems(event) {
const x = event.x;
const y = event.y;
const menuOptions = {
menuClass: 'c-ne__embed__actions-menu',
placement: this.openmct.menus.menuPlacement.TOP_RIGHT
addPopupMenuItems() {
const removeEmbed = {
id: 'removeEmbed',
cssClass: 'icon-trash',
name: this.removeActionString,
callback: this.getRemoveDialog.bind(this)
};
this.openmct.menus.showSuperMenu(x, y, this.menuActions, menuOptions);
},
addMenuActions() {
if (this.embed.snapshot) {
const viewSnapshot = {
id: 'viewSnapshot',
cssClass: 'icon-camera',
name: 'View Snapshot',
description: 'View the snapshot image taken in the form of a jpeg.',
onItemClicked: () => this.openSnapshot()
};
this.menuActions = [viewSnapshot];
}
const navigateToItem = {
id: 'navigateToItem',
cssClass: this.embed.cssClass,
name: 'Navigate to Item',
description: 'Navigate to the item with the current time settings.',
onItemClicked: () => this.navigateToItem()
};
const navigateToItemInTime = {
id: 'navigateToItemInTime',
cssClass: this.embed.cssClass,
name: 'Navigate to Item in Time',
description: 'Navigate to the item in its time frame when captured.',
onItemClicked: () => this.navigateToItemInTime()
};
const quickView = {
id: 'quickView',
const preview = {
id: 'preview',
cssClass: 'icon-eye-open',
name: 'Quick View',
description: 'Full screen overlay view of the item.',
onItemClicked: () => this.previewEmbed()
name: 'Preview',
callback: this.previewEmbed.bind(this)
};
this.menuActions = this.menuActions.concat([quickView, navigateToItem, navigateToItemInTime]);
this.popupMenuItems = [preview];
if (!this.isLocked) {
const removeEmbed = {
id: 'removeEmbed',
cssClass: 'icon-trash',
name: this.removeActionString,
description: 'Permanently delete this embed from this Notebook entry.',
onItemClicked: this.getRemoveDialog.bind(this)
};
this.menuActions.push(removeEmbed);
this.popupMenuItems.unshift(removeEmbed);
}
},
async setEmbedObjectPath() {
this.objectPath = await this.openmct.objects.getOriginalPath(this.embed.domainObject.identifier);
if (this.objectPath.length > 0 && this.objectPath[this.objectPath.length - 1].type === 'root') {
this.objectPath.pop();
}
},
annotateSnapshot() {
const annotateVue = new Vue({
template: '<div id="snap-annotation"></div>'
@@ -228,11 +179,7 @@ export default {
painterroInstance.show(object.configuration.fullSizeImageURL);
});
},
navigateToItem() {
const url = objectPathToUrl(this.openmct, this.objectPath);
this.openmct.router.navigate(url);
},
navigateToItemInTime() {
changeLocation() {
const hash = this.embed.historicLink;
const bounds = this.openmct.time.bounds();

View File

@@ -1,4 +1,3 @@
<!-- eslint-disable vue/no-v-html -->
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
@@ -23,37 +22,23 @@
<template>
<div
class="c-notebook__entry c-ne has-local-controls"
class="c-notebook__entry c-ne has-local-controls has-tag-applier"
aria-label="Notebook Entry"
:class="{ 'locked': isLocked, 'is-selected': isSelectedEntry }"
:class="{ 'locked': isLocked }"
@dragover="changeCursor"
@drop.capture="cancelEditMode"
@drop.prevent="dropOnEntry"
@click="selectEntry($event, entry)"
>
<div class="c-ne__time-and-content">
<div class="c-ne__time-and-creator-and-delete">
<div class="c-ne__time-and-creator">
<span class="c-ne__created-date">{{ createdOnDate }}</span>
<span class="c-ne__created-time">{{ createdOnTime }}</span>
<span
v-if="entry.createdBy"
class="c-ne__creator"
>
<span class="icon-person"></span> {{ entry.createdBy }}
</span>
</div>
<div class="c-ne__time-and-creator">
<span class="c-ne__created-date">{{ createdOnDate }}</span>
<span class="c-ne__created-time">{{ createdOnTime }}</span>
<span
v-if="!readOnly && !isLocked"
class="c-ne__local-controls--hidden"
v-if="entry.createdBy"
class="c-ne__creator"
>
<button
class="c-ne__remove c-icon-button c-icon-button--major icon-trash"
title="Delete this entry"
tabindex="-1"
@click="deleteEntry"
>
</button>
<span class="icon-person"></span> {{ entry.createdBy }}
</span>
</div>
<div class="c-ne__content">
@@ -76,14 +61,12 @@
class="c-ne__text c-ne__input"
aria-label="Notebook Entry Input"
tabindex="0"
:contenteditable="canEdit"
@mouseover="checkEditability($event)"
@mouseleave="canEdit = true"
contenteditable="true"
@focus="editingEntry()"
@blur="updateEntryValue($event)"
@keydown.enter.exact.prevent
@keyup.enter.exact.prevent="forceBlur($event)"
v-html="formattedText"
v-text="entry.text"
>
</div>
</template>
@@ -94,42 +77,43 @@
class="c-ne__text"
contenteditable="false"
tabindex="0"
v-html="formattedText"
v-text="entry.text"
>
</div>
</template>
<div>
<div
v-for="(tag, index) in entryTags"
:key="index"
class="c-tag"
:style="{ backgroundColor: tag.backgroundColor, color: tag.foregroundColor }"
>
{{ tag.label }}
</div>
</div>
<TagEditor
:domain-object="domainObject"
:annotations="notebookAnnotations"
:annotation-type="openmct.annotation.ANNOTATION_TYPES.NOTEBOOK"
:target-specific-details="{entryId: entry.id}"
@tags-updated="timestampAndUpdate"
/>
<div
:class="{'c-scrollcontainer': enableEmbedsWrapperScroll }"
>
<div
ref="embedsWrapper"
class="c-snapshots c-ne__embeds-wrapper"
>
<NotebookEmbed
v-for="embed in entry.embeds"
ref="embeds"
:key="embed.id"
:embed="embed"
:is-locked="isLocked"
@removeEmbed="removeEmbed"
@updateEmbed="updateEmbed"
/>
</div>
<div class="c-snapshots c-ne__embeds">
<NotebookEmbed
v-for="embed in entry.embeds"
:key="embed.id"
:embed="embed"
:is-locked="isLocked"
@removeEmbed="removeEmbed"
@updateEmbed="updateEmbed"
/>
</div>
</div>
</div>
<div
v-if="!readOnly && !isLocked"
class="c-ne__local-controls--hidden"
>
<button
class="c-icon-button c-icon-button--major icon-trash"
title="Delete this entry"
tabindex="-1"
@click="deleteEntry"
>
</button>
</div>
<div
v-if="readOnly"
class="c-ne__section-and-page"
@@ -155,28 +139,22 @@
<script>
import NotebookEmbed from './NotebookEmbed.vue';
import TagEditor from '../../../ui/components/tags/TagEditor.vue';
import TextHighlight from '../../../utils/textHighlight/TextHighlight.vue';
import { createNewEmbed } from '../utils/notebook-entries';
import { saveNotebookImageDomainObject, updateNamespaceOfDomainObject } from '../utils/notebook-image';
import sanitizeHtml from 'sanitize-html';
import _ from 'lodash';
import Moment from 'moment';
const SANITIZATION_SCHEMA = {
allowedTags: [],
allowedAttributes: {}
};
const URL_REGEX = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/g;
const UNKNOWN_USER = 'Unknown';
export default {
components: {
NotebookEmbed,
TextHighlight
TextHighlight,
TagEditor
},
inject: ['openmct', 'snapshotContainer', 'entryUrlWhitelist'],
inject: ['openmct', 'snapshotContainer'],
props: {
domainObject: {
type: Object,
@@ -225,19 +203,8 @@ export default {
default() {
return false;
}
},
selectedEntryId: {
type: String,
required: true
}
},
data() {
return {
editMode: false,
canEdit: true,
enableEmbedsWrapperScroll: false
};
},
computed: {
createdOnDate() {
return this.formatTime(this.entry.createdOn, 'YYYY-MM-DD');
@@ -245,39 +212,6 @@ export default {
createdOnTime() {
return this.formatTime(this.entry.createdOn, 'HH:mm:ss');
},
formattedText() {
// remove ANY tags
let text = sanitizeHtml(this.entry.text, SANITIZATION_SCHEMA);
if (this.editMode || !this.urlWhitelist) {
return text;
}
text = text.replace(URL_REGEX, (match) => {
const url = new URL(match);
const domain = url.hostname;
let result = match;
let isMatch = this.urlWhitelist.find((partialDomain) => {
return domain.endsWith(partialDomain);
});
if (isMatch) {
result = `<a class="c-hyperlink" target="_blank" href="${match}">${match}</a>`;
}
return result;
});
return text;
},
isSelectedEntry() {
return this.selectedEntryId === this.entry.id;
},
entryTags() {
const tagsFromAnnotations = this.openmct.annotation.getTagsFromAnnotations(this.notebookAnnotations);
return tagsFromAnnotations;
},
entryText() {
let text = this.entry.text;
@@ -298,23 +232,7 @@ export default {
}
},
mounted() {
this.manageEmbedLayout = _.debounce(this.manageEmbedLayout, 400);
if (this.$refs.embedsWrapper) {
this.embedsWrapperResizeObserver = new ResizeObserver(this.manageEmbedLayout);
this.embedsWrapperResizeObserver.observe(this.$refs.embedsWrapper);
}
this.manageEmbedLayout();
this.dropOnEntry = this.dropOnEntry.bind(this);
if (this.entryUrlWhitelist?.length > 0) {
this.urlWhitelist = this.entryUrlWhitelist;
}
},
beforeDestroy() {
if (this.embedsWrapperResizeObserver) {
this.embedsWrapperResizeObserver.unobserve(this.$refs.embedsWrapper);
}
},
methods: {
async addNewEmbed(objectPath) {
@@ -327,8 +245,6 @@ export default {
};
const newEmbed = await createNewEmbed(snapshotMeta);
this.entry.embeds.push(newEmbed);
this.manageEmbedLayout();
},
cancelEditMode(event) {
const isEditing = this.openmct.editor.isEditing();
@@ -346,25 +262,9 @@ export default {
event.dataTransfer.effectAllowed = 'none';
}
},
checkEditability($event) {
if ($event.target.nodeName === 'A') {
this.canEdit = false;
}
},
deleteEntry() {
this.$emit('deleteEntry', this.entry.id);
},
manageEmbedLayout() {
if (this.$refs.embeds) {
const embedsWrapperLength = this.$refs.embedsWrapper.clientWidth;
const embedsTotalWidth = this.$refs.embeds.reduce((total, embed) => {
return embed.$el.clientWidth + total;
}, 0);
this.enableEmbedsWrapperScroll = embedsTotalWidth > embedsWrapperLength;
}
},
async dropOnEntry($event) {
$event.stopImmediatePropagation();
@@ -422,8 +322,6 @@ export default {
this.entry.embeds.splice(embedPosition, 1);
this.timestampAndUpdate();
this.manageEmbedLayout();
},
updateEmbed(newEmbed) {
this.entry.embeds.some(e => {
@@ -449,11 +347,9 @@ export default {
this.$emit('updateEntry', this.entry);
},
editingEntry() {
this.editMode = true;
this.$emit('editingEntry');
},
updateEntryValue($event) {
this.editMode = false;
const value = $event.target.innerText;
if (value !== this.entry.text && value.match(/\S/)) {
this.entry.text = value;
@@ -461,38 +357,6 @@ export default {
} else {
this.$emit('cancelEdit');
}
},
selectEntry(event, entry) {
const targetDetails = {};
const keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
targetDetails[keyString] = {
entryId: entry.id
};
const targetDomainObjects = {};
targetDomainObjects[keyString] = this.domainObject;
this.openmct.selection.select(
[
{
element: this.openmct.layout.$refs.browseObject.$el,
context: {
item: this.domainObject
}
},
{
element: event.currentTarget,
context: {
type: 'notebook-entry-selection',
targetDetails,
targetDomainObjects,
annotations: this.notebookAnnotations,
annotationType: this.openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
onAnnotationChange: this.timestampAndUpdate
}
}
],
false);
event.stopPropagation();
this.$emit('entry-selection', this.entry);
}
}
};

View File

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

View File

@@ -103,7 +103,7 @@ function installBaseNotebookFunctionality(openmct) {
monkeyPatchObjectAPIForNotebooks(openmct);
}
function NotebookPlugin(name = 'Notebook', entryUrlWhitelist = []) {
function NotebookPlugin(name = 'Notebook') {
return function install(openmct) {
if (openmct[NOTEBOOK_INSTALLED_KEY]) {
return;
@@ -118,8 +118,8 @@ function NotebookPlugin(name = 'Notebook', entryUrlWhitelist = []) {
const notebookType = new NotebookType(name, description, icon);
openmct.types.addType(NOTEBOOK_TYPE, notebookType);
const notebookView = new NotebookViewProvider(openmct, name, NOTEBOOK_VIEW_TYPE, NOTEBOOK_TYPE, icon, snapshotContainer, entryUrlWhitelist);
openmct.objectViews.addProvider(notebookView, entryUrlWhitelist);
const notebookView = new NotebookViewProvider(openmct, name, NOTEBOOK_VIEW_TYPE, NOTEBOOK_TYPE, icon, snapshotContainer);
openmct.objectViews.addProvider(notebookView);
installBaseNotebookFunctionality(openmct);
@@ -127,7 +127,7 @@ function NotebookPlugin(name = 'Notebook', entryUrlWhitelist = []) {
};
}
function RestrictedNotebookPlugin(name = 'Notebook Shift Log', entryUrlWhitelist = []) {
function RestrictedNotebookPlugin(name = 'Notebook Shift Log') {
return function install(openmct) {
if (openmct[RESTRICTED_NOTEBOOK_INSTALLED_KEY]) {
return;
@@ -140,8 +140,8 @@ function RestrictedNotebookPlugin(name = 'Notebook Shift Log', entryUrlWhitelist
const notebookType = new NotebookType(name, description, icon);
openmct.types.addType(RESTRICTED_NOTEBOOK_TYPE, notebookType);
const notebookView = new NotebookViewProvider(openmct, name, RESTRICTED_NOTEBOOK_VIEW_TYPE, RESTRICTED_NOTEBOOK_TYPE, icon, snapshotContainer, entryUrlWhitelist);
openmct.objectViews.addProvider(notebookView, entryUrlWhitelist);
const notebookView = new NotebookViewProvider(openmct, name, RESTRICTED_NOTEBOOK_VIEW_TYPE, RESTRICTED_NOTEBOOK_TYPE, icon, snapshotContainer);
openmct.objectViews.addProvider(notebookView);
installBaseNotebookFunctionality(openmct);

View File

@@ -1,20 +1,14 @@
<template>
<div
v-if="notifications.length === 0 ? showNotificationsOverlay : notifications.length > 0"
v-if="notifications.length > 0"
class="c-indicator c-indicator--clickable icon-bell"
:class="[severityClass]"
>
<span class="c-indicator__label">
<button
:aria-label="'Review ' + notificationsCountMessage(notifications.length)"
@click="toggleNotificationsList(true)"
>
<button @click="toggleNotificationsList(true)">
{{ notificationsCountMessage(notifications.length) }}
</button>
<button
aria-label="Clear all notifications"
@click="dismissAllNotifications()"
>
<button @click="dismissAllNotifications()">
Clear All
</button>
</span>

View File

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

View File

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

View File

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

View File

@@ -88,14 +88,6 @@
padding: 3px 0;
}
[class*='__label'] {
padding: 3px 0;
}
[class*='__poll-table'] {
grid-column: span 2;
}
[class*='new-question'] {
align-items: center;
display: flex;
@@ -131,12 +123,6 @@
opacity: 0.6;
}
}
&__actions {
display:flex;
flex: auto;
flex-direction: row;
justify-content: flex-end;
}
}
.c-indicator {

View File

@@ -58,13 +58,6 @@
{{ entry.roleCount }}
</div>
</div>
<div class="c-status-poll-report__actions">
<button
class="c-button"
title="Clear the previous poll question"
@click="clearPollQuestion"
>Clear Poll</button>
</div>
</div>
</template>
@@ -81,41 +74,6 @@
@click="updatePollQuestion"
>Update</button>
</div>
<div class="c-table c-spq__poll-table">
<table class="c-table__body">
<thead class="c-table__header">
<tr>
<th>
Position
</th>
<th>
Status
</th>
<th>
Age
</th>
</tr>
</thead>
<tbody>
<tr
v-for="statusForRole in statusesForRolesViewModel"
:key="statusForRole.key"
>
<td>
{{ statusForRole.role }}
</td>
<td
:style="{ background: statusForRole.status.statusBgColor, color: statusForRole.status.statusFgColor }"
>
{{ statusForRole.status.label }}
</td>
<td>
{{ statusForRole.age }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
@@ -139,11 +97,9 @@ export default {
data() {
return {
pollQuestionUpdated: '--',
pollQuestionTimestamp: undefined,
currentPollQuestion: '--',
newPollQuestion: undefined,
statusCountViewModel: [],
statusesForRolesViewModel: []
statusCountViewModel: []
};
},
computed: {
@@ -179,17 +135,9 @@ export default {
this.openmct.user.status.on('pollQuestionChange', this.setPollQuestion);
},
setPollQuestion(pollQuestion) {
let pollQuestionText = pollQuestion.question;
if (!pollQuestionText || pollQuestionText === '') {
pollQuestionText = '--';
this.indicator.text('No Poll Question');
} else {
this.indicator.text(pollQuestionText);
}
this.currentPollQuestion = pollQuestionText;
this.pollQuestionTimestamp = pollQuestion.timestamp;
this.currentPollQuestion = pollQuestion.question;
this.pollQuestionUpdated = new Date(pollQuestion.timestamp).toISOString();
this.indicator.text(pollQuestion.question);
},
async updatePollQuestion() {
const result = await this.openmct.user.status.setPollQuestion(this.newPollQuestion);
@@ -201,13 +149,6 @@ export default {
this.newPollQuestion = undefined;
},
async clearPollQuestion() {
this.currentPollQuestion = undefined;
await Promise.all([
this.openmct.user.status.resetAllStatuses(),
this.openmct.user.status.setPollQuestion()
]);
},
async fetchStatusSummary() {
const allStatuses = await this.openmct.user.status.getPossibleStatuses();
const statusCountMap = allStatuses.reduce((statusToCountMap, status) => {
@@ -217,6 +158,7 @@ export default {
}, {});
const allStatusRoles = await this.openmct.user.status.getAllStatusRoles();
const statusesForRoles = await Promise.all(allStatusRoles.map(role => this.openmct.user.status.getStatusForRole(role)));
statusesForRoles.forEach((status, i) => {
const currentCount = statusCountMap[status.key];
statusCountMap[status.key] = currentCount + 1;
@@ -228,51 +170,6 @@ export default {
roleCount: statusCountMap[status.key]
};
});
const defaultStatuses = await Promise.all(allStatusRoles.map(role => this.openmct.user.status.getDefaultStatusForRole(role)));
this.statusesForRolesViewModel = [];
statusesForRoles.forEach((status, index) => {
const isDefaultStatus = defaultStatuses[index].key === status.key;
let statusTimestamp = status.timestamp;
if (isDefaultStatus) {
// if the default is selected, set timestamp to undefined
statusTimestamp = undefined;
}
this.statusesForRolesViewModel.push({
status: this.applyStyling(status),
role: allStatusRoles[index],
age: this.formatStatusAge(statusTimestamp, this.pollQuestionTimestamp)
});
});
},
formatStatusAge(statusTimestamp, pollQuestionTimestamp) {
if (statusTimestamp === undefined || pollQuestionTimestamp === undefined) {
return '--';
}
const statusAgeInMs = statusTimestamp - pollQuestionTimestamp;
const absoluteTotalSeconds = Math.floor(Math.abs(statusAgeInMs) / 1000);
let hours = Math.floor(absoluteTotalSeconds / 3600);
let minutes = Math.floor((absoluteTotalSeconds - (hours * 3600)) / 60);
let secondsString = absoluteTotalSeconds - (hours * 3600) - (minutes * 60);
if (statusAgeInMs > 0 || (absoluteTotalSeconds === 0)) {
hours = `+ ${hours}`;
} else {
hours = `- ${hours}`;
}
if (minutes < 10) {
minutes = `0${minutes}`;
}
if (secondsString < 10) {
secondsString = `0${secondsString}`;
}
const statusAgeString = `${hours}:${minutes}:${secondsString}`;
return statusAgeString;
},
applyStyling(status) {
const stylesForStatus = this.configuration?.statusStyles?.[status.label];

View File

@@ -51,7 +51,7 @@ export default class PollQuestionIndicator extends AbstractStatusIndicator {
createIndicator() {
const pollQuestionIndicator = this.openmct.indicators.simpleIndicator();
pollQuestionIndicator.text("No Poll Question");
pollQuestionIndicator.text("Poll Question");
pollQuestionIndicator.description("Set the current poll question");
pollQuestionIndicator.iconClass('icon-status-poll-edit');
pollQuestionIndicator.element.classList.add("c-indicator--operator-status");

View File

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

View File

@@ -96,13 +96,8 @@ class CouchObjectProvider {
let keyString = this.openmct.objects.makeKeyString(objectIdentifier);
//TODO: Optimize this so that we don't 'get' the object if it's current revision (from this.objectQueue) is the same as the one we already have.
let observersForObject = this.observers[keyString];
let isInTransaction = false;
if (this.openmct.objects.isTransactionActive()) {
isInTransaction = this.openmct.objects.transaction.getDirtyObject(objectIdentifier);
}
if (observersForObject && !isInTransaction) {
if (observersForObject) {
observersForObject.forEach(async (observer) => {
const updatedObject = await this.get(objectIdentifier);
if (this.isSynchronizedObject(updatedObject)) {
@@ -224,12 +219,7 @@ class CouchObjectProvider {
console.error(error.message);
throw new Error(`CouchDB Error - No response"`);
} else {
if (body?.model && isNotebookOrAnnotationType(body.model)) {
// warn since we handle conflicts for notebooks
console.warn(error.message);
} else {
console.error(error.message);
}
console.error(error.message);
throw error;
}

View File

@@ -41,7 +41,7 @@
<y-axis
v-for="(yAxis, index) in yAxesIds"
:id="yAxis.id"
:key="`yAxis-${yAxis.id}-${index}`"
:key="`yAxis-${index}`"
:multiple-left-axes="multipleLeftAxes"
:position="yAxis.id > 2 ? 'right' : 'left'"
:class="{'plot-yaxis-right': yAxis.id > 2}"
@@ -49,7 +49,6 @@
:plot-left-tick-width="yAxis.id > 2 ? yAxis.tickWidth: plotLeftTickWidth"
@yKeyChanged="setYAxisKey"
@tickWidthChanged="onTickWidthChange"
@toggleAxisVisibility="toggleSeriesForYAxis"
/>
</div>
<div
@@ -92,11 +91,7 @@
<mct-chart
:rectangles="rectangles"
:highlights="highlights"
:annotated-points="annotatedPoints"
:annotation-selections="annotationSelections"
:show-limit-line-labels="showLimitLineLabels"
:hidden-y-axis-ids="hiddenYAxisIds"
:annotation-viewing-and-editing-allowed="annotationViewingAndEditingAllowed"
@plotReinitializeCanvas="initCanvas"
@chartLoaded="initialize"
/>
@@ -222,7 +217,6 @@ import MctTicks from "./MctTicks.vue";
import MctChart from "./chart/MctChart.vue";
import XAxis from "./axis/XAxis.vue";
import YAxis from "./axis/YAxis.vue";
import KDBush from 'kdbush';
import _ from "lodash";
const OFFSET_THRESHOLD = 10;
@@ -281,8 +275,6 @@ export default {
return {
altPressed: false,
highlights: [],
annotatedPoints: [],
annotationSelections: [],
lockHighlightPoint: false,
yKeyOptions: [],
yAxisLabel: '',
@@ -300,9 +292,7 @@ export default {
isFrozenOnMouseDown: false,
cursorGuide: this.initCursorGuide,
gridLines: this.initGridLines,
yAxes: [],
hiddenYAxisIds: [],
yAxisListWithRange: []
yAxes: []
};
},
computed: {
@@ -333,10 +323,6 @@ export default {
isFrozen() {
return this.config.xAxis.get('frozen') === true && this.config.yAxis.get('frozen') === true;
},
annotationViewingAndEditingAllowed() {
// only allow annotations viewing/editing if plot is paused or in fixed time mode
return this.isFrozen || !this.isRealTime;
},
plotLegendPositionClass() {
return !this.isNestedWithinAStackedPlot ? `plot-legend-${this.config.legend.get('position')}` : '';
},
@@ -379,7 +365,6 @@ export default {
}
},
mounted() {
this.yAxisIdVisibility = {};
this.offsetWidth = 0;
document.addEventListener('keydown', this.handleKeyDown);
@@ -406,8 +391,10 @@ export default {
}));
}
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.$emit('configLoaded', configId);
if (this.isNestedWithinAStackedPlot) {
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.$emit('configLoaded', configId);
}
this.listenTo(this.config.series, 'add', this.addSeries, this);
this.listenTo(this.config.series, 'remove', this.removeSeries, this);
@@ -422,83 +409,16 @@ export default {
this.removeStatusListener = this.openmct.status.observe(this.domainObject.identifier, this.updateStatus);
this.openmct.objectViews.on('clearData', this.clearData);
this.$on('loadingUpdated', this.loadAnnotations);
this.openmct.selection.on('change', this.updateSelection);
this.setTimeContext();
this.yAxisListWithRange = [this.config.yAxis, ...this.config.additionalYAxes];
this.loaded = true;
},
beforeDestroy() {
this.openmct.selection.off('change', this.updateSelection);
document.removeEventListener('keydown', this.handleKeyDown);
document.removeEventListener('keyup', this.handleKeyUp);
this.destroy();
},
methods: {
updateSelection(selection) {
const selectionContext = selection?.[0]?.[0]?.context?.item;
if (!selectionContext
|| this.openmct.objects.areIdsEqual(selectionContext.identifier, this.domainObject.identifier)) {
// Selection changed, but it's us, so ignoring it
return;
}
const selectionType = selection?.[0]?.[1]?.context?.type;
if (selectionType !== 'plot-points-selection') {
// wrong type of selection
return;
}
const currentXaxis = this.config.xAxis.get('displayRange');
const currentYaxis = this.config.yAxis.get('displayRange');
// when there is no plot data, the ranges can be undefined
// in which case we should not perform selection
if (!currentXaxis || !currentYaxis) {
return;
}
const selectedAnnotations = selection?.[0]?.[1]?.context?.annotations;
if (selectedAnnotations?.length) {
// just use first annotation
const boundingBoxes = Object.values(selectedAnnotations[0].targets);
let minX = Number.MAX_SAFE_INTEGER;
let minY = Number.MAX_SAFE_INTEGER;
let maxX = Number.MIN_SAFE_INTEGER;
let maxY = Number.MIN_SAFE_INTEGER;
boundingBoxes.forEach(boundingBox => {
if (boundingBox.minX < minX) {
minX = boundingBox.minX;
}
if (boundingBox.maxX > maxX) {
maxX = boundingBox.maxX;
}
if (boundingBox.maxY > maxY) {
maxY = boundingBox.maxY;
}
if (boundingBox.minY < minY) {
minY = boundingBox.minY;
}
});
this.config.xAxis.set('displayRange', {
min: minX,
max: maxX
});
this.config.yAxis.set('displayRange', {
min: minY,
max: maxY
});
this.zoom('out', 0.2);
}
this.prepareExistingAnnotationSelection(selectedAnnotations);
},
handleKeyDown(event) {
if (event.key === 'Alt') {
this.altPressed = true;
@@ -560,7 +480,6 @@ export default {
this.listenTo(series, 'change:interpolate', () => {
this.loadSeriesData(series);
}, this);
this.listenTo(series, 'change:yAxisId', this.updateTicksAndSeriesForYAxis, this);
this.loadSeriesData(series);
},
@@ -572,32 +491,13 @@ export default {
this.stopListening(plotSeries);
},
updateTicksAndSeriesForYAxis(newAxisId, oldAxisId) {
this.updateAxisUsageCount(oldAxisId, -1);
this.updateAxisUsageCount(newAxisId, 1);
},
updateAxisUsageCount(yAxisId, updateCountBy) {
const foundYAxis = this.yAxes.find(yAxis => yAxis.id === yAxisId);
if (foundYAxis) {
foundYAxis.seriesCount = foundYAxis.seriesCount + updateCountBy;
}
},
async loadAnnotations() {
if (!this.openmct.annotation.getAvailableTags().length) {
// don't bother loading annotations if there are no tags
return;
}
const rawAnnotationsForPlot = [];
await Promise.all(this.seriesModels.map(async (seriesModel) => {
const seriesAnnotations = await this.openmct.annotation.getAnnotations(seriesModel.model.identifier);
rawAnnotationsForPlot.push(...seriesAnnotations);
}));
if (rawAnnotationsForPlot) {
this.annotatedPoints = this.findAnnotationPoints(rawAnnotationsForPlot);
}
},
loadSeriesData(series) {
//this check ensures that duplicate requests don't happen on load
if (!this.timeContext) {
@@ -621,7 +521,8 @@ export default {
end: bounds.end
};
series.load(options).then(this.stopLoading.bind(this));
series.load(options)
.then(this.stopLoading.bind(this));
},
loadMoreData(range, purge) {
@@ -813,91 +714,10 @@ export default {
this.listenTo(this.canvas, 'mousemove', this.trackMousePosition, this);
this.listenTo(this.canvas, 'mouseleave', this.untrackMousePosition, this);
this.listenTo(this.canvas, 'mousedown', this.onMouseDown, this);
this.listenTo(this.canvas, 'click', this.selectNearbyAnnotations, this);
this.listenTo(this.canvas, 'wheel', this.wheelZoom, this);
}
},
marqueeAnnotations(annotationsToSelect) {
annotationsToSelect.forEach(annotationToSelect => {
Object.keys(annotationToSelect.targets).forEach(targetKeyString => {
const target = annotationToSelect.targets[targetKeyString];
const series = this.seriesModels.find(seriesModel => seriesModel.keyString === targetKeyString);
if (!series) {
return;
}
const yAxisId = series.get('yAxisId');
const rectangle = {
start: {
x: target.minX,
y: [target.minY],
yAxisIds: [yAxisId]
},
end: {
x: target.maxX,
y: [target.maxY],
yAxisIds: [yAxisId]
},
color: [1, 1, 1, 0.10]
};
this.rectangles.push(rectangle);
});
});
},
gatherNearbyAnnotations() {
const nearbyAnnotations = [];
this.config.series.models.forEach(series => {
if (series.closest.annotationsById) {
Object.values(series.closest.annotationsById).forEach(closeAnnotation => {
const addedAnnotationAlready = nearbyAnnotations.some(annotation => {
return _.isEqual(annotation.targets, closeAnnotation.targets)
&& _.isEqual(annotation.tags, closeAnnotation.tags);
});
if (!addedAnnotationAlready) {
nearbyAnnotations.push(closeAnnotation);
}
});
}
});
return nearbyAnnotations;
},
prepareExistingAnnotationSelection(annotations) {
const targetDomainObjects = {};
this.config.series.models.forEach(series => {
targetDomainObjects[series.keyString] = series.domainObject;
});
const targetDetails = {};
const uniqueBoundsAnnotations = [];
annotations.forEach(annotation => {
Object.entries(annotation.targets).forEach(([key, value]) => {
targetDetails[key] = value;
});
const boundingBoxAlreadyAdded = uniqueBoundsAnnotations.some(existingAnnotation => {
const existingBoundingBox = Object.values(existingAnnotation.targets)[0];
const newBoundingBox = Object.values(annotation.targets)[0];
return (existingBoundingBox.minX === newBoundingBox.minX
&& existingBoundingBox.minY === newBoundingBox.minY
&& existingBoundingBox.maxX === newBoundingBox.maxX
&& existingBoundingBox.maxY === newBoundingBox.maxY);
});
if (!boundingBoxAlreadyAdded) {
uniqueBoundsAnnotations.push(annotation);
}
});
this.marqueeAnnotations(uniqueBoundsAnnotations);
return {
targetDomainObjects,
targetDetails
};
},
initialize() {
this.handleWindowResize = _.debounce(this.handleWindowResize, 500);
this.plotContainerResizeObserver = new ResizeObserver(this.handleWindowResize);
@@ -905,13 +725,8 @@ export default {
// Setup canvas etc.
this.xScale = new LinearScale(this.config.xAxis.get('displayRange'));
this.yScale = [];
this.yAxisListWithRange.forEach((yAxis) => {
this.yScale.push({
id: yAxis.id,
scale: new LinearScale(yAxis.get('displayRange'))
});
});
//TODO: handle yScale, zoom/pan for all yAxes
this.yScale = new LinearScale(this.config.yAxis.get('displayRange'));
this.pan = undefined;
this.marquee = undefined;
@@ -927,8 +742,9 @@ export default {
this.cursorGuideHorizontal = this.$refs.cursorGuideHorizontal;
this.listenTo(this.config.xAxis, 'change:displayRange', this.onXAxisChange, this);
this.yAxisListWithRange.forEach((yAxis) => {
this.listenTo(yAxis, 'change:displayRange', this.onYAxisChange.bind(this, yAxis.id), this);
this.listenTo(this.config.yAxis, 'change:displayRange', this.onYAxisChange, this);
this.config.additionalYAxes.forEach(yAxis => {
this.listenTo(yAxis, 'change:displayRange', this.onYAxisChange, this);
});
},
@@ -938,11 +754,9 @@ export default {
}
},
onYAxisChange(yAxisId, displayBounds) {
onYAxisChange(displayBounds) {
if (displayBounds) {
this.yScale.filter((yAxis) => yAxis.id === yAxisId).forEach((yAxis) => {
yAxis.scale.domain(displayBounds);
});
this.yScale.domain(displayBounds);
}
},
@@ -966,30 +780,15 @@ export default {
}
},
toggleSeriesForYAxis({ id, visible}) {
//if toggling to visible, re-fetch the data for the series that are part of this y Axis
if (visible === true) {
this.config.series.models.filter(model => model.get('yAxisId') === id)
.forEach(this.loadSeriesData, this);
}
this.yAxisIdVisibility[id] = visible;
this.hiddenYAxisIds = Object.keys(this.yAxisIdVisibility).map(Number).filter(key => {
return this.yAxisIdVisibility[key] === false;
});
},
trackMousePosition(event) {
this.trackChartElementBounds(event);
this.xScale.range({
min: 0,
max: this.chartElementBounds.width
});
this.yScale.forEach((yAxis) => {
yAxis.scale.range({
min: 0,
max: this.chartElementBounds.height
});
this.yScale.range({
min: 0,
max: this.chartElementBounds.height
});
this.positionOverElement = {
@@ -998,13 +797,9 @@ export default {
- (event.clientY - this.chartElementBounds.top)
};
const yLocationForPositionOverPlot = this.yScale.map((yAxis) => yAxis.scale.invert(this.positionOverElement.y));
const yAxisIds = this.yScale.map((yAxis) => yAxis.id);
// Also store the order of yAxisIds so that we can associate the y location to the yAxis
this.positionOverPlot = {
x: this.xScale.invert(this.positionOverElement.x),
y: yLocationForPositionOverPlot,
yAxisIds
y: this.yScale.invert(this.positionOverElement.y)
};
if (this.cursorGuide) {
@@ -1017,12 +812,6 @@ export default {
event.preventDefault();
},
getYPositionForYAxis(object, yAxis) {
const index = object.yAxisIds.findIndex(yAxisId => yAxisId === yAxis.get('id'));
return object.y[index];
},
updateCrosshairs(event) {
this.cursorGuideVertical.style.left = (event.clientX - this.chartElementBounds.x) + 'px';
this.cursorGuideHorizontal.style.top = (event.clientY - this.chartElementBounds.y) + 'px';
@@ -1076,7 +865,7 @@ export default {
},
onMouseDown(event) {
// do not monitor drag events on browser context click
// do not monitor drag events on browser context click
if (event.ctrlKey) {
return;
}
@@ -1088,12 +877,10 @@ export default {
const isFrozen = this.config.xAxis.get('frozen') === true && this.config.yAxis.get('frozen') === true;
this.isFrozenOnMouseDown = isFrozen;
if (event.altKey && !event.shiftKey) {
if (event.altKey) {
return this.startPan(event);
} else if (this.annotationViewingAndEditingAllowed && event.altKey && event.shiftKey) {
return this.startMarquee(event, true);
} else {
return this.startMarquee(event, false);
return this.startMarquee(event);
}
},
@@ -1101,7 +888,7 @@ export default {
this.stopListening(window, 'mouseup', this.onMouseUp, this);
this.stopListening(window, 'mousemove', this.trackMousePosition, this);
if (this.isMouseClick() && event.shiftKey) {
if (this.isMouseClick()) {
this.lockHighlightPoint = !this.lockHighlightPoint;
this.$emit('lockHighlightPoint', this.lockHighlightPoint);
}
@@ -1129,9 +916,8 @@ export default {
}
const { start, end } = this.marquee;
const someYPositionOverPlot = start.y.some(y => y);
return start.x === end.x && someYPositionOverPlot;
return start.x === end.x && start.y === end.y;
},
updateMarquee() {
@@ -1143,9 +929,7 @@ export default {
this.marquee.endPixels = this.positionOverElement;
},
startMarquee(event, annotationEvent) {
this.rectangles = [];
this.annotationSelections = [];
startMarquee(event) {
this.canvas.classList.remove('plot-drag');
this.canvas.classList.add('plot-marquee');
@@ -1159,181 +943,12 @@ export default {
end: this.positionOverPlot,
color: [1, 1, 1, 0.5]
};
if (annotationEvent) {
this.marquee.annotationEvent = true;
}
this.rectangles.push(this.marquee);
this.trackHistory();
}
},
selectNearbyAnnotations(event) {
event.stopPropagation();
if (!this.annotationViewingAndEditingAllowed || this.annotationSelections.length) {
return;
}
const nearbyAnnotations = this.gatherNearbyAnnotations();
const { targetDomainObjects, targetDetails } = this.prepareExistingAnnotationSelection(nearbyAnnotations);
this.selectPlotAnnotations({
targetDetails,
targetDomainObjects,
annotations: nearbyAnnotations
});
},
selectPlotAnnotations({targetDetails, targetDomainObjects, annotations}) {
const selection =
[
{
element: this.openmct.layout.$refs.browseObject.$el,
context: {
item: this.domainObject
}
},
{
element: this.$el,
context: {
type: 'plot-points-selection',
targetDetails,
targetDomainObjects,
annotations,
annotationType: this.openmct.annotation.ANNOTATION_TYPES.PLOT_SPATIAL,
onAnnotationChange: this.onAnnotationChange
}
}
];
this.openmct.selection.select(selection, true);
},
selectNewPlotAnnotations(boundingBoxPerYAxis, pointsInBox, event) {
let targetDomainObjects = {};
let targetDetails = {};
let annotations = {};
pointsInBox.forEach(pointInBox => {
if (pointInBox.length) {
const seriesID = pointInBox[0].series.keyString;
const boundingBoxWithId = boundingBoxPerYAxis.find(box => box.id === pointInBox[0].series.get('yAxisId'));
targetDetails[seriesID] = boundingBoxWithId?.boundingBox;
targetDomainObjects[seriesID] = pointInBox[0].series.domainObject;
}
});
this.selectPlotAnnotations({
targetDetails,
targetDomainObjects,
annotations
});
},
findAnnotationPoints(rawAnnotations) {
const annotationsByPoints = [];
rawAnnotations.forEach(rawAnnotation => {
if (rawAnnotation.targets) {
const targetValues = Object.values(rawAnnotation.targets);
const targetKeys = Object.keys(rawAnnotation.targets);
if (targetValues && targetValues.length) {
let boundingBoxPerYAxis = [];
targetValues.forEach((boundingBox, index) => {
const seriesId = targetKeys[index];
const series = this.seriesModels.find(seriesModel => seriesModel.keyString === seriesId);
if (!series) {
return;
}
boundingBoxPerYAxis.push({
id: series.get('yAxisId'),
boundingBox
});
});
const pointsInBox = this.getPointsInBox(boundingBoxPerYAxis, rawAnnotation);
if (pointsInBox && pointsInBox.length) {
annotationsByPoints.push(pointsInBox.flat());
}
}
}
});
return annotationsByPoints.flat();
},
getPointsInBox(boundingBoxPerYAxis, rawAnnotation) {
// load series models in KD-Trees
const seriesKDTrees = [];
this.seriesModels.forEach(seriesModel => {
const boundingBoxWithId = boundingBoxPerYAxis.find(box => box.id === seriesModel.get('yAxisId'));
const boundingBox = boundingBoxWithId?.boundingBox;
//Series was probably added after the last annotations were saved
if (!boundingBox) {
return;
}
const seriesData = seriesModel.getSeriesData();
if (seriesData && seriesData.length) {
const kdTree = new KDBush(seriesData,
(point) => {
return seriesModel.getXVal(point);
},
(point) => {
return seriesModel.getYVal(point);
}
);
const searchResults = [];
const rangeResults = kdTree.range(boundingBox.minX, boundingBox.minY, boundingBox.maxX, boundingBox.maxY);
rangeResults.forEach(id => {
const seriesDatum = seriesData[id];
if (seriesDatum) {
const result = {
series: seriesModel,
point: seriesDatum
};
searchResults.push(result);
}
if (rawAnnotation) {
if (!seriesDatum.annotationsById) {
seriesDatum.annotationsById = {};
}
const annotationKeyString = this.openmct.objects.makeKeyString(rawAnnotation.identifier);
seriesDatum.annotationsById[annotationKeyString] = rawAnnotation;
}
});
if (searchResults.length) {
seriesKDTrees.push(searchResults);
}
}
});
return seriesKDTrees;
},
endAnnotationMarquee(event) {
const boundingBoxPerYAxis = [];
this.yAxisListWithRange.forEach((yAxis, yIndex) => {
const minX = Math.min(this.marquee.start.x, this.marquee.end.x);
const minY = Math.min(this.marquee.start.y[yIndex], this.marquee.end.y[yIndex]);
const maxX = Math.max(this.marquee.start.x, this.marquee.end.x);
const maxY = Math.max(this.marquee.start.y[yIndex], this.marquee.end.y[yIndex]);
const boundingBox = {
minX,
minY,
maxX,
maxY
};
boundingBoxPerYAxis.push({
id: yAxis.get('id'),
boundingBox
});
});
const pointsInBox = this.getPointsInBox(boundingBoxPerYAxis);
if (!pointsInBox) {
return;
}
this.annotationSelections = pointsInBox.flat();
this.selectNewPlotAnnotations(boundingBoxPerYAxis, pointsInBox, event);
},
endZoomMarquee() {
endMarquee() {
const startPixels = this.marquee.startPixels;
const endPixels = this.marquee.endPixels;
const marqueeDistance = Math.sqrt(
@@ -1346,13 +961,9 @@ export default {
min: Math.min(this.marquee.start.x, this.marquee.end.x),
max: Math.max(this.marquee.start.x, this.marquee.end.x)
});
this.yAxisListWithRange.forEach((yAxis) => {
const yStartPosition = this.getYPositionForYAxis(this.marquee.start, yAxis);
const yEndPosition = this.getYPositionForYAxis(this.marquee.end, yAxis);
yAxis.set('displayRange', {
min: Math.min(yStartPosition, yEndPosition),
max: Math.max(yStartPosition, yEndPosition)
});
this.config.yAxis.set('displayRange', {
min: Math.min(this.marquee.start.y, this.marquee.end.y),
max: Math.max(this.marquee.start.y, this.marquee.end.y)
});
this.userViewportChangeEnd();
} else {
@@ -1360,40 +971,18 @@ export default {
// if marquee zoom doesn't occur.
this.plotHistory.pop();
}
},
endMarquee(event) {
if (this.marquee.annotationEvent) {
this.endAnnotationMarquee(event);
} else {
this.endZoomMarquee();
this.rectangles = [];
}
this.marquee = null;
},
onAnnotationChange(annotations) {
if (this.marquee) {
this.marquee.annotationEvent = false;
this.endMarquee();
}
this.loadAnnotations();
this.rectangles = [];
this.marquee = undefined;
},
zoom(zoomDirection, zoomFactor) {
const currentXaxis = this.config.xAxis.get('displayRange');
let doesYAxisHaveRange = false;
this.yAxisListWithRange.forEach((yAxisModel) => {
if (yAxisModel.get('displayRange')) {
doesYAxisHaveRange = true;
}
});
const currentYaxis = this.config.yAxis.get('displayRange');
// when there is no plot data, the ranges can be undefined
// in which case we should not perform zoom
if (!currentXaxis || !doesYAxisHaveRange) {
if (!currentXaxis || !currentYaxis) {
return;
}
@@ -1401,6 +990,7 @@ export default {
this.trackHistory();
const xAxisDist = (currentXaxis.max - currentXaxis.min) * zoomFactor;
const yAxisDist = (currentYaxis.max - currentYaxis.min) * zoomFactor;
if (zoomDirection === 'in') {
this.config.xAxis.set('displayRange', {
@@ -1408,17 +998,9 @@ export default {
max: currentXaxis.max - xAxisDist
});
this.yAxisListWithRange.forEach((yAxisModel) => {
const currentYaxis = yAxisModel.get('displayRange');
if (!currentYaxis) {
return;
}
const yAxisDist = (currentYaxis.max - currentYaxis.min) * zoomFactor;
yAxisModel.set('displayRange', {
min: currentYaxis.min + yAxisDist,
max: currentYaxis.max - yAxisDist
});
this.config.yAxis.set('displayRange', {
min: currentYaxis.min + yAxisDist,
max: currentYaxis.max - yAxisDist
});
} else if (zoomDirection === 'out') {
this.config.xAxis.set('displayRange', {
@@ -1426,17 +1008,9 @@ export default {
max: currentXaxis.max + xAxisDist
});
this.yAxisListWithRange.forEach((yAxisModel) => {
const currentYaxis = yAxisModel.get('displayRange');
if (!currentYaxis) {
return;
}
const yAxisDist = (currentYaxis.max - currentYaxis.min) * zoomFactor;
yAxisModel.set('displayRange', {
min: currentYaxis.min - yAxisDist,
max: currentYaxis.max + yAxisDist
});
this.config.yAxis.set('displayRange', {
min: currentYaxis.min - yAxisDist,
max: currentYaxis.max + yAxisDist
});
}
@@ -1453,17 +1027,11 @@ export default {
}
let xDisplayRange = this.config.xAxis.get('displayRange');
let doesYAxisHaveRange = false;
this.yAxisListWithRange.forEach((yAxisModel) => {
if (yAxisModel.get('displayRange')) {
doesYAxisHaveRange = true;
}
});
let yDisplayRange = this.config.yAxis.get('displayRange');
// when there is no plot data, the ranges can be undefined
// in which case we should not perform zoom
if (!xDisplayRange || !doesYAxisHaveRange) {
if (!xDisplayRange || !yDisplayRange) {
return;
}
@@ -1471,19 +1039,22 @@ export default {
window.clearTimeout(this.stillZooming);
let xAxisDist = (xDisplayRange.max - xDisplayRange.min);
let yAxisDist = (yDisplayRange.max - yDisplayRange.min);
let xDistMouseToMax = xDisplayRange.max - this.positionOverPlot.x;
let xDistMouseToMin = this.positionOverPlot.x - xDisplayRange.min;
let yDistMouseToMax = yDisplayRange.max - this.positionOverPlot.y;
let yDistMouseToMin = this.positionOverPlot.y - yDisplayRange.min;
let xAxisMaxDist = xDistMouseToMax / xAxisDist;
let xAxisMinDist = xDistMouseToMin / xAxisDist;
let yAxisMaxDist = yDistMouseToMax / yAxisDist;
let yAxisMinDist = yDistMouseToMin / yAxisDist;
let plotHistoryStep;
if (!plotHistoryStep) {
const yRangeList = [];
this.yAxisListWithRange.map((yAxis) => yRangeList.push(yAxis.get('displayRange')));
plotHistoryStep = {
x: this.config.xAxis.get('displayRange'),
y: yRangeList
x: xDisplayRange,
y: yDisplayRange
};
}
@@ -1494,47 +1065,20 @@ export default {
max: xDisplayRange.max - ((xAxisDist * ZOOM_AMT) * xAxisMaxDist)
});
this.yAxisListWithRange.forEach((yAxisModel) => {
const yDisplayRange = yAxisModel.get('displayRange');
if (!yDisplayRange) {
return;
}
const yPosition = this.getYPositionForYAxis(this.positionOverPlot, yAxisModel);
let yAxisDist = (yDisplayRange.max - yDisplayRange.min);
let yDistMouseToMax = yDisplayRange.max - yPosition;
let yDistMouseToMin = yPosition - yDisplayRange.min;
let yAxisMaxDist = yDistMouseToMax / yAxisDist;
let yAxisMinDist = yDistMouseToMin / yAxisDist;
yAxisModel.set('displayRange', {
min: yDisplayRange.min + ((yAxisDist * ZOOM_AMT) * yAxisMinDist),
max: yDisplayRange.max - ((yAxisDist * ZOOM_AMT) * yAxisMaxDist)
});
this.config.yAxis.set('displayRange', {
min: yDisplayRange.min + ((yAxisDist * ZOOM_AMT) * yAxisMinDist),
max: yDisplayRange.max - ((yAxisDist * ZOOM_AMT) * yAxisMaxDist)
});
} else if (event.wheelDelta >= 0) {
this.config.xAxis.set('displayRange', {
min: xDisplayRange.min - ((xAxisDist * ZOOM_AMT) * xAxisMinDist),
max: xDisplayRange.max + ((xAxisDist * ZOOM_AMT) * xAxisMaxDist)
});
this.yAxisListWithRange.forEach((yAxisModel) => {
const yDisplayRange = yAxisModel.get('displayRange');
if (!yDisplayRange) {
return;
}
const yPosition = this.getYPositionForYAxis(this.positionOverPlot, yAxisModel);
let yAxisDist = (yDisplayRange.max - yDisplayRange.min);
let yDistMouseToMax = yDisplayRange.max - yPosition;
let yDistMouseToMin = yPosition - yDisplayRange.min;
let yAxisMaxDist = yDistMouseToMax / yAxisDist;
let yAxisMinDist = yDistMouseToMin / yAxisDist;
yAxisModel.set('displayRange', {
min: yDisplayRange.min - ((yAxisDist * ZOOM_AMT) * yAxisMinDist),
max: yDisplayRange.max + ((yAxisDist * ZOOM_AMT) * yAxisMaxDist)
});
this.config.yAxis.set('displayRange', {
min: yDisplayRange.min - ((yAxisDist * ZOOM_AMT) * yAxisMinDist),
max: yDisplayRange.max + ((yAxisDist * ZOOM_AMT) * yAxisMaxDist)
});
}
@@ -1567,48 +1111,24 @@ export default {
}
const dX = this.pan.start.x - this.positionOverPlot.x;
const dY = this.pan.start.y - this.positionOverPlot.y;
const xRange = this.config.xAxis.get('displayRange');
const yRange = this.config.yAxis.get('displayRange');
this.config.xAxis.set('displayRange', {
min: xRange.min + dX,
max: xRange.max + dX
});
const dY = [];
this.positionOverPlot.y.forEach((yAxisPosition, index) => {
const yAxisId = this.positionOverPlot.yAxisIds[index];
dY.push({
yAxisId: yAxisId,
y: this.pan.start.y[index] - yAxisPosition
});
});
this.yAxisListWithRange.forEach((yAxis) => {
const yRange = yAxis.get('displayRange');
if (!yRange) {
return;
}
const yIndex = dY.findIndex(y => y.yAxisId === yAxis.get('id'));
yAxis.set('displayRange', {
min: yRange.min + dY[yIndex].y,
max: yRange.max + dY[yIndex].y
});
this.config.yAxis.set('displayRange', {
min: yRange.min + dY,
max: yRange.max + dY
});
},
trackHistory() {
const yRangeList = [];
const yAxisIds = [];
this.yAxisListWithRange.forEach((yAxis) => {
yRangeList.push(yAxis.get('displayRange'));
yAxisIds.push(yAxis.get('id'));
});
this.plotHistory.push({
x: this.config.xAxis.get('displayRange'),
y: yRangeList,
yAxisIds
y: this.config.yAxis.get('displayRange')
});
},
@@ -1618,9 +1138,7 @@ export default {
},
freeze() {
this.yAxisListWithRange.forEach((yAxis) => {
yAxis.set('frozen', true);
});
this.config.yAxis.set('frozen', true);
this.config.xAxis.set('frozen', true);
this.setStatus();
},
@@ -1631,9 +1149,7 @@ export default {
},
clearPanZoomHistory() {
this.yAxisListWithRange.forEach((yAxis) => {
yAxis.set('frozen', false);
});
this.config.yAxis.set('frozen', false);
this.config.xAxis.set('frozen', false);
this.setStatus();
this.plotHistory = [];
@@ -1648,11 +1164,7 @@ export default {
}
this.config.xAxis.set('displayRange', previousAxisRanges.x);
this.yAxisListWithRange.forEach((yAxis) => {
const yPosition = this.getYPositionForYAxis(previousAxisRanges, yAxis);
yAxis.set('displayRange', yPosition);
});
this.config.yAxis.set('displayRange', previousAxisRanges.y);
this.userViewportChangeEnd();
},

View File

@@ -23,7 +23,6 @@
<div
ref="plotWrapper"
class="c-plot holder holder-plot has-control-bar"
:class="staleClass"
>
<div
ref="plotContainer"
@@ -51,7 +50,6 @@ import eventHelpers from './lib/eventHelpers';
import ImageExporter from '../../exporters/ImageExporter';
import MctPlot from './MctPlot.vue';
import ProgressBar from "../../ui/components/ProgressBar.vue";
import StalenessUtils from '@/utils/staleness';
export default {
components: {
@@ -76,95 +74,21 @@ export default {
cursorGuide: false,
gridLines: !this.options.compact,
loading: false,
status: '',
staleObjects: []
status: ''
};
},
computed: {
staleClass() {
if (this.staleObjects.length !== 0) {
return 'is-stale';
}
return '';
}
},
mounted() {
eventHelpers.extend(this);
this.imageExporter = new ImageExporter(this.openmct);
this.loadComposition();
this.stalenessSubscription = {};
},
beforeDestroy() {
this.destroy();
},
methods: {
loadComposition() {
this.compositionCollection = this.openmct.composition.get(this.domainObject);
if (this.compositionCollection) {
this.compositionCollection.on('add', this.addItem);
this.compositionCollection.on('remove', this.removeItem);
this.compositionCollection.load();
}
},
addItem(object) {
const keystring = this.openmct.objects.makeKeyString(object.identifier);
if (!this.stalenessSubscription[keystring]) {
this.stalenessSubscription[keystring] = {};
this.stalenessSubscription[keystring].stalenessUtils = new StalenessUtils(this.openmct, object);
}
this.openmct.telemetry.isStale(object).then((stalenessResponse) => {
if (stalenessResponse !== undefined) {
this.handleStaleness(keystring, stalenessResponse);
}
});
const unsubscribeFromStaleness = this.openmct.telemetry.subscribeToStaleness(object, (stalenessResponse) => {
this.handleStaleness(keystring, stalenessResponse);
});
this.stalenessSubscription[keystring].unsubscribe = unsubscribeFromStaleness;
},
removeItem(object) {
const SKIP_CHECK = true;
const keystring = this.openmct.objects.makeKeyString(object);
this.stalenessSubscription[keystring].unsubscribe();
this.stalenessSubscription[keystring].stalenessUtils.destroy();
this.handleStaleness(keystring, { isStale: false }, SKIP_CHECK);
},
handleStaleness(id, stalenessResponse, skipCheck = false) {
if (skipCheck || this.stalenessSubscription[id].stalenessUtils.shouldUpdateStaleness(stalenessResponse, id)) {
const index = this.staleObjects.indexOf(id);
if (stalenessResponse.isStale) {
if (index === -1) {
this.staleObjects.push(id);
}
} else {
if (index !== -1) {
this.staleObjects.splice(index, 1);
}
}
}
},
loadingUpdated(loading) {
this.loading = loading;
},
destroy() {
if (this.stalenessSubscription) {
Object.values(this.stalenessSubscription).forEach(stalenessSubscription => {
stalenessSubscription.unsubscribe();
stalenessSubscription.stalenessUtils.destroy();
});
}
if (this.compositionCollection) {
this.compositionCollection.off('add', this.addItem);
this.compositionCollection.off('remove', this.removeItem);
}
this.stopListening();
},
exportJPG() {

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