Compare commits

..

3 Commits

Author SHA1 Message Date
Jesse Mazzella
f146f07fcd [WIP] start filling out test stub for remote clock tests 2022-09-15 13:44:59 -07:00
Jesse Mazzella
599b1865b6 Add method to set remote clock mode 2022-09-15 13:44:42 -07:00
Jesse Mazzella
eab6071095 Make time conductor modes have unique testIds 2022-09-15 13:44:10 -07:00
138 changed files with 1464 additions and 4397 deletions

View File

@@ -21,9 +21,9 @@ Closes <!--- Insert Issue Number(s) this PR addresses. Start by typing # will op
### Reviewer Checklist
* [ ] Changes appear to address issue?
* [ ] Reviewer has tested changes by following the provided instructions?
* [ ] Changes appear not to be breaking changes?
* [ ] Appropriate automated tests included?
* [ ] Appropriate unit tests included?
* [ ] Code style and in-line documentation are appropriate?
* [ ] Commit messages meet standards?
* [ ] Has associated issue been labelled unverified? (only applicable if this PR closes the issue)
* [ ] Has associated issue been labelled bug? (only applicable if this PR is for a bug fix)

View File

@@ -1 +0,0 @@
name: 'Custom CodeQL config'

View File

@@ -13,12 +13,12 @@ updates:
- "pr:daveit"
- "pr:platform"
ignore:
- dependency-name: "@playwright/test" #We have to source the playwright container which is not detected by Dependabot
- dependency-name: "playwright-core" #We have to source the playwright container which is not detected by Dependabot
- dependency-name: "@babel/eslint-parser" #Lots of noise in these type patch releases.
update-types: ["version-update:semver-patch"]
- dependency-name: "eslint-plugin-vue" #Lots of noise in these type patch releases.
#We have to source the container which is not detected by Dependabot
- dependency-name: "@playwright/test"
#Lots of noise in these type patch releases.
- dependency-name: "@babel/eslint-parser"
update-types: ["version-update:semver-patch"]
- package-ecosystem: "github-actions"
directory: "/"
schedule:

View File

@@ -1,10 +1,11 @@
name: 'CodeQL'
name: "CodeQL"
on:
push:
branches: [master, 'release/*']
branches: [ master ]
pull_request:
branches: [master, 'release/*']
branches: [ master ]
paths-ignore:
- '**/*Spec.js'
- '**/*.md'
@@ -26,19 +27,17 @@ jobs:
security-events: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Checkout repository
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
config-file: ./.github/codeql/codeql-config.yml
languages: javascript
queries: security-and-quality
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: javascript
- name: Autobuild
uses: github/codeql-action/autobuild@v2
- name: Autobuild
uses: github/codeql-action/autobuild@v2
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2

98
.github/workflows/lighthouse.yml vendored Normal file
View File

@@ -0,0 +1,98 @@
name: lighthouse
on:
workflow_dispatch:
inputs:
version:
description: 'Which branch do you want to test?' # Limited to branch for now
required: false
default: 'master'
pull_request:
types:
- labeled
jobs:
lighthouse-pr:
if: ${{ github.event.label.name == 'pr:lighthouse' }}
runs-on: ubuntu-latest
steps:
- name: Checkout Master for Baseline
uses: actions/checkout@v3
with:
ref: master #explicitly checkout master for baseline
- name: Install Node 16
uses: actions/setup-node@v3
with:
node-version: '16'
- name: Cache node modules
uses: actions/cache@v2
env:
cache-name: cache-node-modules
with:
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
- name: npm install with lighthouse cli
run: npm install && npm install -g @lhci/cli
- name: Run lhci against master to generate baseline and ignore exit codes
run: lhci autorun || true
- name: Perform clean checkout of PR
uses: actions/checkout@v3
with:
clean: true
- name: Install Node version which is compatible with PR
uses: actions/setup-node@v3
- name: npm install with lighthouse cli
run: npm install && npm install -g @lhci/cli
- name: Run lhci with PR
run: lhci autorun
env:
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
lighthouse-nightly:
if: ${{ github.event.schedule }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Node 16
uses: actions/setup-node@v3
with:
node-version: '16'
- name: Cache node modules
uses: actions/cache@v2
env:
cache-name: cache-node-modules
with:
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
- name: npm install with lighthouse cli
run: npm install && npm install -g @lhci/cli
- name: Run lhci against master to generate baseline
run: lhci autorun
env:
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
lighthouse-dispatch:
if: ${{ github.event.workflow_dispatch }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
ref: ${{ github.event.inputs.version }}
- name: Install Node 14
uses: actions/setup-node@v3
with:
node-version: '16'
- name: Cache node modules
uses: actions/cache@v3
env:
cache-name: cache-node-modules
with:
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
- name: npm install with lighthouse cli
run: npm install && npm install -g @lhci/cli
- name: Run lhci against master to generate baseline
run: lhci autorun

View File

@@ -10,6 +10,9 @@
# https://github.com/nasa/openmct/issues/4992
!/example/**/*
# We will remove this in https://github.com/nasa/openmct/issues/4922
!/app.js
# ...except for these files in the above folders.
/src/**/*Spec.js
/src/**/test/

7
API.md
View File

@@ -57,7 +57,7 @@
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
# Developing Applications With Open MCT
# Building Applications With Open MCT
## Scope and purpose of this document
@@ -72,7 +72,8 @@ MCT, as well as addressing some common developer use cases.
## Building From Source
The latest version of Open MCT is available from [our GitHub repository](https://github.com/nasa/openmct).
If you have `git`, and `node` installed, you can build Open MCT with the commands
If you have `git`, and `node` installed, you can build Open MCT with the
commands
```bash
git clone https://github.com/nasa/openmct.git
@@ -85,7 +86,7 @@ build a minified version that can be included in your application. The output
of the build process is placed in a `dist` folder under the openmct source
directory, which can be copied out to another location as needed. The contents
of this folder will include a minified javascript file named `openmct.js` as
well as assets such as html, css, and images necessary for the UI.
well as assets such as html, css, and images necessary for the UI.
## Starting an Open MCT application

1
Procfile Normal file
View File

@@ -0,0 +1 @@
web: node app.js --port $PORT

View File

@@ -30,8 +30,6 @@ Building and running Open MCT in your local dev environment is very easy. Be sur
Open MCT is now running, and can be accessed by pointing a web browser at [http://localhost:8080/](http://localhost:8080/)
Open MCT is built using [`npm`](http://npmjs.com/) and [`webpack`](https://webpack.js.org/).
## Documentation
Documentation is available on the [Open MCT website](https://nasa.github.io/openmct/documentation/).
@@ -45,9 +43,11 @@ our documentation.
We want Open MCT to be as easy to use, install, run, and develop for as
possible, and your feedback will help us get there! Feedback can be provided via [GitHub issues](https://github.com/nasa/openmct/issues/new/choose), [Starting a GitHub Discussion](https://github.com/nasa/openmct/discussions), or by emailing us at [arc-dl-openmct@mail.nasa.gov](mailto:arc-dl-openmct@mail.nasa.gov).
## Developing Applications With Open MCT
## Building Applications With Open MCT
For more on developing with Open MCT, see our documentation for a guide on [Developing Applications with Open MCT](./API.md#starting-an-open-mct-application).
Open MCT is built using [`npm`](http://npmjs.com/) and [`webpack`](https://webpack.js.org/).
See our documentation for a guide on [building Applications with Open MCT](https://github.com/nasa/openmct/blob/master/API.md#starting-an-open-mct-application).
## Compatibility
@@ -64,7 +64,7 @@ that is intended to be added or removed as a single unit.
As well as providing an extension mechanism, most of the core Open MCT codebase is also
written as plugins.
For information on writing plugins, please see [our API documentation](./API.md#plugins).
For information on writing plugins, please see [our API documentation](https://github.com/nasa/openmct/blob/master/API.md#plugins).
## Tests
@@ -100,7 +100,7 @@ To run the performance tests:
The test suite is configured to all tests localed in `e2e/tests/` ending in `*.e2e.spec.js`. For more about the e2e test suite, please see the [README](./e2e/README.md)
### Security Tests
Each commit is analyzed for known security vulnerabilities using [CodeQL](https://codeql.github.com/docs/codeql-language-guides/codeql-library-for-javascript/) and our overall security report is available on [LGTM](https://lgtm.com/projects/g/nasa/openmct/). The list of CWE coverage items is avaiable in the [CodeQL docs](https://codeql.github.com/codeql-query-help/javascript-cwe/). The CodeQL workflow is specified in the [CodeQL analysis file](./.github/workflows/codeql-analysis.yml) and the custom [CodeQL config](./.github/codeql/codeql-config.yml).
Each commit is analyzed for known security vulnerabilities using [CodeQL](https://codeql.github.com/docs/codeql-language-guides/codeql-library-for-javascript/) and our overall security report is available on [LGTM](https://lgtm.com/projects/g/nasa/openmct/)
### Test Reporting and Code Coverage

92
app.js Normal file
View File

@@ -0,0 +1,92 @@
/*global process*/
/**
* Usage:
*
* npm install minimist express
* node app.js [options]
*/
const options = require('minimist')(process.argv.slice(2));
const express = require('express');
const app = express();
const fs = require('fs');
const request = require('request');
const __DEV__ = !process.env.NODE_ENV || process.env.NODE_ENV === 'development';
// Defaults
options.port = options.port || options.p || 8080;
options.host = options.host || 'localhost';
options.directory = options.directory || options.D || '.';
// Show command line options
if (options.help || options.h) {
console.log("\nUsage: node app.js [options]\n");
console.log("Options:");
console.log(" --help, -h Show this message.");
console.log(" --port, -p <number> Specify port.");
console.log(" --directory, -D <bundle> Serve files from specified directory.");
console.log("");
process.exit(0);
}
app.disable('x-powered-by');
app.use('/proxyUrl', function proxyRequest(req, res, next) {
console.log('Proxying request to: ', req.query.url);
req.pipe(request({
url: req.query.url,
strictSSL: false
}).on('error', next)).pipe(res);
});
class WatchRunPlugin {
apply(compiler) {
compiler.hooks.emit.tapAsync('WatchRunPlugin', (compilation, callback) => {
console.log('Begin compile at ' + new Date());
callback();
});
}
}
const webpack = require('webpack');
let webpackConfig;
if (__DEV__) {
webpackConfig = require('./webpack.dev');
webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin());
webpackConfig.entry.openmct = [
'webpack-hot-middleware/client?reload=true',
webpackConfig.entry.openmct
];
webpackConfig.plugins.push(new WatchRunPlugin());
} else {
webpackConfig = require('./webpack.coverage');
}
const compiler = webpack(webpackConfig);
app.use(require('webpack-dev-middleware')(
compiler,
{
publicPath: '/dist',
stats: 'errors-warnings'
}
));
if (__DEV__) {
app.use(require('webpack-hot-middleware')(
compiler,
{}
));
}
// Expose index.html for development users.
app.get('/', function (req, res) {
fs.createReadStream('index.html').pipe(res);
});
// Finally, open the HTTP server and log the instance to the console
app.listen(options.port, options.host, function () {
console.log('Open MCT application running at %s:%s', options.host, options.port);
});

3
docs/footer.html Normal file
View File

@@ -0,0 +1,3 @@
<hr>
</body>
</html>

209
docs/gendocs.js Normal file
View File

@@ -0,0 +1,209 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/*global require,process,__dirname,GLOBAL*/
/*jslint nomen: false */
// Usage:
// node gendocs.js --in <source directory> --out <dest directory>
var CONSTANTS = {
DIAGRAM_WIDTH: 800,
DIAGRAM_HEIGHT: 500
},
TOC_HEAD = "# Table of Contents";
GLOBAL.window = GLOBAL.window || GLOBAL; // nomnoml expects window to be defined
(function () {
"use strict";
var fs = require("fs"),
mkdirp = require("mkdirp"),
path = require("path"),
glob = require("glob"),
marked = require("marked"),
split = require("split"),
stream = require("stream"),
nomnoml = require('nomnoml'),
toc = require("markdown-toc"),
Canvas = require('canvas'),
header = fs.readFileSync(path.resolve(__dirname, 'header.html')),
footer = fs.readFileSync(path.resolve(__dirname, 'footer.html')),
options = require("minimist")(process.argv.slice(2));
// Convert from nomnoml source to a target PNG file.
function renderNomnoml(source, target) {
var canvas =
new Canvas(CONSTANTS.DIAGRAM_WIDTH, CONSTANTS.DIAGRAM_HEIGHT);
nomnoml.draw(canvas, source, 1.0);
canvas.pngStream().pipe(fs.createWriteStream(target));
}
// Stream transform.
// Pulls out nomnoml diagrams from fenced code blocks and renders them
// as PNG files in the output directory, prefixed with a provided name.
// The fenced code blocks will be replaced with Markdown in the
// output of this stream.
function nomnomlifier(outputDirectory, prefix) {
var transform = new stream.Transform({ objectMode: true }),
isBuilding = false,
counter = 1,
outputPath,
source = "";
transform._transform = function (chunk, encoding, done) {
if (!isBuilding) {
if (chunk.trim().indexOf("```nomnoml") === 0) {
var outputFilename = prefix + '-' + counter + '.png';
outputPath = path.join(outputDirectory, outputFilename);
this.push([
"\n![Diagram ",
counter,
"](",
outputFilename,
")\n\n"
].join(""));
isBuilding = true;
source = "";
counter += 1;
} else {
// Otherwise, pass through
this.push(chunk + '\n');
}
} else {
if (chunk.trim() === "```") {
// End nomnoml
renderNomnoml(source, outputPath);
isBuilding = false;
} else {
source += chunk + '\n';
}
}
done();
};
return transform;
}
// Convert from Github-flavored Markdown to HTML
function gfmifier(renderTOC) {
var transform = new stream.Transform({ objectMode: true }),
markdown = "";
transform._transform = function (chunk, encoding, done) {
markdown += chunk;
done();
};
transform._flush = function (done) {
if (renderTOC){
// Prepend table of contents
markdown =
[ TOC_HEAD, toc(markdown).content, "", markdown ].join("\n");
}
this.push(header);
this.push(marked(markdown));
this.push(footer);
done();
};
return transform;
}
// Custom renderer for marked; converts relative links from md to html,
// and makes headings linkable.
function CustomRenderer() {
var renderer = new marked.Renderer(),
customRenderer = Object.create(renderer);
customRenderer.heading = function (text, level) {
var escapedText = (text || "").trim().toLowerCase().replace(/\W/g, "-"),
aOpen = "<a name=\"" + escapedText + "\" href=\"#" + escapedText + "\">",
aClose = "</a>";
return aOpen + renderer.heading.apply(renderer, arguments) + aClose;
};
// Change links to .md files to .html
customRenderer.link = function (href, title, text) {
// ...but only if they look like relative paths
return (href || "").indexOf(":") === -1 && href[0] !== "/" ?
renderer.link(href.replace(/\.md/, ".html"), title, text) :
renderer.link.apply(renderer, arguments);
};
return customRenderer;
}
options['in'] = options['in'] || options.i;
options.out = options.out || options.o;
marked.setOptions({
renderer: new CustomRenderer(),
gfm: true,
tables: true,
breaks: false,
pedantic: false,
sanitize: true,
smartLists: true,
smartypants: false
});
// Convert all markdown files.
// First, pull out nomnoml diagrams.
// Then, convert remaining Markdown to HTML.
glob(options['in'] + "/**/*.md", {}, function (err, files) {
files.forEach(function (file) {
var destination = file.replace(options['in'], options.out)
.replace(/md$/, "html"),
destPath = path.dirname(destination),
prefix = path.basename(destination).replace(/\.html$/, ""),
//Determine whether TOC should be rendered for this file based
//on regex provided as command line option
renderTOC = file.match(options['suppress-toc'] || "") === null;
mkdirp(destPath, function (err) {
fs.createReadStream(file, { encoding: 'utf8' })
.pipe(split())
.pipe(nomnomlifier(destPath, prefix))
.pipe(gfmifier(renderTOC))
.pipe(fs.createWriteStream(destination, {
encoding: 'utf8'
}));
});
});
});
// Also copy over all HTML, CSS, or PNG files
glob(options['in'] + "/**/*.@(html|css|png)", {}, function (err, files) {
files.forEach(function (file) {
var destination = file.replace(options['in'], options.out),
destPath = path.dirname(destination),
streamOptions = {};
if (file.match(/png$/)) {
streamOptions.encoding = null;
} else {
streamOptions.encoding = 'utf8';
}
mkdirp(destPath, function (err) {
fs.createReadStream(file, streamOptions)
.pipe(fs.createWriteStream(destination, streamOptions));
});
});
});
}());

9
docs/header.html Normal file
View File

@@ -0,0 +1,9 @@
<html>
<head>
<link rel="stylesheet"
href="//nasa.github.io/openmct/static/res/css/styles.css">
<link rel="stylesheet"
href="//nasa.github.io/openmct/static/res/css/documentation.css">
</head>
<body>

View File

@@ -15,8 +15,8 @@
## Sections
* The [API](api/) uses inline documentation
using [TypeScript](https://www.typescriptlang.org) and some legacy [JSDoc](https://jsdoc.app/). It describes the JavaScript objects and
* The [API](api/) document is generated from inline documentation
using [JSDoc](http://usejsdoc.org/), and describes the JavaScript objects and
functions that make up the software platform.
* The [Development Process](process/) document describes the

View File

@@ -151,7 +151,7 @@ Current list of test tags:
- `@ipad` - Test case or test suite is compatible with Playwright's iPad support and Open MCT's read-only mobile view (i.e. no Create button).
- `@gds` - Denotes a GDS Test Case used in the VIPER Mission.
- `@addInit` - Initializes the browser with an injected and artificial state. Useful for loading non-default plugins. Likely will not work outside of `npm start`.
- `@addInit` - Initializes the browser with an injected and artificial state. Useful for loading non-default plugins. Likely will not work outside of app.js.
- `@localStorage` - Captures or generates session storage to manipulate browser state. Useful for excluding in tests which require a persistent backend (i.e. CouchDB).
- `@snapshot` - Uses Playwright's snapshot functionality to record a copy of the DOM for direct comparison. Must be run inside of the playwright container.
- `@unstable` - A new test or test which is known to be flaky.

View File

@@ -102,17 +102,6 @@ async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine
};
}
/**
* @param {import('@playwright/test').Page} page
* @param {string} name
*/
async function expandTreePaneItemByName(page, name) {
const treePane = page.locator('#tree-pane');
const treeItem = treePane.locator(`role=treeitem[expanded=false][name=/${name}/]`);
const expandTriangle = treeItem.locator('.c-disclosure-triangle');
await expandTriangle.click();
}
/**
* Create a Plan object from JSON with the provided options.
* @param {import('@playwright/test').Page} page
@@ -239,18 +228,14 @@ async function _isInEditMode(page, identifier) {
/**
* Set the time conductor mode to either fixed timespan or realtime mode.
* @param {import('@playwright/test').Page} page
* @param {boolean} [isFixedTimespan=true] true for fixed timespan mode, false for realtime mode; default is true
* @param {'fixed'|'local-clock'|'remote-clock'} [clockType='fixed'] the clock type to set the time conductor to. default: 'fixed'
*/
async function setTimeConductorMode(page, isFixedTimespan = true) {
async function setTimeConductorMode(page, clockType = 'fixed') {
// Click 'mode' button
await page.locator('.c-mode-button').click();
// Switch time conductor mode
if (isFixedTimespan) {
await page.locator('data-testid=conductor-modeOption-fixed').click();
} else {
await page.locator('data-testid=conductor-modeOption-realtime').click();
}
await page.locator(`data-testid=conductor-modeOption-${clockType}`).click();
}
/**
@@ -258,15 +243,23 @@ async function setTimeConductorMode(page, isFixedTimespan = true) {
* @param {import('@playwright/test').Page} page
*/
async function setFixedTimeMode(page) {
await setTimeConductorMode(page, true);
await setTimeConductorMode(page);
}
/**
* Set the time conductor to realtime mode
* Set the time conductor to local clock mode
* @param {import('@playwright/test').Page} page
*/
async function setRealTimeMode(page) {
await setTimeConductorMode(page, false);
async function setLocalClockMode(page) {
await setTimeConductorMode(page, 'local-clock');
}
/**
* Set the time conductor to remote clock mode
* @param {import('@playwright/test').Page} page
*/
async function setRemoteClockMode(page) {
await setTimeConductorMode(page, 'remote-clock');
}
/**
@@ -324,13 +317,13 @@ async function setEndOffset(page, offset) {
// eslint-disable-next-line no-undef
module.exports = {
createDomainObjectWithDefaults,
expandTreePaneItemByName,
createPlanFromJSON,
openObjectTreeContextMenu,
getHashUrlToDomainObject,
getFocusedObjectUuid,
setFixedTimeMode,
setRealTimeMode,
setLocalClockMode,
setRemoteClockMode,
setStartOffset,
setEndOffset
};

View File

@@ -1,65 +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.
*****************************************************************************/
const NOTEBOOK_DROP_AREA = '.c-notebook__drag-area';
/**
* @param {import('@playwright/test').Page} page
*/
async function enterTextEntry(page, text) {
// Click .c-notebook__drag-area
await page.locator(NOTEBOOK_DROP_AREA).click();
// enter text
await page.locator('div.c-ne__text').click();
await page.locator('div.c-ne__text').fill(text);
await page.locator('div.c-ne__text').press('Enter');
}
/**
* @param {import('@playwright/test').Page} page
*/
async function dragAndDropEmbed(page, myItemsFolderName) {
// Click button:has-text("Create")
await page.locator('button:has-text("Create")').click();
// Click li:has-text("Sine Wave Generator")
await page.locator('li:has-text("Sine Wave Generator")').click();
// Click form[name="mctForm"] >> text=My Items
await page.locator(`form[name="mctForm"] >> text=${myItemsFolderName}`).click();
// Click text=OK
await page.locator('text=OK').click();
// Click text=Open MCT My Items >> span >> nth=3
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
// Click text=Unnamed CUSTOM_NAME
await Promise.all([
page.waitForNavigation(),
page.locator('text=Unnamed CUSTOM_NAME').click()
]);
await page.dragAndDrop('text=UNNAMED SINE WAVE GENERATOR', NOTEBOOK_DROP_AREA);
}
// eslint-disable-next-line no-undef
module.exports = {
enterTextEntry,
dragAndDropEmbed
};

View File

@@ -14,7 +14,7 @@ const config = {
testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-perfromance.config.js
timeout: 60 * 1000,
webServer: {
command: 'npm run start:coverage',
command: 'cross-env NODE_ENV=test npm run start',
url: 'http://localhost:8080/#',
timeout: 200 * 1000,
reuseExistingServer: false

View File

@@ -12,7 +12,10 @@ const config = {
testIgnore: '**/*.perf.spec.js',
timeout: 30 * 1000,
webServer: {
command: 'npm run start:coverage',
env: {
NODE_ENV: 'test'
},
command: 'npm run start',
url: 'http://localhost:8080/#',
timeout: 120 * 1000,
reuseExistingServer: true

View File

@@ -6,12 +6,12 @@ const CI = process.env.CI === 'true';
/** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = {
retries: 1, //Only for debugging purposes for trace: 'on-first-retry'
retries: 1, //Only for debugging purposes because trace is enabled only on first retry
testDir: 'tests/performance/',
timeout: 60 * 1000,
workers: 1, //Only run in serial with 1 worker
webServer: {
command: 'npm run start', //coverage not generated
command: 'cross-env NODE_ENV=test npm run start',
url: 'http://localhost:8080/#',
timeout: 200 * 1000,
reuseExistingServer: !CI

View File

@@ -4,13 +4,13 @@
/** @type {import('@playwright/test').PlaywrightTestConfig<{ theme: string }>} */
const config = {
retries: 0, // Visual tests should never retry due to snapshot comparison errors. Leaving as a shim
retries: 1, // visual tests should never retry due to snapshot comparison errors. Leaving as a shim
testDir: 'tests/visual',
testMatch: '**/*.visual.spec.js', // only run visual tests
timeout: 60 * 1000,
workers: 1, //Lower stress on Circle CI Agent for Visual tests https://github.com/percy/cli/discussions/1067
webServer: {
command: 'npm run start:coverage',
command: 'cross-env NODE_ENV=test npm run start',
url: 'http://localhost:8080/#',
timeout: 200 * 1000,
reuseExistingServer: !process.env.CI
@@ -31,7 +31,7 @@ const config = {
}
},
{
name: 'chrome-snow-theme', //Runs the same visual tests but with snow-theme enabled
name: 'chrome-snow-theme',
use: {
browserName: 'chromium',
theme: 'snow'

File diff suppressed because one or more lines are too long

View File

@@ -23,7 +23,7 @@
/*
This test suite is dedicated to testing our use of the playwright framework as it
relates to how we've extended it (i.e. ./e2e/baseFixtures.js) and assumptions made in our dev environment
(`npm start` and ./e2e/webpack-dev-middleware.js)
(app.js and ./e2e/webpack-dev-middleware.js)
*/
const { test } = require('../../baseFixtures.js');

View File

@@ -100,7 +100,7 @@ test.describe("CouchDB initialization @couchdb", () => {
expect(bannerMessage).toEqual('Failed to retrieve object mine');
// Verify that a PUT request to create "My Items" folder was made
await expect.poll(() => createMineFolderRequests.length, {
expect.poll(() => createMineFolderRequests.length, {
message: 'Verify that PUT request to create "mine" folder was made',
timeout: 1000
}).toBeGreaterThanOrEqual(1);

View File

@@ -22,10 +22,19 @@
// FIXME: Remove this eslint exception once tests are implemented
// eslint-disable-next-line no-unused-vars
const { setRemoteClockMode } = require('../../../../appActions');
const { test, expect } = require('../../../../baseFixtures');
// const path = require('path');
test.describe('Remote Clock', () => {
// eslint-disable-next-line require-await
// test.use({ storageState: path.join(__dirname, '../../../../test-data/RemoteClockTestData_storage.json')});
// eslint-disable-next-line no-undef
test.beforeEach(async ({ page }) => {
// eslint-disable-next-line no-undef
// await page.addInitScript({ path: path.join(__dirname, '../../../../helper/', 'addInitRemoteClock.js') });
await page.goto('./', { waitUntil: 'networkidle' });
});
test.fixme('blocks historical requests until first tick is received', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
@@ -33,7 +42,11 @@ test.describe('Remote Clock', () => {
});
// addInitScript to with remote clock
// Switch time conductor mode to 'remote clock'
await setRemoteClockMode(page);
// Navigate to telemetry
await page.click('role=treeitem[name=/Remote Clock Ticker/]');
await page.reload();
// Verify that the plot renders historical data within the correct bounds
// Refresh the page
// Verify again that the plot renders historical data within the correct bounds

View File

@@ -21,13 +21,13 @@
*****************************************************************************/
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults, setStartOffset, setFixedTimeMode, setRealTimeMode } = require('../../../../appActions');
const { createDomainObjectWithDefaults, setStartOffset, setFixedTimeMode, setLocalClockMode } = require('../../../../appActions');
test.describe('Testing Display Layout @unstable', () => {
let sineWaveObject;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
await setRealTimeMode(page);
await setLocalClockMode(page);
// Create Sine Wave Generator
sineWaveObject = await createDomainObjectWithDefaults(page, {

View File

@@ -1,66 +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.
*****************************************************************************/
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
test.describe('Testing Flexible Layout @unstable', () => {
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
// Create Sine Wave Generator
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: "Test Sine Wave Generator"
});
// Create Clock Object
await createDomainObjectWithDefaults(page, {
type: 'Clock',
name: "Test Clock"
});
});
test('panes have the appropriate draggable attribute while in Edit and Browse modes', async ({ page }) => {
// Create a Flexible Layout
await createDomainObjectWithDefaults(page, {
type: 'Flexible Layout',
name: "Test Flexible Layout"
});
// Edit Flexible Layout
await page.locator('[title="Edit"]').click();
// Expand the 'My Items' folder in the left tree
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').first().click();
// Add the Sine Wave Generator and Clock to the Flexible Layout
await page.dragAndDrop('text=Test Sine Wave Generator', '.c-fl__container.is-empty');
await page.dragAndDrop('text=Test Clock', '.c-fl__container.is-empty');
// Check that panes can be dragged while Flexible Layout is in Edit mode
let dragWrapper = await page.locator('.c-fl-container__frames-holder .c-fl-frame__drag-wrapper').first();
await expect(dragWrapper).toHaveAttribute('draggable', 'true');
// Save Flexible Layout
await page.locator('button[title="Save"]').click();
await page.locator('text=Save and Finish Editing').click();
// Check that panes are not draggable while Flexible Layout is in Browse mode
dragWrapper = await page.locator('.c-fl-container__frames-holder .c-fl-frame__drag-wrapper').first();
await expect(dragWrapper).toHaveAttribute('draggable', 'false');
});
});

View File

@@ -160,18 +160,13 @@ test.describe('Example Imagery Object', () => {
await page.locator('.c-mode-button').click();
// Click local clock
await page.locator('[data-testid="conductor-modeOption-realtime"]').click();
await page.locator('[data-testid="conductor-modeOption-local-clock"]').click();
await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/);
// Zoom in via button
await zoomIntoImageryByButton(page);
await expect(pausePlayButton).not.toHaveClass(/is-paused/);
});
test('Uses low fetch priority', async ({ page }) => {
const priority = await page.locator('.js-imageryView-image').getAttribute('fetchpriority');
await expect(priority).toBe('low');
});
});
test.describe('Example Imagery in Display Layout', () => {
@@ -417,7 +412,7 @@ async function performImageryViewOperationsAndAssert(page) {
await page.locator('.c-mode-button').click();
// Select local clock mode
await page.locator('[data-testid=conductor-modeOption-realtime]').click();
await page.locator('[data-testid=conductor-modeOption-local-clock]').click();
// Zoom in on next image
await mouseZoomOnImageAndAssert(page, 2);

View File

@@ -21,13 +21,13 @@
*****************************************************************************/
const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults, setStartOffset, setFixedTimeMode, setRealTimeMode } = require('../../../../appActions');
const { createDomainObjectWithDefaults, setStartOffset, setFixedTimeMode, setLocalClockMode } = require('../../../../appActions');
test.describe('Testing LAD table @unstable', () => {
let sineWaveObject;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
await setRealTimeMode(page);
await setLocalClockMode(page);
// Create Sine Wave Generator
sineWaveObject = await createDomainObjectWithDefaults(page, {

View File

@@ -27,8 +27,7 @@ This test suite is dedicated to tests which verify the basic operations surround
// FIXME: Remove this eslint exception once tests are implemented
// eslint-disable-next-line no-unused-vars
const { test, expect } = require('../../../../baseFixtures');
const { expandTreePaneItemByName, createDomainObjectWithDefaults } = require('../../../../appActions');
const nbUtils = require('../../../../helper/notebookUtils');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
test.describe('Notebook CRUD Operations', () => {
test.fixme('Can create a Notebook Object', async ({ page }) => {
@@ -210,58 +209,13 @@ test.describe('Notebook search tests', () => {
test.describe('Notebook entry tests', () => {
test.fixme('When a new entry is created, it should be focused', async ({ page }) => {});
test('When an object is dropped into a notebook, a new entry is created and it should be focused @unstable', async ({ page }) => {
await page.goto('./#/browse/mine', { waitUntil: 'networkidle' });
// Create Notebook
const notebook = await createDomainObjectWithDefaults(page, {
type: 'Notebook',
name: "Embed Test Notebook"
});
// Create Overlay Plot
await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot',
name: "Dropped Overlay Plot"
});
await expandTreePaneItemByName(page, 'My Items');
await page.goto(notebook.url);
await page.dragAndDrop('role=treeitem[name=/Dropped Overlay Plot/]', '.c-notebook__drag-area');
const embed = page.locator('.c-ne__embed__link');
const embedName = await embed.textContent();
await expect(embed).toHaveClass(/icon-plot-overlay/);
expect(embedName).toBe('Dropped Overlay Plot');
test.fixme('When a telemetry object is dropped into a notebook, a new entry is created and it should be focused', async ({ page }) => {
// Drag and drop any telmetry object on 'drop object'
// new entry gets created with telemtry object
});
test('When an object is dropped into a notebooks existing entry, it should be focused @unstable', async ({ page }) => {
await page.goto('./#/browse/mine', { waitUntil: 'networkidle' });
// Create Notebook
const notebook = await createDomainObjectWithDefaults(page, {
type: 'Notebook',
name: "Embed Test Notebook"
});
// Create Overlay Plot
await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot',
name: "Dropped Overlay Plot"
});
await expandTreePaneItemByName(page, 'My Items');
await page.goto(notebook.url);
await nbUtils.enterTextEntry(page, 'Entry to drop into');
await page.dragAndDrop('role=treeitem[name=/Dropped Overlay Plot/]', 'text=Entry to drop into');
const existingEntry = page.locator('.c-ne__content', { has: page.locator('text="Entry to drop into"') });
const embed = existingEntry.locator('.c-ne__embed__link');
const embedName = await embed.textContent();
await expect(embed).toHaveClass(/icon-plot-overlay/);
expect(embedName).toBe('Dropped Overlay Plot');
test.fixme('When a telemetry object is dropped into a notebooks existing entry, it should be focused', async ({ page }) => {
// Drag and drop any telemetry object onto existing entry
// Entry updated with object and snapshot
});
test.fixme('new entries persist through navigation events without save', async ({ page }) => {});
test.fixme('previous and new entries can be deleted', async ({ page }) => {});

View File

@@ -1,271 +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 with CouchDB.
*/
const { test, expect } = require('../../../../baseFixtures');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
test.describe('Notebook Tests with CouchDB @couchdb', () => {
let testNotebook;
test.beforeEach(async ({ page }) => {
//Navigate to baseURL
await page.goto('./', { waitUntil: 'networkidle' });
// Create Notebook
testNotebook = await createDomainObjectWithDefaults(page, {
type: 'Notebook',
name: "TestNotebook"
});
});
test('Inspect Notebook Entry Network Requests', async ({ page }) => {
// Expand sidebar
await page.locator('.c-notebook__toggle-nav-button').click();
// Collect all request events to count and assert after notebook action
let addingNotebookElementsRequests = [];
page.on('request', (request) => addingNotebookElementsRequests.push(request));
let [notebookUrlRequest, allDocsRequest] = await Promise.all([
// Waits for the next request with the specified url
page.waitForRequest(`**/openmct/${testNotebook.uuid}`),
page.waitForRequest('**/openmct/_all_docs?include_docs=true'),
// Triggers the request
page.click('[aria-label="Add Page"]'),
// Ensures that there are no other network requests
page.waitForLoadState('networkidle')
]);
// Assert that only two requests are made
// Network Requests are:
// 1) The actual POST to create the page
// 2) The shared worker event from 👆 request
expect(addingNotebookElementsRequests.length).toBe(2);
// Assert on request object
expect(notebookUrlRequest.postDataJSON().metadata.name).toBe('TestNotebook');
expect(notebookUrlRequest.postDataJSON().model.persisted).toBeGreaterThanOrEqual(notebookUrlRequest.postDataJSON().model.modified);
expect(allDocsRequest.postDataJSON().keys).toContain(testNotebook.uuid);
// Add an entry
// Network Requests are:
// 1) The actual POST to create the entry
// 2) The shared worker event from 👆 POST request
addingNotebookElementsRequests = [];
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.waitForLoadState('networkidle');
expect(addingNotebookElementsRequests.length).toBeLessThanOrEqual(2);
// Add some tags
// Network Requests are for each tag creation are:
// 1) Getting the original path of the parent object
// 2) Getting the original path of the grandparent object (recursive call)
// 3) Creating the annotation/tag object
// 4) The shared worker event from 👆 POST request
// 5) Mutate notebook domain object's annotationModified property
// 6) The shared worker event from 👆 POST request
// 7) Notebooks fetching new annotations due to annotationModified changed
// 8) The update of the notebook domain's object's modified property
// 9) The shared worker event from 👆 POST request
// 10) Entry is timestamped
// 11) The shared worker event from 👆 POST request
addingNotebookElementsRequests = [];
await page.hover(`button:has-text("Add Tag")`);
await page.locator(`button:has-text("Add Tag")`).click();
await page.locator('[placeholder="Type to select tag"]').click();
await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click();
await page.waitForSelector('[aria-label="Tag"]:has-text("Driving")');
page.waitForLoadState('networkidle');
expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(11);
addingNotebookElementsRequests = [];
await page.hover(`button:has-text("Add Tag")`);
await page.locator(`button:has-text("Add Tag")`).click();
await page.locator('[placeholder="Type to select tag"]').click();
await page.locator('[aria-label="Autocomplete Options"] >> text=Drilling').click();
await page.waitForSelector('[aria-label="Tag"]:has-text("Drilling")');
page.waitForLoadState('networkidle');
expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(11);
addingNotebookElementsRequests = [];
await page.hover(`button:has-text("Add Tag")`);
await page.locator(`button:has-text("Add Tag")`).click();
await page.locator('[placeholder="Type to select tag"]').click();
await page.locator('[aria-label="Autocomplete Options"] >> text=Science').click();
await page.waitForSelector('[aria-label="Tag"]:has-text("Science")');
page.waitForLoadState('networkidle');
expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(11);
// Delete all the tags
// Network requests are:
// 1) Send POST to mutate _delete property to true on annotation with tag
// 2) The shared worker event from 👆 POST request
// 3) Timestamp update on entry
// 4) The shared worker event from 👆 POST request
// This happens for 3 tags so 12 requests
addingNotebookElementsRequests = [];
await page.hover('[aria-label="Tag"]:has-text("Driving")');
await page.locator('[aria-label="Tag"]:has-text("Driving") ~ .c-completed-tag-deletion').click();
await page.waitForSelector('[aria-label="Tag"]:has-text("Driving")', {state: 'hidden'});
await page.hover('[aria-label="Tag"]:has-text("Drilling")');
await page.locator('[aria-label="Tag"]:has-text("Drilling") ~ .c-completed-tag-deletion').click();
await page.waitForSelector('[aria-label="Tag"]:has-text("Drilling")', {state: 'hidden'});
page.hover('[aria-label="Tag"]:has-text("Science")');
await page.locator('[aria-label="Tag"]:has-text("Science") ~ .c-completed-tag-deletion').click();
await page.waitForSelector('[aria-label="Tag"]:has-text("Science")', {state: 'hidden'});
page.waitForLoadState('networkidle');
expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(12);
// Add two more pages
await page.click('[aria-label="Add Page"]');
await page.click('[aria-label="Add Page"]');
// Add three entries
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('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('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`);
// Add three tags
await page.hover(`button:has-text("Add Tag") >> nth=2`);
await page.locator(`button:has-text("Add Tag") >> nth=2`).click();
await page.locator('[placeholder="Type to select tag"]').click();
await page.locator('[aria-label="Autocomplete Options"] >> text=Science').click();
await page.waitForSelector('[aria-label="Tag"]:has-text("Science")');
await page.hover(`button:has-text("Add Tag") >> nth=2`);
await page.locator(`button:has-text("Add Tag") >> nth=2`).click();
await page.locator('[placeholder="Type to select tag"]').click();
await page.locator('[aria-label="Autocomplete Options"] >> text=Drilling').click();
await page.waitForSelector('[aria-label="Tag"]:has-text("Drilling")');
await page.hover(`button:has-text("Add Tag") >> nth=2`);
await page.locator(`button:has-text("Add Tag") >> nth=2`).click();
await page.locator('[placeholder="Type to select tag"]').click();
await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click();
await page.waitForSelector('[aria-label="Tag"]:has-text("Driving")');
page.waitForLoadState('networkidle');
// Add a fourth entry
// Network requests are:
// 1) Send POST to add new entry
// 2) The shared worker event from 👆 POST request
// 3) Timestamp update on entry
// 4) The shared worker event from 👆 POST request
addingNotebookElementsRequests = [];
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=3').click();
await page.locator('[aria-label="Notebook Entry Input"] >> nth=3').fill(`Fourth Entry`);
await page.locator('[aria-label="Notebook Entry Input"] >> nth=3').press('Enter');
page.waitForLoadState('networkidle');
expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(4);
// Add a fifth entry
// Network requests are:
// 1) Send POST to add new entry
// 2) The shared worker event from 👆 POST request
// 3) Timestamp update on entry
// 4) The shared worker event from 👆 POST request
addingNotebookElementsRequests = [];
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=4').click();
await page.locator('[aria-label="Notebook Entry Input"] >> nth=4').fill(`Fifth Entry`);
await page.locator('[aria-label="Notebook Entry Input"] >> nth=4').press('Enter');
page.waitForLoadState('networkidle');
expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(4);
// Add a sixth entry
// 1) Send POST to add new entry
// 2) The shared worker event from 👆 POST request
// 3) Timestamp update on entry
// 4) The shared worker event from 👆 POST request
addingNotebookElementsRequests = [];
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=5').click();
await page.locator('[aria-label="Notebook Entry Input"] >> nth=5').fill(`Sixth Entry`);
await page.locator('[aria-label="Notebook Entry Input"] >> nth=5').press('Enter');
page.waitForLoadState('networkidle');
expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(4);
});
test('Search tests', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/akhenry/openmct-yamcs/issues/69'
});
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');
// Add three tags
await page.hover(`button:has-text("Add Tag")`);
await page.locator(`button:has-text("Add Tag")`).click();
await page.locator('[placeholder="Type to select tag"]').click();
await page.locator('[aria-label="Autocomplete Options"] >> text=Science').click();
await page.waitForSelector('[aria-label="Tag"]:has-text("Science")');
await page.hover(`button:has-text("Add Tag")`);
await page.locator(`button:has-text("Add Tag")`).click();
await page.locator('[placeholder="Type to select tag"]').click();
await page.locator('[aria-label="Autocomplete Options"] >> text=Drilling').click();
await page.waitForSelector('[aria-label="Tag"]:has-text("Drilling")');
await page.hover(`button:has-text("Add Tag")`);
await page.locator(`button:has-text("Add Tag")`).click();
await page.locator('[placeholder="Type to select tag"]').click();
await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click();
await page.waitForSelector('[aria-label="Tag"]:has-text("Driving")');
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Sc');
await expect(page.locator('[aria-label="Search Result"]').first()).toContainText("Science");
await expect(page.locator('[aria-label="Search Result"]').first()).not.toContainText("Driving");
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Xq');
await expect(page.locator('text=No results found')).toBeVisible();
});
});
// Try to reduce indeterminism of browser requests by only returning fetch requests.
// Filter out preflight CORS, fetching stylesheets, page icons, etc. that can occur during tests
function filterNonFetchRequests(requests) {
return requests.filter(request => {
return (request.resourceType() === 'fetch');
});
}

View File

@@ -23,11 +23,11 @@
const { test, expect } = require('../../../../pluginFixtures');
const { openObjectTreeContextMenu, createDomainObjectWithDefaults } = require('../../../../appActions');
const path = require('path');
const nbUtils = require('../../../../helper/notebookUtils');
const TEST_TEXT = 'Testing text for entries.';
const TEST_TEXT_NAME = 'Test Page';
const CUSTOM_NAME = 'CUSTOM_NAME';
const NOTEBOOK_DROP_AREA = '.c-notebook__drag-area';
test.describe('Restricted Notebook', () => {
let notebook;
@@ -66,7 +66,7 @@ test.describe('Restricted Notebook', () => {
test('Can be locked if at least one page has one entry @addInit', async ({ page }) => {
await nbUtils.enterTextEntry(page, TEST_TEXT);
await enterTextEntry(page);
const commitButton = page.locator('button:has-text("Commit Entries")');
expect(await commitButton.count()).toEqual(1);
@@ -78,7 +78,7 @@ test.describe('Restricted Notebook with at least one entry and with the page loc
let notebook;
test.beforeEach(async ({ page }) => {
notebook = await startAndAddRestrictedNotebookObject(page);
await nbUtils.enterTextEntry(page, TEST_TEXT);
await enterTextEntry(page);
await lockPage(page);
// open sidebar
@@ -121,7 +121,7 @@ test.describe('Restricted Notebook with at least one entry and with the page loc
expect.soft(newPageCount).toEqual(1);
// enter test text
await nbUtils.enterTextEntry(page, TEST_TEXT);
await enterTextEntry(page);
// expect new page to be lockable
const commitButton = page.locator('BUTTON:HAS-TEXT("COMMIT ENTRIES")');
@@ -148,7 +148,7 @@ test.describe('Restricted Notebook with a page locked and with an embed @addInit
test.beforeEach(async ({ page, openmctConfig }) => {
const { myItemsFolderName } = openmctConfig;
await startAndAddRestrictedNotebookObject(page);
await nbUtils.dragAndDropEmbed(page, myItemsFolderName);
await dragAndDropEmbed(page, myItemsFolderName);
});
test('Allows embeds to be deleted if page unlocked @addInit', async ({ page }) => {
@@ -181,6 +181,42 @@ async function startAndAddRestrictedNotebookObject(page) {
return createDomainObjectWithDefaults(page, { type: CUSTOM_NAME });
}
/**
* @param {import('@playwright/test').Page} page
*/
async function enterTextEntry(page) {
// Click .c-notebook__drag-area
await page.locator(NOTEBOOK_DROP_AREA).click();
// enter text
await page.locator('div.c-ne__text').click();
await page.locator('div.c-ne__text').fill(TEST_TEXT);
await page.locator('div.c-ne__text').press('Enter');
}
/**
* @param {import('@playwright/test').Page} page
*/
async function dragAndDropEmbed(page, myItemsFolderName) {
// Click button:has-text("Create")
await page.locator('button:has-text("Create")').click();
// Click li:has-text("Sine Wave Generator")
await page.locator('li:has-text("Sine Wave Generator")').click();
// Click form[name="mctForm"] >> text=My Items
await page.locator(`form[name="mctForm"] >> text=${myItemsFolderName}`).click();
// Click text=OK
await page.locator('text=OK').click();
// Click text=Open MCT My Items >> span >> nth=3
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
// Click text=Unnamed CUSTOM_NAME
await Promise.all([
page.waitForNavigation(),
page.locator('text=Unnamed CUSTOM_NAME').click()
]);
await page.dragAndDrop('text=UNNAMED SINE WAVE GENERATOR', NOTEBOOK_DROP_AREA);
}
/**
* @param {import('@playwright/test').Page} page
*/

View File

@@ -39,7 +39,7 @@ async function createNotebookAndEntry(page, iterations = 1) {
createDomainObjectWithDefaults(page, { type: 'Notebook' });
for (let iteration = 0; iteration < iterations; iteration++) {
// Create an entry
// Click text=To start a new entry, click here or drag and drop any object
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
const entryLocator = `[aria-label="Notebook Entry Input"] >> nth = ${iteration}`;
await page.locator(entryLocator).click();
@@ -81,8 +81,10 @@ test.describe('Tagging in Notebooks @addInit', () => {
test('Can load tags', async ({ page }) => {
await createNotebookAndEntry(page);
// Click text=To start a new entry, click here or drag and drop any object
await page.locator('button:has-text("Add Tag")').click();
// Click [placeholder="Type to select tag"]
await page.locator('[placeholder="Type to select tag"]').click();
await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Science");
@@ -95,7 +97,9 @@ test.describe('Tagging in Notebooks @addInit', () => {
await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Science");
await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Driving");
// Click button:has-text("Add Tag")
await page.locator('button:has-text("Add Tag")').click();
// Click [placeholder="Type to select tag"]
await page.locator('[placeholder="Type to select tag"]').click();
await expect(page.locator('[aria-label="Autocomplete Options"]')).not.toContainText("Science");
@@ -104,56 +108,43 @@ test.describe('Tagging in Notebooks @addInit', () => {
});
test('Can search for tags', async ({ page }) => {
await createNotebookEntryAndTags(page);
// Click [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
// Fill [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
await expect(page.locator('[aria-label="Search Result"]')).toContainText("Science");
await expect(page.locator('[aria-label="Search Result"]')).not.toContainText("Driving");
await expect(page.locator('[aria-label="Search Result"]')).toContainText("Driving");
// Click [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
// Fill [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Sc');
await expect(page.locator('[aria-label="Search Result"]')).toContainText("Science");
await expect(page.locator('[aria-label="Search Result"]')).not.toContainText("Driving");
await expect(page.locator('[aria-label="Search Result"]')).toContainText("Driving");
// Click [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
// Fill [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Xq');
await expect(page.locator('text=No results found')).toBeVisible();
await expect(page.locator('[aria-label="Search Result"]')).toBeHidden();
await expect(page.locator('[aria-label="Search Result"]')).toBeHidden();
});
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 page.hover('.c-tag__label:has-text("Driving")');
await page.locator('.c-tag__label:has-text("Driving") ~ .c-completed-tag-deletion').click();
await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Science");
await expect(page.locator('[aria-label="Notebook Entry"]')).not.toContainText("Driving");
// Fill [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
await expect(page.locator('[aria-label="Search Result"]')).not.toContainText("Driving");
});
test('Can delete entries without tags', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5823'
});
await createNotebookEntryAndTags(page);
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
const entryLocator = `[aria-label="Notebook Entry Input"] >> nth = 1`;
await page.locator(entryLocator).click();
await page.locator(entryLocator).fill(`An entry without tags`);
await page.locator('[aria-label="Notebook Entry Input"] >> nth=1').press('Enter');
await page.hover('[aria-label="Notebook Entry Input"] >> nth=1');
await page.locator('button[title="Delete this entry"]').last().click();
await expect(page.locator('text=This action will permanently delete this entry. Do you wish to continue?')).toBeVisible();
await page.locator('button:has-text("Ok")').click();
await expect(page.locator('text=This action will permanently delete this entry. Do you wish to continue?')).toBeHidden();
});
test('Can delete objects with tags and neither return in search', async ({ page }) => {
await createNotebookEntryAndTags(page);
// Delete Notebook
@@ -162,12 +153,13 @@ test.describe('Tagging in Notebooks @addInit', () => {
await page.locator('button:has-text("OK")').click();
await page.goto('./', { waitUntil: 'networkidle' });
// Fill [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Unnamed');
await expect(page.locator('text=No results found')).toBeVisible();
await expect(page.locator('text=No matching results.')).toBeVisible();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sci');
await expect(page.locator('text=No results found')).toBeVisible();
await expect(page.locator('text=No matching results.')).toBeVisible();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('dri');
await expect(page.locator('text=No results found')).toBeVisible();
await expect(page.locator('text=No matching results.')).toBeVisible();
});
test('Tags persist across reload', async ({ page }) => {
//Go to baseURL

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -1,54 +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 testing the rendering and interaction of plots.
*
*/
const { test, expect } = require('../../../../baseFixtures');
const { createDomainObjectWithDefaults } = require('../../../../appActions');
test.describe('Plot Integrity Testing @unstable', () => {
let sineWaveGeneratorObject;
test.beforeEach(async ({ page }) => {
//Open a browser, navigate to the main page, and wait until all networkevents to resolve
await page.goto('./', { waitUntil: 'networkidle' });
sineWaveGeneratorObject = await createDomainObjectWithDefaults(page, { type: 'Sine Wave Generator' });
});
test('Plots do not re-request data when a plot is clicked', async ({ page }) => {
//Navigate to Sine Wave Generator
await page.goto(sineWaveGeneratorObject.url);
//Capture the number of plots points and store as const name numberOfPlotPoints
//Click on the plot canvas
await page.locator('canvas').nth(1).click();
//No request was made to get historical data
const createMineFolderRequests = [];
page.on('request', req => {
// eslint-disable-next-line playwright/no-conditional-in-test
createMineFolderRequests.push(req);
});
expect(createMineFolderRequests.length).toEqual(0);
});
});

View File

@@ -21,7 +21,7 @@
*****************************************************************************/
const { test, expect } = require('../../../../baseFixtures');
const { setFixedTimeMode, setRealTimeMode, setStartOffset, setEndOffset } = require('../../../../appActions');
const { setFixedTimeMode, setLocalClockMode, setStartOffset, setEndOffset } = require('../../../../appActions');
test.describe('Time conductor operations', () => {
test('validate start time does not exceeds end time', async ({ page }) => {
@@ -85,7 +85,7 @@ test.describe('Time conductor input fields real-time mode', () => {
await page.goto('./', { waitUntil: 'networkidle' });
// Switch to real-time mode
await setRealTimeMode(page);
await setLocalClockMode(page);
// Set start time offset
await setStartOffset(page, startOffset);
@@ -122,7 +122,7 @@ test.describe('Time conductor input fields real-time mode', () => {
await page.goto('./', { waitUntil: 'networkidle' });
// Switch to real-time mode
await setRealTimeMode(page);
await setLocalClockMode(page);
// Set start time offset
await setStartOffset(page, startOffset);
@@ -134,7 +134,7 @@ test.describe('Time conductor input fields real-time mode', () => {
await setFixedTimeMode(page);
// Switch back to real-time mode
await setRealTimeMode(page);
await setLocalClockMode(page);
// Verify updated start time offset persists after mode switch
await expect(page.locator('data-testid=conductor-start-offset-button')).toContainText('00:30:23');
@@ -168,23 +168,3 @@ test.describe('Time conductor input fields real-time mode', () => {
// select an option and verify the offsets are updated correctly
});
});
test.describe('Time Conductor History', () => {
test("shows milliseconds on hover @unstable", async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/4386'
});
// Navigate to Open MCT in Fixed Time Mode, UTC Time System
// with startBound at 2022-01-01 00:00:00.000Z
// and endBound at 2022-01-01 00:00:00.200Z
await page.goto('./#/browse/mine?view=grid&tc.mode=fixed&tc.startBound=1640995200000&tc.endBound=1640995200200&tc.timeSystem=utc&hideInspector=true', { waitUntil: 'networkidle' });
await page.locator("[aria-label='Time Conductor History']").hover({ trial: true});
await page.locator("[aria-label='Time Conductor History']").click();
// Validate history item format
const historyItem = page.locator('text="2022-01-01 00:00:00 + 200ms"');
await expect(historyItem).toBeEnabled();
await expect(historyItem).toHaveAttribute('title', '2022-01-01 00:00:00.000 - 2022-01-01 00:00:00.200');
});
});

View File

@@ -111,7 +111,7 @@ test.describe("Search Tests @unstable", () => {
expect(await searchResults.count()).toBe(0);
// Verify proper message appears
await expect(page.locator('text=No results found')).toBeVisible();
await expect(page.locator('text=No matching results.')).toBeVisible();
});
test('Validate single object in search result @couchdb', async ({ page }) => {
@@ -220,7 +220,7 @@ async function createObjectsForSearch(page, myItemsFolderName) {
]);
await page.locator('button:has-text("Create")').click();
await page.locator('li[title="A digital clock that uses system time and supports a variety of display formats and timezones."]').click();
await page.locator('li[title="A UTC-based clock that supports a variety of display formats. Clocks can be added to Display Layouts."]').click();
await Promise.all([
page.waitForNavigation(),
await page.locator('text=Properties Title Notes >> input[type="text"] >> nth=0').fill('Clock A'),
@@ -229,7 +229,7 @@ async function createObjectsForSearch(page, myItemsFolderName) {
]);
await page.locator('button:has-text("Create")').click();
await page.locator('li[title="A digital clock that uses system time and supports a variety of display formats and timezones."]').click();
await page.locator('li[title="A UTC-based clock that supports a variety of display formats. Clocks can be added to Display Layouts."]').click();
await Promise.all([
page.waitForNavigation(),
await page.locator('text=Properties Title Notes >> input[type="text"] >> nth=0').fill('Clock B'),
@@ -238,7 +238,7 @@ async function createObjectsForSearch(page, myItemsFolderName) {
]);
await page.locator('button:has-text("Create")').click();
await page.locator('li[title="A digital clock that uses system time and supports a variety of display formats and timezones."]').click();
await page.locator('li[title="A UTC-based clock that supports a variety of display formats. Clocks can be added to Display Layouts."]').click();
await Promise.all([
page.waitForNavigation(),
await page.locator('text=Properties Title Notes >> input[type="text"] >> nth=0').fill('Clock C'),
@@ -247,7 +247,7 @@ async function createObjectsForSearch(page, myItemsFolderName) {
]);
await page.locator('button:has-text("Create")').click();
await page.locator('li[title="A digital clock that uses system time and supports a variety of display formats and timezones."]').click();
await page.locator('li[title="A UTC-based clock that supports a variety of display formats. Clocks can be added to Display Layouts."]').click();
await Promise.all([
page.waitForNavigation(),
await page.locator('text=Properties Title Notes >> input[type="text"] >> nth=0').fill('Clock D'),

View File

@@ -22,7 +22,7 @@
/*
Collection of Visual Tests set to run in a default context. The tests within this suite
are only meant to run against openmct started by `npm start` within the
are only meant to run against openmct's app.js started by `npm run start` within the
`./e2e/playwright-visual.config.js` file.
*/

View File

@@ -22,7 +22,7 @@
/*
Collection of Visual Tests set to run in a default context. The tests within this suite
are only meant to run against openmct started by `npm start` within the
are only meant to run against openmct's app.js started by `npm run start` within the
`./e2e/playwright-visual.config.js` file.
These should only use functional expect statements to verify assumptions about the state

View File

@@ -1,51 +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.
*****************************************************************************/
const { test } = require('../../pluginFixtures');
const percySnapshot = require('@percy/playwright');
const { expandTreePaneItemByName, createDomainObjectWithDefaults } = require('../../appActions');
test.describe('Visual - Notebook', () => {
test('Accepts dropped objects as embeds @unstable', async ({ page, theme, openmctConfig }) => {
const { myItemsFolderName } = openmctConfig;
await page.goto('./#/browse/mine', { waitUntil: 'networkidle' });
// Create Notebook
const notebook = await createDomainObjectWithDefaults(page, {
type: 'Notebook',
name: "Embed Test Notebook"
});
// Create Overlay Plot
await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot',
name: "Dropped Overlay Plot"
});
await expandTreePaneItemByName(page, myItemsFolderName);
await page.goto(notebook.url);
await page.dragAndDrop('role=treeitem[name=/Dropped Overlay Plot/]', '.c-notebook__drag-area');
await percySnapshot(page, `Notebook w/ dropped embed (theme: ${theme})`);
});
});

View File

@@ -36,7 +36,7 @@ define([
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.",
description: "For development use. Generates test enumerated telemetry by cycling through a given set of states",
cssClass: "icon-generator-telemetry",
creatable: true,
form: [

12
jsdoc.json Normal file
View File

@@ -0,0 +1,12 @@
{
"source": {
"include": [
"src/"
],
"includePattern": "src/.+\\.js$",
"excludePattern": ".+\\Spec\\.js$|lib/.+"
},
"plugins": [
"plugins/markdown"
]
}

View File

@@ -23,32 +23,14 @@
/*global module,process*/
module.exports = (config) => {
let webpackConfig;
let browsers;
let singleRun;
if (process.env.KARMA_DEBUG) {
webpackConfig = require('./webpack.dev.js');
browsers = ['ChromeDebugging'];
singleRun = false;
} else {
webpackConfig = require('./webpack.coverage.js');
browsers = ['ChromeHeadless'];
singleRun = true;
}
const webpackConfig = require('./webpack.coverage.js');
delete webpackConfig.output;
// karma doesn't support webpack entry
delete webpackConfig.entry;
config.set({
basePath: '',
frameworks: ['jasmine', 'webpack'],
frameworks: ['jasmine'],
files: [
'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*',
included: false
@@ -64,7 +46,7 @@ module.exports = (config) => {
],
port: 9876,
reporters: ['spec', 'junit', 'coverage-istanbul'],
browsers,
browsers: [process.env.NODE_ENV === 'debug' ? 'ChromeDebugging' : 'ChromeHeadless'],
client: {
jasmine: {
random: false,
@@ -88,7 +70,6 @@ module.exports = (config) => {
},
coverageIstanbulReporter: {
fixWebpackSourcePaths: true,
skipFilesWithNoCoverage: true,
dir: "coverage/unit", //Sets coverage file to be consumed by codecov.io
reports: ['lcovonly']
},
@@ -109,7 +90,7 @@ module.exports = (config) => {
stats: 'errors-warnings'
},
concurrency: 1,
singleRun,
singleRun: true,
browserNoActivityTimeout: 400000
});
};

96
lighthouserc.yml Normal file
View File

@@ -0,0 +1,96 @@
---
ci:
collect:
urls:
- http://localhost/
numberOfRuns: 5
settings:
onlyCategories:
- performance
- best-practices
upload:
target: temporary-public-storage
assert:
preset: lighthouse:recommended
assertions:
### Applicable assertions
bootup-time:
- warn
- minScore: 0.88 #Original value was calculated at 0.88
dom-size:
- error
- maxNumericValue: 200 #Original value was calculated at 188
first-contentful-paint:
- error
- minScore: 0.07 #Original value was calculated at 0.08
mainthread-work-breakdown:
- warn
- minScore: 0.8 #Original value was calculated at 0.8
unused-javascript:
- warn
- maxLength: 1
- error
- maxNumericValue: 2000 #Original value was calculated at 1855
unused-css-rules: warn
installable-manifest: warn
service-worker: warn
### Disabled seo, accessibility, and pwa assertions, below
categories:seo: 'off'
categories:accessibility: 'off'
categories:pwa: 'off'
accesskeys: 'off'
apple-touch-icon: 'off'
aria-allowed-attr: 'off'
aria-command-name: 'off'
aria-hidden-body: 'off'
aria-hidden-focus: 'off'
aria-input-field-name: 'off'
aria-meter-name: 'off'
aria-progressbar-name: 'off'
aria-required-attr: 'off'
aria-required-children: 'off'
aria-required-parent: 'off'
aria-roles: 'off'
aria-toggle-field-name: 'off'
aria-tooltip-name: 'off'
aria-treeitem-name: 'off'
aria-valid-attr: 'off'
aria-valid-attr-value: 'off'
button-name: 'off'
bypass: 'off'
canonical: 'off'
color-contrast: 'off'
content-width: 'off'
crawlable-anchors: 'off'
csp-xss: 'off'
font-display: 'off'
font-size: 'off'
maskable-icon: 'off'
heading-order: 'off'
hreflang: 'off'
html-has-lang: 'off'
html-lang-valid: 'off'
http-status-code: 'off'
image-alt: 'off'
input-image-alt: 'off'
is-crawlable: 'off'
label: 'off'
link-name: 'off'
link-text: 'off'
list: 'off'
listitem: 'off'
meta-description: 'off'
meta-refresh: 'off'
meta-viewport: 'off'
object-alt: 'off'
plugins: 'off'
robots-txt: 'off'
splash-screen: 'off'
tabindex: 'off'
tap-targets: 'off'
td-headers-attr: 'off'
th-has-data-cells: 'off'
themed-omnibox: 'off'
valid-lang: 'off'
video-caption: 'off'
viewport: 'off'

View File

@@ -1,30 +1,35 @@
{
"name": "openmct",
"version": "2.1.2",
"version": "2.1.0-SNAPSHOT",
"description": "The Open MCT core platform",
"devDependencies": {
"@babel/eslint-parser": "7.18.9",
"@braintree/sanitize-url": "6.0.0",
"@percy/cli": "1.11.0",
"@percy/cli": "1.7.2",
"@percy/playwright": "1.0.4",
"@playwright/test": "1.25.2",
"@types/jasmine": "4.0.1",
"@types/lodash": "4.14.178",
"@types/eventemitter3": "^1.0.0",
"@types/jasmine": "^4.0.1",
"@types/karma": "^6.3.2",
"@types/lodash": "^4.14.178",
"@types/mocha": "^9.1.0",
"babel-loader": "8.2.5",
"babel-plugin-istanbul": "6.1.1",
"codecov": "3.8.3",
"comma-separated-values": "3.6.4",
"codecov":"3.8.3",
"copy-webpack-plugin": "11.0.0",
"cross-env": "7.0.3",
"css-loader": "6.7.1",
"d3-axis": "3.0.0",
"d3-scale": "3.3.0",
"d3-selection": "3.0.0",
"eslint": "8.25.0",
"eslint": "8.23.1",
"eslint-plugin-compat": "4.0.2",
"eslint-plugin-playwright": "0.11.2",
"eslint-plugin-vue": "9.6.0",
"eslint-plugin-playwright": "0.11.1",
"eslint-plugin-vue": "9.3.0",
"eslint-plugin-you-dont-need-lodash-underscore": "6.12.0",
"eventemitter3": "1.2.0",
"express": "4.13.1",
"file-saver": "2.0.5",
"git-rev-sync": "3.0.2",
"html2canvas": "1.4.1",
@@ -46,14 +51,14 @@
"moment": "2.29.4",
"moment-duration-format": "2.3.2",
"moment-timezone": "0.5.37",
"nyc": "15.1.0",
"nyc":"15.1.0",
"painterro": "1.2.78",
"playwright-core": "1.25.2",
"plotly.js-basic-dist": "2.14.0",
"plotly.js-gl2d-dist": "2.14.0",
"printj": "1.3.1",
"request": "2.88.2",
"resolve-url-loader": "5.0.0",
"sass": "1.55.0",
"sass": "1.54.9",
"sass-loader": "13.0.2",
"sinon": "14.0.0",
"style-loader": "^3.3.1",
@@ -64,23 +69,23 @@
"vue-template-compiler": "2.6.14",
"webpack": "5.74.0",
"webpack-cli": "4.10.0",
"webpack-dev-server": "^4.11.1",
"webpack-dev-middleware": "5.3.3",
"webpack-hot-middleware": "2.25.2",
"webpack-merge": "5.8.0"
},
"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.dev.js",
"start:coverage": "npx webpack serve --config ./webpack.coverage.js",
"start": "node app.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.prod.js",
"build:prod": "cross-env 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",
"test": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run",
"test:debug": "cross-env NODE_ENV=debug karma start --no-single-run",
"test:e2e": "npx playwright test",
"test:e2e:couchdb": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @couchdb",
"test:e2e:stable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep-invert \"@unstable|@couchdb\"",
@@ -90,12 +95,13 @@
"test:e2e:visual": "percy exec --config ./e2e/.percy.yml -- npx playwright test --config=e2e/playwright-visual.config.js --grep-invert @unstable",
"test:e2e:full": "npx playwright test --config=e2e/playwright-ci.config.js --grep-invert @couchdb",
"test:perf": "npx playwright test --config=e2e/playwright-performance.config.js",
"test:watch": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --no-single-run",
"update-about-dialog-copyright": "perl -pi -e 's/20\\d\\d\\-202\\d/2014\\-2022/gm' ./src/ui/layout/AboutDialog.vue",
"update-copyright-date": "npm run update-about-dialog-copyright && grep -lr --null --include=*.{js,scss,vue,ts,sh,html,md,frag} 'Copyright (c) 20' . | xargs -r0 perl -pi -e 's/Copyright\\s\\(c\\)\\s20\\d\\d\\-20\\d\\d/Copyright \\(c\\)\\ 2014\\-2022/gm'",
"cov:e2e:report": "nyc report --reporter=lcovonly --report-dir=./coverage/e2e",
"cov:e2e:full:publish": "codecov --disable=gcov -f ./coverage/e2e/lcov.info -F e2e-full",
"cov:e2e:stable:publish": "codecov --disable=gcov -f ./coverage/e2e/lcov.info -F e2e-stable",
"cov:unit:publish": "codecov --disable=gcov -f ./coverage/unit/lcov.info -F unit",
"cov:e2e:report":"nyc report --reporter=lcovonly --report-dir=./coverage/e2e",
"cov:e2e:full:publish":"codecov --disable=gcov -f ./coverage/e2e/lcov.info -F e2e-full",
"cov:e2e:stable:publish":"codecov --disable=gcov -f ./coverage/e2e/lcov.info -F e2e-stable",
"cov:unit:publish":"codecov --disable=gcov -f ./coverage/unit/lcov.info -F unit",
"prepare": "npm run build:prod"
},
"repository": {
@@ -105,6 +111,9 @@
"engines": {
"node": ">=14.19.1"
},
"overrides": {
"core-js": "3.21.1"
},
"browserslist": [
"Firefox ESR",
"not IE 11",

View File

@@ -63,9 +63,10 @@ export default class Editor extends EventEmitter {
.then(() => {
this.editing = false;
this.emit('isEditing', false);
this.openmct.objects.endTransaction();
}).catch(error => {
throw error;
}).finally(() => {
this.openmct.objects.endTransaction();
});
}

View File

@@ -1,80 +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 {
createOpenMct, resetApplicationState
} from '../utils/testing';
describe('The Editor API', () => {
let openmct;
beforeEach((done) => {
openmct = createOpenMct();
openmct.on('start', done);
spyOn(openmct.objects, 'endTransaction');
openmct.startHeadless();
});
afterEach(() => {
return resetApplicationState(openmct);
});
it('opens a transaction on edit', () => {
expect(
openmct.objects.isTransactionActive()
).toBeFalse();
openmct.editor.edit();
expect(
openmct.objects.isTransactionActive()
).toBeTrue();
});
it('closes an open transaction on successful save', async () => {
spyOn(openmct.objects, 'getActiveTransaction')
.and.returnValue({
commit: () => Promise.resolve(true)
});
openmct.editor.edit();
await openmct.editor.save();
expect(
openmct.objects.endTransaction
).toHaveBeenCalled();
});
it('does not close an open transaction on failed save', async () => {
spyOn(openmct.objects, 'getActiveTransaction')
.and.returnValue({
commit: () => Promise.reject()
});
openmct.editor.edit();
await openmct.editor.save().catch(() => {});
expect(
openmct.objects.endTransaction
).not.toHaveBeenCalled();
});
});

View File

@@ -22,7 +22,6 @@
import { v4 as uuid } from 'uuid';
import EventEmitter from 'EventEmitter';
import _ from 'lodash';
/**
* @readonly
@@ -43,28 +42,19 @@ const ANNOTATION_TYPES = Object.freeze({
const ANNOTATION_TYPE = 'annotation';
const ANNOTATION_LAST_CREATED = 'annotationLastCreated';
/**
* @typedef {Object} Tag
* @property {String} key a unique identifier for the tag
* @property {String} backgroundColor eg. "#cc0000"
* @property {String} foregroundColor eg. "#ffffff"
*/
export default class AnnotationAPI extends EventEmitter {
/**
* @param {OpenMCT} openmct
*/
constructor(openmct) {
super();
this.openmct = openmct;
this.availableTags = {};
this.ANNOTATION_TYPES = ANNOTATION_TYPES;
this.ANNOTATION_TYPE = ANNOTATION_TYPE;
this.ANNOTATION_LAST_CREATED = ANNOTATION_LAST_CREATED;
this.openmct.types.addType(ANNOTATION_TYPE, {
name: 'Annotation',
@@ -73,7 +63,6 @@ export default class AnnotationAPI extends EventEmitter {
cssClass: 'icon-notebook',
initialize: function (domainObject) {
domainObject.targets = domainObject.targets || {};
domainObject._deleted = domainObject._deleted || false;
domainObject.originalContextPath = domainObject.originalContextPath || '';
domainObject.tags = domainObject.tags || [];
domainObject.contentText = domainObject.contentText || '';
@@ -123,7 +112,6 @@ export default class AnnotationAPI extends EventEmitter {
namespace
},
tags,
_deleted: false,
annotationType,
contentText,
originalContextPath
@@ -139,7 +127,6 @@ export default class AnnotationAPI extends EventEmitter {
const success = await this.openmct.objects.save(createdObject);
if (success) {
this.emit('annotationCreated', createdObject);
this.#updateAnnotationModified(domainObject);
return createdObject;
} else {
@@ -147,32 +134,14 @@ export default class AnnotationAPI extends EventEmitter {
}
}
#updateAnnotationModified(domainObject) {
this.openmct.objects.mutate(domainObject, this.ANNOTATION_LAST_CREATED, Date.now());
}
/**
* @method defineTag
* @param {String} key a unique identifier for the tag
* @param {Tag} tagsDefinition the definition of the tag to add
*/
defineTag(tagKey, tagsDefinition) {
this.availableTags[tagKey] = tagsDefinition;
}
/**
* @method isAnnotation
* @param {import('../objects/ObjectAPI').DomainObject} domainObject domainObject the domainObject in question
* @returns {Boolean} Returns true if the domain object is an annotation
*/
isAnnotation(domainObject) {
return domainObject && (domainObject.type === ANNOTATION_TYPE);
}
/**
* @method getAvailableTags
* @returns {Tag[]} Returns an array of the available tags that have been loaded
*/
getAvailableTags() {
if (this.availableTags) {
const rearrangedToArray = Object.keys(this.availableTags).map(tagKey => {
@@ -188,26 +157,18 @@ export default class AnnotationAPI extends EventEmitter {
}
}
/**
* @method getAnnotations
* @param {String} query - The keystring of the domain object to search for annotations for
* @returns {import('../objects/ObjectAPI').DomainObject[]} Returns an array of domain objects that match the search query
*/
async getAnnotations(query) {
const searchResults = (await Promise.all(this.openmct.objects.search(query, null, this.openmct.objects.SEARCH_TYPES.ANNOTATIONS))).flat();
async getAnnotation(query, searchType) {
let foundAnnotation = null;
return searchResults;
const searchResults = (await Promise.all(this.openmct.objects.search(query, null, searchType))).flat();
if (searchResults) {
foundAnnotation = searchResults[0];
}
return foundAnnotation;
}
/**
* @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) {
async addAnnotationTag(existingAnnotation, targetDomainObject, targetSpecificDetails, annotationType, tag) {
if (!existingAnnotation) {
const targets = {};
const targetKeyString = this.openmct.objects.makeKeyString(targetDomainObject.identifier);
@@ -225,44 +186,27 @@ export default class AnnotationAPI extends EventEmitter {
return newAnnotation;
} else {
if (!existingAnnotation.tags.includes(tag)) {
throw new Error(`Existing annotation did not contain tag ${tag}`);
}
if (existingAnnotation._deleted) {
this.unDeleteAnnotation(existingAnnotation);
}
const tagArray = [tag, ...existingAnnotation.tags];
this.openmct.objects.mutate(existingAnnotation, 'tags', tagArray);
return existingAnnotation;
}
}
/**
* @method deleteAnnotations
* @param {import('../objects/ObjectAPI').DomainObject[]} existingAnnotation - An array of annotations to delete (set _deleted to true)
*/
deleteAnnotations(annotations) {
if (!annotations) {
throw new Error('Asked to delete null annotations! 🙅‍♂️');
removeAnnotationTag(existingAnnotation, tagToRemove) {
if (existingAnnotation && existingAnnotation.tags.includes(tagToRemove)) {
const cleanedArray = existingAnnotation.tags.filter(extantTag => extantTag !== tagToRemove);
this.openmct.objects.mutate(existingAnnotation, 'tags', cleanedArray);
} else {
throw new Error(`Asked to remove tag (${tagToRemove}) that doesn't exist`, existingAnnotation);
}
annotations.forEach(annotation => {
if (!annotation._deleted) {
this.openmct.objects.mutate(annotation, '_deleted', true);
}
});
}
/**
* @method deleteAnnotations
* @param {import('../objects/ObjectAPI').DomainObject} existingAnnotation - An annotations to undelete (set _deleted to false)
*/
unDeleteAnnotation(annotation) {
if (!annotation) {
throw new Error('Asked to undelete null annotation! 🙅‍♂️');
removeAnnotationTags(existingAnnotation) {
// just removes tags on the annotation as we can't really delete objects
if (existingAnnotation && existingAnnotation.tags) {
this.openmct.objects.mutate(existingAnnotation, 'tags', []);
}
this.openmct.objects.mutate(annotation, '_deleted', false);
}
#getMatchingTags(query) {
@@ -322,40 +266,16 @@ export default class AnnotationAPI extends EventEmitter {
return modelAddedToResults;
}
#combineSameTargets(results) {
const combinedResults = [];
results.forEach(currentAnnotation => {
const existingAnnotation = combinedResults.find((annotationToFind) => {
return _.isEqual(currentAnnotation.targets, annotationToFind.targets);
});
if (!existingAnnotation) {
combinedResults.push(currentAnnotation);
} else {
existingAnnotation.tags.push(...currentAnnotation.tags);
}
});
return combinedResults;
}
/**
* @method searchForTags
* @param {String} query A query to match against tags. E.g., "dr" will match the tags "drilling" and "driving"
* @param {Object} [abortController] An optional abort method to stop the query
* @param {Object} abortController An optional abort method to stop the query
* @returns {Promise} returns a model of matching tags with their target domain objects attached
*/
async searchForTags(query, abortController) {
const matchingTagKeys = this.#getMatchingTags(query);
if (!matchingTagKeys.length) {
return [];
}
const searchResults = (await Promise.all(this.openmct.objects.search(matchingTagKeys, abortController, this.openmct.objects.SEARCH_TYPES.TAGS))).flat();
const filteredDeletedResults = searchResults.filter((result) => {
return !(result._deleted);
});
const combinedSameTargets = this.#combineSameTargets(filteredDeletedResults);
const appliedTagSearchResults = this.#addTagMetaInformationToResults(combinedSameTargets, matchingTagKeys);
const appliedTagSearchResults = this.#addTagMetaInformationToResults(searchResults, matchingTagKeys);
const appliedTargetsModels = await this.#addTargetModelsToResults(appliedTagSearchResults);
const resultsWithValidPath = appliedTargetsModels.filter(result => {
return this.openmct.objects.isReachable(result.targetModels?.[0]?.originalPath);

View File

@@ -126,44 +126,34 @@ describe("The Annotation API", () => {
describe("Tagging", () => {
it("can create a tag", async () => {
const annotationObject = await openmct.annotation.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
const annotationObject = await openmct.annotation.addAnnotationTag(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.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
const originalAnnotationObject = await openmct.annotation.addAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
const annotationObject = await openmct.annotation.addAnnotationTag(originalAnnotationObject, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'anotherTagToRemove');
expect(annotationObject).toBeDefined();
openmct.annotation.deleteAnnotations([annotationObject]);
expect(annotationObject._deleted).toBeTrue();
openmct.annotation.removeAnnotationTag(annotationObject, 'anotherTagToRemove');
expect(annotationObject.tags).toEqual(['aWonderfulTag']);
openmct.annotation.removeAnnotationTag(annotationObject, 'aWonderfulTag');
expect(annotationObject.tags).toEqual([]);
});
it("throws an error if deleting non-existent tag", async () => {
const annotationObject = await openmct.annotation.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
const annotationObject = await openmct.annotation.addAnnotationTag(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.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
const annotationObject = await openmct.annotation.addAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
expect(annotationObject).toBeDefined();
expect(() => {
openmct.annotation.deleteAnnotations([annotationObject]);
openmct.annotation.removeAnnotationTags(annotationObject);
}).not.toThrow();
expect(annotationObject._deleted).toBeTrue();
});
it("can add/delete/add a tag", async () => {
let annotationObject = await openmct.annotation.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
expect(annotationObject).toBeDefined();
expect(annotationObject.type).toEqual('annotation');
expect(annotationObject.tags).toContain('aWonderfulTag');
openmct.annotation.deleteAnnotations([annotationObject]);
expect(annotationObject._deleted).toBeTrue();
annotationObject = await openmct.annotation.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
expect(annotationObject).toBeDefined();
expect(annotationObject.type).toEqual('annotation');
expect(annotationObject.tags).toContain('aWonderfulTag');
expect(annotationObject._deleted).toBeFalse();
expect(annotationObject.tags).toEqual([]);
});
});
@@ -185,10 +175,16 @@ describe("The Annotation API", () => {
expect(results).toBeDefined();
expect(results.length).toEqual(1);
});
it("returns no tags for empty search", async () => {
const results = await openmct.annotation.searchForTags('q');
it("can get notebook annotations", async () => {
const targetKeyString = openmct.objects.makeKeyString(mockDomainObject.identifier);
const query = {
targetKeyString,
entryId: 'fooBarEntry'
};
const results = await openmct.annotation.getAnnotation(query, openmct.objects.SEARCH_TYPES.NOTEBOOK_ANNOTATIONS);
expect(results).toBeDefined();
expect(results.length).toEqual(0);
expect(results.tags.length).toEqual(2);
});
});
});

View File

@@ -199,7 +199,7 @@ define([
if (this.returnMutables && this.publicAPI.objects.supportsMutation(child.identifier)) {
let keyString = this.publicAPI.objects.makeKeyString(child.identifier);
child = this.publicAPI.objects.toMutable(child);
child = this.publicAPI.objects._toMutable(child);
this.mutables[keyString] = child;
}

View File

@@ -139,7 +139,7 @@ export default class FormsAPI extends EventEmitter {
} else {
overlay = self.openmct.overlays.overlay({
element: vm.$el,
size: 'dialog',
size: 'small',
onDestroy: () => vm.$destroy()
});
}

View File

@@ -32,49 +32,53 @@
prevent
class="u-contents"
>
<input
v-model="date"
class="field control date"
:pattern="/\d{4}-\d{2}-\d{2}/"
:placeholder="format"
type="date"
name="date"
@change="onChange"
>
<input
v-model="hour"
class="field control hour c-input--sm"
:pattern="/\d+/"
type="number"
name="hour"
maxlength="10"
min="0"
max="23"
@change="onChange"
>
<input
v-model="min"
class="field control min c-input--sm"
:pattern="/\d+/"
type="number"
name="min"
maxlength="2"
min="0"
max="59"
@change="onChange"
>
<input
v-model="sec"
class="field control sec c-input--sm"
:pattern="/\d+/"
type="number"
name="sec"
maxlength="2"
min="0"
max="59"
@change="onChange"
>
<div class="field control hint timezone">
<div class="field control date">
<input
v-model="date"
:pattern="/\d{4}-\d{2}-\d{2}/"
:placeholder="format"
type="date"
name="date"
@change="onChange"
>
</div>
<div class="field control hour sm">
<input
v-model="hour"
:pattern="/\d+/"
type="number"
name="hour"
maxlength="10"
min="0"
max="23"
@change="onChange"
>
</div>
<div class="field control min sm">
<input
v-model="min"
:pattern="/\d+/"
type="number"
name="min"
maxlength="2"
min="0"
max="59"
@change="onChange"
>
</div>
<div class="field control sec sm">
<input
v-model="sec"
:pattern="/\d+/"
type="number"
name="sec"
maxlength="2"
min="0"
max="59"
@change="onChange"
>
</div>
<div class="field control timezone">
UTC
</div>
</form>

View File

@@ -42,6 +42,7 @@ class InMemorySearchProvider {
this.openmct = openmct;
this.indexedIds = {};
this.indexedCompositions = {};
this.indexedTags = {};
this.idsToIndex = [];
this.pendingIndex = {};
this.pendingRequests = 0;
@@ -60,6 +61,7 @@ class InMemorySearchProvider {
this.localSearchForObjects = this.localSearchForObjects.bind(this);
this.localSearchForAnnotations = this.localSearchForAnnotations.bind(this);
this.localSearchForTags = this.localSearchForTags.bind(this);
this.localSearchForNotebookAnnotations = this.localSearchForNotebookAnnotations.bind(this);
this.onAnnotationCreation = this.onAnnotationCreation.bind(this);
this.onCompositionAdded = this.onCompositionAdded.bind(this);
this.onCompositionRemoved = this.onCompositionRemoved.bind(this);
@@ -91,7 +93,7 @@ class InMemorySearchProvider {
this.searchTypes = this.openmct.objects.SEARCH_TYPES;
this.supportedSearchTypes = [this.searchTypes.OBJECTS, this.searchTypes.ANNOTATIONS, this.searchTypes.TAGS];
this.supportedSearchTypes = [this.searchTypes.OBJECTS, this.searchTypes.ANNOTATIONS, this.searchTypes.NOTEBOOK_ANNOTATIONS, this.searchTypes.TAGS];
this.scheduleForIndexing(rootObject.identifier);
@@ -161,6 +163,8 @@ class InMemorySearchProvider {
return this.localSearchForObjects(queryId, query, maxResults);
} else if (searchType === this.searchTypes.ANNOTATIONS) {
return this.localSearchForAnnotations(queryId, query, maxResults);
} else if (searchType === this.searchTypes.NOTEBOOK_ANNOTATIONS) {
return this.localSearchForNotebookAnnotations(queryId, query, maxResults);
} else if (searchType === this.searchTypes.TAGS) {
return this.localSearchForTags(queryId, query, maxResults);
} else {
@@ -277,6 +281,13 @@ class InMemorySearchProvider {
provider.index(domainObject);
}
onTagMutation(domainObject, newTags) {
domainObject.tags = newTags;
const provider = this;
provider.index(domainObject);
}
onCompositionAdded(newDomainObjectToIndex) {
const provider = this;
// The object comes in as a mutable domain object, which has functions,
@@ -331,6 +342,14 @@ class InMemorySearchProvider {
composition.on('remove', this.onCompositionRemoved);
this.indexedCompositions[keyString] = composition;
}
if (domainObject.type === 'annotation') {
this.indexedTags[keyString] = this.openmct.objects.observe(
domainObject,
'tags',
this.onTagMutation.bind(this, domainObject)
);
}
}
if ((keyString !== 'ROOT')) {
@@ -562,6 +581,43 @@ class InMemorySearchProvider {
this.onWorkerMessage(eventToReturn);
}
/**
* A local version of the same SharedWorker function
* if we don't have SharedWorkers available (e.g., iOS)
*/
localSearchForNotebookAnnotations(queryId, {entryId, targetKeyString}, maxResults) {
// This results dictionary will have domain object ID keys which
// point to the value the domain object's score.
let results = [];
const message = {
request: 'searchForNotebookAnnotations',
results: [],
total: 0,
queryId
};
const matchingAnnotations = this.localIndexedAnnotationsByDomainObject[targetKeyString];
if (matchingAnnotations) {
results = matchingAnnotations.filter(matchingAnnotation => {
if (!matchingAnnotation.targets) {
return false;
}
const target = matchingAnnotation.targets[targetKeyString];
return (target && target.entryId && (target.entryId === entryId));
});
}
message.total = results.length;
message.results = results
.slice(0, maxResults);
const eventToReturn = {
data: message
};
this.onWorkerMessage(eventToReturn);
}
destroyObservers(observers) {
Object.entries(observers).forEach(([keyString, unobserve]) => {
if (typeof unobserve === 'function') {

View File

@@ -43,6 +43,8 @@
port.postMessage(searchForAnnotations(event.data));
} else if (requestType === 'TAGS') {
port.postMessage(searchForTags(event.data));
} else if (requestType === 'NOTEBOOK_ANNOTATIONS') {
port.postMessage(searchForNotebookAnnotations(event.data));
} else {
throw new Error(`Unknown request ${event.data.request}`);
}
@@ -202,4 +204,33 @@
return message;
}
function searchForNotebookAnnotations(data) {
let results = [];
const message = {
request: 'searchForNotebookAnnotations',
results: {},
total: 0,
queryId: data.queryId
};
const matchingAnnotations = indexedAnnotationsByDomainObject[data.input.targetKeyString];
if (matchingAnnotations) {
results = matchingAnnotations.filter(matchingAnnotation => {
if (!matchingAnnotation.targets) {
return false;
}
const target = matchingAnnotation.targets[data.input.targetKeyString];
return (target && target.entryId && (target.entryId === data.input.entryId));
});
}
message.total = results.length;
message.results = results
.slice(0, data.maxResults);
return message;
}
}());

View File

@@ -64,15 +64,6 @@ import InMemorySearchProvider from './InMemorySearchProvider';
* to load domain objects
* @memberof module:openmct
*/
/**
* @readonly
* @enum {String} SEARCH_TYPES
* @property {String} OBJECTS Search for objects
* @property {String} ANNOTATIONS Search for annotations
* @property {String} TAGS Search for tags
*/
/**
* Utilities for loading, saving, and manipulating domain objects.
* @interface ObjectAPI
@@ -85,6 +76,7 @@ export default class ObjectAPI {
this.SEARCH_TYPES = Object.freeze({
OBJECTS: 'OBJECTS',
ANNOTATIONS: 'ANNOTATIONS',
NOTEBOOK_ANNOTATIONS: 'NOTEBOOK_ANNOTATIONS',
TAGS: 'TAGS'
});
this.eventEmitter = new EventEmitter();
@@ -196,6 +188,7 @@ export default class ObjectAPI {
* @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) {
let keystring = this.makeKeyString(identifier);
@@ -230,7 +223,7 @@ export default class ObjectAPI {
if (result.isMutable) {
result.$refresh(result);
} else {
let mutableDomainObject = this.toMutable(result);
let mutableDomainObject = this._toMutable(result);
mutableDomainObject.$refresh(result);
}
@@ -307,7 +300,7 @@ export default class ObjectAPI {
}
return this.get(identifier).then((object) => {
return this.toMutable(object);
return this._toMutable(object);
});
}
@@ -497,7 +490,7 @@ export default class ObjectAPI {
} else {
//Creating a temporary mutable domain object allows other mutable instances of the
//object to be kept in sync.
let mutableDomainObject = this.toMutable(domainObject);
let mutableDomainObject = this._toMutable(domainObject);
//Mutate original object
MutableDomainObject.mutateObject(domainObject, path, value);
@@ -517,19 +510,15 @@ export default class ObjectAPI {
}
/**
* Create a mutable domain object from an existing domain object
* @param {module:openmct.DomainObject} domainObject the object to make mutable
* @returns {MutableDomainObject} a mutable domain object that will automatically sync
* @method toMutable
* @memberof module:openmct.ObjectAPI#
* @private
*/
toMutable(domainObject) {
_toMutable(object) {
let mutableObject;
if (domainObject.isMutable) {
mutableObject = domainObject;
if (object.isMutable) {
mutableObject = object;
} else {
mutableObject = MutableDomainObject.createMutable(domainObject, this.eventEmitter);
mutableObject = MutableDomainObject.createMutable(object, this.eventEmitter);
// Check if provider supports realtime updates
let identifier = utils.parseKeyString(mutableObject.identifier);
@@ -537,11 +526,9 @@ export default class ObjectAPI {
if (provider !== undefined
&& provider.observe !== undefined
&& this.SYNCHRONIZED_OBJECT_TYPES.includes(domainObject.type)) {
&& this.SYNCHRONIZED_OBJECT_TYPES.includes(object.type)) {
let unobserve = provider.observe(identifier, (updatedModel) => {
// modified can sometimes be undefined, so make it 0 in this case
const mutableObjectModification = mutableObject.modified ?? Number.MIN_SAFE_INTEGER;
if (updatedModel.persisted > mutableObjectModification) {
if (updatedModel.persisted > mutableObject.modified) {
//Don't replace with a stale model. This can happen on slow connections when multiple mutations happen
//in rapid succession and intermediate persistence states are returned by the observe function.
updatedModel = this.applyGetInterceptors(identifier, updatedModel);
@@ -595,7 +582,7 @@ export default class ObjectAPI {
if (domainObject.isMutable) {
return domainObject.$observe(path, callback);
} else {
let mutable = this.toMutable(domainObject);
let mutable = this._toMutable(domainObject);
mutable.$observe(path, callback);
return () => mutable.$destroy();
@@ -688,10 +675,8 @@ export default class ObjectAPI {
}
#hasAlreadyBeenPersisted(domainObject) {
// modified can sometimes be undefined, so make it 0 in this case
const modified = domainObject.modified ?? Number.MIN_SAFE_INTEGER;
const result = domainObject.persisted !== undefined
&& domainObject.persisted >= modified;
&& domainObject.persisted >= domainObject.modified;
return result;
}

View File

@@ -320,7 +320,7 @@ describe("The Object API", () => {
beforeEach(function () {
// Duplicate object to guarantee we are not sharing object instance, which would invalidate test
testObjectDuplicate = JSON.parse(JSON.stringify(testObject));
mutableSecondInstance = objectAPI.toMutable(testObjectDuplicate);
mutableSecondInstance = objectAPI._toMutable(testObjectDuplicate);
});
afterEach(() => {

View File

@@ -6,8 +6,7 @@ const cssClasses = {
large: 'l-overlay-large',
small: 'l-overlay-small',
fit: 'l-overlay-fit',
fullscreen: 'l-overlay-fullscreen',
dialog: 'l-overlay-dialog'
fullscreen: 'l-overlay-fullscreen'
};
class Overlay extends EventEmitter {

View File

@@ -1,4 +1,4 @@
@mixin overlaySizing($marginTB: auto, $marginLR: auto, $width: auto, $height: auto) {
@mixin overlaySizing($marginTB: 5%, $marginLR: $marginTB, $width: auto, $height: auto) {
position: absolute;
top: $marginTB; right: $marginLR; bottom: $marginTB; left: $marginLR;
width: $width;
@@ -98,7 +98,6 @@ body.desktop {
// Overlay types, styling for desktop. Appended to .l-overlay-wrapper element.
.l-overlay-large,
.l-overlay-small,
.l-overlay-dialog,
.l-overlay-fit {
.c-overlay__outer {
border-radius: $overlayCr;
@@ -109,7 +108,7 @@ body.desktop {
.l-overlay-fullscreen {
// Used by About > Licenses display
.c-overlay__outer {
@include overlaySizing(nth($overlayOuterMarginFullscreen, 1), nth($overlayOuterMarginFullscreen, 2));
@include overlaySizing($overlayOuterMarginFullscreen);
}
}
@@ -120,7 +119,7 @@ body.desktop {
$lrPad: $pad;
.c-overlay {
&__outer {
@include overlaySizing(nth($overlayOuterMarginLarge, 1), nth($overlayOuterMarginLarge, 2));
@include overlaySizing($overlayOuterMarginLarge);
padding: $tbPad $lrPad;
}
@@ -138,20 +137,14 @@ body.desktop {
.l-overlay-small {
.c-overlay__outer {
@include overlaySizing(nth($overlayOuterMarginSmall, 1), nth($overlayOuterMarginSmall, 2));
}
}
.l-overlay-dialog {
.c-overlay__outer {
@include overlaySizing(nth($overlayOuterMarginDialog, 1), nth($overlayOuterMarginDialog, 2));
@include overlaySizing($overlayOuterMarginDialog);
}
}
.t-dialog-sm .l-overlay-small, // Legacy dialog support
.l-overlay-fit {
.c-overlay__outer {
@include overlaySizing(auto, auto);
@include overlaySizing(auto);
top: 50%;
left: 50%;
transform: translate(-50%, -50%);

View File

@@ -32,7 +32,7 @@ export default function plugin() {
openmct.types.addType('LadTable', {
name: "LAD Table",
creatable: true,
description: "Display the current value for one or more telemetry end points in a fixed table. Each row is a telemetry end point.",
description: "A Latest Available Data tabular view in which each row displays the values for one or more contained telemetry objects.",
cssClass: 'icon-tabular-lad',
initialize(domainObject) {
domainObject.composition = [];
@@ -42,7 +42,7 @@ export default function plugin() {
openmct.types.addType('LadTableSet', {
name: "LAD Table Set",
creatable: true,
description: "Group LAD Tables together into a single view with sub-headers.",
description: "A Latest Available Data tabular view in which each row displays the values for one or more contained telemetry objects.",
cssClass: 'icon-tabular-lad-set',
initialize(domainObject) {
domainObject.composition = [];

View File

@@ -32,7 +32,7 @@ export default function ClockPlugin(options) {
const CLOCK_INDICATOR_FORMAT = 'YYYY/MM/DD HH:mm:ss';
openmct.types.addType('clock', {
name: 'Clock',
description: 'A digital clock that uses system time and supports a variety of display formats and timezones.',
description: 'A UTC-based clock that supports a variety of display formats. Clocks can be added to Display Layouts.',
creatable: true,
cssClass: 'icon-clock',
initialize: function (domainObject) {

View File

@@ -93,7 +93,7 @@ define(['lodash'], function (_) {
'table': {
value: 'table',
name: 'Table',
class: 'icon-tabular-scrolling'
class: 'icon-tabular-realtime'
}
};
const APPLICABLE_VIEWS = {

View File

@@ -1,34 +0,0 @@
const displayLayoutDrawingObjectTypes = {
'box-view': {
name: "Box",
creatable: false,
description: 'A rectangle shape.',
cssClass: 'icon-box-round-corners'
},
'ellipse-view': {
name: "Ellipse",
creatable: false,
description: 'A ellipse shape.',
cssClass: 'icon-circle'
},
'line-view': {
name: "Line",
creatable: false,
description: 'A line.',
cssClass: 'icon-line-horz'
},
'text-view': {
name: "Text",
creatable: false,
description: 'An editable text box.',
cssClass: 'icon-font'
},
'image-view': {
name: "Image",
creatable: false,
description: 'An image.',
cssClass: 'icon-image'
}
};
export default displayLayoutDrawingObjectTypes;

View File

@@ -147,7 +147,7 @@ export default {
this.mutablePromise.then(() => {
this.openmct.objects.destroyMutable(this.domainObject);
});
} else if (this?.domainObject?.isMutable) {
} else if (this.domainObject.isMutable) {
this.openmct.objects.destroyMutable(this.domainObject);
}
},

View File

@@ -243,7 +243,7 @@ export default {
this.mutablePromise.then(() => {
this.openmct.objects.destroyMutable(this.domainObject);
});
} else if (this?.domainObject?.isMutable) {
} else if (this.domainObject.isMutable) {
this.openmct.objects.destroyMutable(this.domainObject);
}
},

View File

@@ -1,3 +1,16 @@
@mixin displayMarquee($c) {
> .c-frame-edit {
// All other frames
//@include test($c, 0.4);
display: block;
}
> .c-frame > .c-frame-edit {
// Line object frame
//@include test($c, 0.4);
display: block;
}
}
.l-layout {
@include abs();
display: flex;
@@ -70,36 +83,14 @@
}
/*********************** EDIT MARQUEE CONTROL */
*[s-selected] {
// The selected frame should always be a sibling of .c-frame-edit
// except for the Display Layout line object
> .c-frame-edit,
~ .c-frame-edit {
display: block;
*[s-selected-parent] {
> .l-layout {
// When main shell layout is the parent
@include displayMarquee(deeppink);
}
&.l-layout__frame {
> *:not(.c-so-view--layout) .c-so-view__object-view {
pointer-events: none;
}
}
// When select context is main view layout
&.is-object-type-layout,
&.is-object-type-tabs {
.l-layout,
.c-so-view--layout {
.c-object-view:not(.is-object-type-layout) {
pointer-events: none;
}
}
}
}
// When editing, turn off pointer events for all sub-objects other than layouts
.c-so-view--layout {
.c-object-view:not(.is-object-type-layout) {
pointer-events: none;
> * > * > * {
// When a sub-layout is the parent
@include displayMarquee(blue);
}
}
}

View File

@@ -69,20 +69,19 @@
transition-delay: $moveBarOutDelay;
}
~ .c-frame__move-bar {
+ .c-frame__move-bar {
transition: $transOut;
transition-delay: $moveBarOutDelay;
@include userSelectNone();
background: $editFrameMovebarColorBg;
box-shadow: rgba(black, 0.3) 0 2px;
box-shadow: rgba(black, 0.2) 0 1px;
bottom: auto;
display: block;
height: 0; // Height is set on hover below
opacity: 0.9;
opacity: 0.8;
max-height: 100%;
overflow: hidden;
text-align: center;
z-index: 10;
&:before {
// Grippy
@@ -105,6 +104,7 @@
> .c-so-view.has-complex-content {
transition: $transIn;
transition-delay: 0s;
padding-top: $editFrameMovebarH + $interiorMarginSm;
> .c-so-view__local-controls {
transform: translateY($editFrameMovebarH);

View File

@@ -4,7 +4,7 @@
> * {
// Label and value holders
flex: 1 1 50%;
flex: 1 1 auto;
display: flex;
flex-direction: row;
align-items: center;

View File

@@ -25,7 +25,6 @@ import CopyToClipboardAction from './actions/CopyToClipboardAction';
import DisplayLayout from './components/DisplayLayout.vue';
import DisplayLayoutToolbar from './DisplayLayoutToolbar.js';
import DisplayLayoutType from './DisplayLayoutType.js';
import DisplayLayoutDrawingObjectTypes from './DrawingObjectTypes.js';
import objectUtils from 'objectUtils';
@@ -126,11 +125,6 @@ export default function DisplayLayoutPlugin(options) {
return true;
}
});
for (const [type, definition] of Object.entries(DisplayLayoutDrawingObjectTypes)) {
openmct.types.addType(type, definition);
}
DisplayLayoutPlugin._installed = true;
};
}

View File

@@ -281,10 +281,6 @@ export default {
return false;
}
if (!this.isEditing) {
return false;
}
let containerId = event.dataTransfer.getData('containerid');
let container = this.containers.filter((c) => c.id === containerId)[0];
let containerPos = this.containers.indexOf(container);

View File

@@ -31,7 +31,7 @@
<div
ref="frame"
class="c-frame c-fl-frame__drag-wrapper is-selectable u-inspectable is-moveable"
:draggable="draggable"
draggable="true"
@dragstart="initDrag"
>
<object-frame
@@ -93,20 +93,18 @@ export default {
computed: {
hasFrame() {
return !this.frame.noFrame;
},
draggable() {
return this.isEditing;
}
},
mounted() {
if (this.frame.domainObjectIdentifier) {
let domainObjectPromise;
if (this.openmct.objects.supportsMutation(this.frame.domainObjectIdentifier)) {
this.domainObjectPromise = this.openmct.objects.getMutable(this.frame.domainObjectIdentifier);
domainObjectPromise = this.openmct.objects.getMutable(this.frame.domainObjectIdentifier);
} else {
this.domainObjectPromise = this.openmct.objects.get(this.frame.domainObjectIdentifier);
domainObjectPromise = this.openmct.objects.get(this.frame.domainObjectIdentifier);
}
this.domainObjectPromise.then((object) => {
domainObjectPromise.then((object) => {
this.setDomainObject(object);
});
}
@@ -114,13 +112,7 @@ export default {
this.dragGhost = document.getElementById('js-fl-drag-ghost');
},
beforeDestroy() {
if (this.domainObjectPromise) {
this.domainObjectPromise.then(() => {
if (this?.domainObject?.isMutable) {
this.openmct.objects.destroyMutable(this.domainObject);
}
});
} else if (this?.domainObject?.isMutable) {
if (this.domainObject.isMutable) {
this.openmct.objects.destroyMutable(this.domainObject);
}

View File

@@ -159,7 +159,7 @@ function ToolbarProvider(openmct) {
let prompt = openmct.overlays.dialog({
iconClass: 'alert',
message: 'This action will permanently delete this container from this Flexible Layout. Do you want to continue?',
message: 'This action will permanently delete this container from this Flexible Layout',
buttons: [
{
label: 'OK',

View File

@@ -52,8 +52,6 @@ $meterNeedleBorderRadius: 5px;
.c-dial {
max-height: 100%;
max-width: 100%;
display: block;
margin: auto; // Centers SVG in container while allowing scaling
&__bg {
fill: $colorGaugeBg;

View File

@@ -27,7 +27,7 @@ export default function () {
openmct.types.addType('hyperlink', {
name: 'Hyperlink',
key: 'hyperlink',
description: 'A text element or button that links to any URL including Open MCT views.',
description: 'A hyperlink to redirect to a different link',
creatable: true,
cssClass: 'icon-chain-links',
initialize: function (domainObject) {

View File

@@ -38,7 +38,6 @@
<img
class="c-thumb__image"
:src="image.url"
fetchpriority="low"
>
</a>
<div class="c-thumb__timestamp">{{ image.formattedTime }}</div>

View File

@@ -82,7 +82,6 @@
}"
:data-openmct-image-timestamp="time"
:data-openmct-object-keystring="keyString"
fetchpriority="low"
>
<div
v-if="imageUrl"

View File

@@ -151,7 +151,6 @@
:key="entry.id"
:entry="entry"
:domain-object="domainObject"
:notebook-annotations="notebookAnnotations[entry.id]"
:selected-page="selectedPage"
:selected-section="selectedSection"
:read-only="false"
@@ -220,12 +219,10 @@ export default {
isRestricted: this.domainObject.type === RESTRICTED_NOTEBOOK_TYPE,
search: '',
searchResults: [],
lastLocalAnnotationCreation: 0,
showTime: this.domainObject.configuration.showTime || 0,
showNav: false,
sidebarCoversEntries: false,
filteredAndSortedEntries: [],
notebookAnnotations: {}
filteredAndSortedEntries: []
};
},
computed: {
@@ -292,8 +289,7 @@ export default {
this.getSearchResults = debounce(this.getSearchResults, 500);
this.syncUrlWithPageAndSection = debounce(this.syncUrlWithPageAndSection, 100);
},
async mounted() {
await this.loadAnnotations();
mounted() {
this.formatSidebar();
this.setSectionAndPageFromUrl();
@@ -311,13 +307,6 @@ export default {
this.unobserveEntries();
}
Object.keys(this.notebookAnnotations).forEach(entryID => {
const notebookAnnotationsForEntry = this.notebookAnnotations[entryID];
notebookAnnotationsForEntry.forEach(notebookAnnotation => {
this.openmct.objects.destroyMutable(notebookAnnotation);
});
});
window.removeEventListener('orientationchange', this.formatSidebar);
window.removeEventListener('hashchange', this.setSectionAndPageFromUrl);
},
@@ -349,32 +338,6 @@ export default {
}
});
},
async loadAnnotations() {
if (!this.openmct.annotation.getAvailableTags().length) {
return;
}
this.lastLocalAnnotationCreation = this.domainObject.annotationLastCreated ?? 0;
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;
if (!this.notebookAnnotations[entryId]) {
this.$set(this.notebookAnnotations, entryId, []);
}
const annotationExtant = this.notebookAnnotations[entryId].some((existingAnnotation) => {
return this.openmct.objects.areIdsEqual(existingAnnotation.identifier, foundAnnotation.identifier);
});
if (!annotationExtant) {
const annotationArray = this.notebookAnnotations[entryId];
const mutableAnnotation = this.openmct.objects.toMutable(foundAnnotation);
annotationArray.push(mutableAnnotation);
}
});
},
filterAndSortEntries() {
const filterTime = Date.now();
const pageEntries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage) || [];
@@ -387,10 +350,6 @@ export default {
this.filteredAndSortedEntries = this.defaultSort === 'oldest'
? filteredPageEntriesByTime
: [...filteredPageEntriesByTime].reverse();
if (this.lastLocalAnnotationCreation < this.domainObject.annotationLastCreated) {
this.loadAnnotations();
}
},
changeSelectedSection({ sectionId, pageId }) {
const sections = this.sections.map(s => {
@@ -514,10 +473,14 @@ export default {
]
});
},
removeAnnotations(entryId) {
if (this.notebookAnnotations[entryId]) {
this.openmct.annotation.deleteAnnotations(this.notebookAnnotations[entryId]);
}
async removeAnnotations(entryId) {
const targetKeyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
const query = {
targetKeyString,
entryId
};
const existingAnnotation = await this.openmct.annotation.getAnnotation(query, this.openmct.objects.SEARCH_TYPES.NOTEBOOK_ANNOTATIONS);
this.openmct.annotation.removeAnnotationTags(existingAnnotation);
},
checkEntryPos(entry) {
const entryPos = getEntryPosById(entry.id, this.domainObject, this.selectedSection, this.selectedPage);
@@ -540,7 +503,7 @@ export default {
this.openmct.editor.cancel();
}
},
async dropOnEntry(event) {
dropOnEntry(event) {
event.preventDefault();
event.stopImmediatePropagation();
@@ -566,8 +529,7 @@ export default {
objectPath,
openmct: this.openmct
};
const embed = await createNewEmbed(snapshotMeta);
const embed = createNewEmbed(snapshotMeta);
this.newEntry(embed);
},
focusOnEntryId() {

View File

@@ -84,8 +84,9 @@
<TagEditor
:domain-object="domainObject"
:annotations="notebookAnnotations"
:annotation-query="annotationQuery"
:annotation-type="openmct.annotation.ANNOTATION_TYPES.NOTEBOOK"
:annotation-search-type="openmct.objects.SEARCH_TYPES.NOTEBOOK_ANNOTATIONS"
:target-specific-details="{entryId: entry.id}"
@tags-updated="timestampAndUpdate"
/>
@@ -162,12 +163,6 @@ export default {
return {};
}
},
notebookAnnotations: {
type: Array,
default() {
return [];
}
},
entry: {
type: Object,
default() {
@@ -209,6 +204,15 @@ export default {
createdOnDate() {
return this.formatTime(this.entry.createdOn, 'YYYY-MM-DD');
},
annotationQuery() {
const targetKeyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
return {
targetKeyString,
entryId: this.entry.id,
modified: this.entry.modified
};
},
createdOnTime() {
return this.formatTime(this.entry.createdOn, 'HH:mm:ss');
},

View File

@@ -6,7 +6,6 @@
<span class="c-sidebar__header-label">{{ sectionTitle }}</span>
<button
class="c-icon-button c-icon-button--major icon-plus"
aria-label="Add Section"
@click="addSection"
>
<span class="c-list-button__label">Add</span>
@@ -34,7 +33,6 @@
<button
class="c-icon-button c-icon-button--major icon-plus"
aria-label="Add Page"
@click="addPage"
>
<span class="c-icon-button__label">Add</span>

View File

@@ -1,23 +1,22 @@
import { isAnnotationType, isNotebookType, isNotebookOrAnnotationType } from './notebook-constants';
import _ from 'lodash';
import { isNotebookType } from './notebook-constants';
export default function (openmct) {
const apiSave = openmct.objects.save.bind(openmct.objects);
openmct.objects.save = async (domainObject) => {
if (!isNotebookOrAnnotationType(domainObject)) {
if (!isNotebookType(domainObject)) {
return apiSave(domainObject);
}
const isNewMutable = !domainObject.isMutable;
const localMutable = openmct.objects.toMutable(domainObject);
const localMutable = openmct.objects._toMutable(domainObject);
let result;
try {
result = await apiSave(localMutable);
} catch (error) {
if (error instanceof openmct.objects.errors.Conflict) {
result = await resolveConflicts(domainObject, localMutable, openmct);
result = resolveConflicts(localMutable, openmct);
} else {
result = Promise.reject(error);
}
@@ -31,56 +30,16 @@ export default function (openmct) {
};
}
function resolveConflicts(domainObject, localMutable, openmct) {
if (isNotebookType(domainObject)) {
return resolveNotebookEntryConflicts(localMutable, openmct);
} else if (isAnnotationType(domainObject)) {
return resolveNotebookTagConflicts(localMutable, openmct);
}
}
function resolveConflicts(localMutable, openmct) {
const localEntries = JSON.parse(JSON.stringify(localMutable.configuration.entries));
async function resolveNotebookTagConflicts(localAnnotation, openmct) {
const localClonedAnnotation = structuredClone(localAnnotation);
const remoteMutable = await openmct.objects.getMutable(localClonedAnnotation.identifier);
// should only be one annotation per targetID, entryID, and tag; so for sanity, ensure we have the
// same targetID, entryID, and tags for this conflict
if (!(_.isEqual(remoteMutable.tags, localClonedAnnotation.tags))) {
throw new Error('Conflict on annotation\'s tag has different tags than remote');
}
Object.keys(localClonedAnnotation.targets).forEach(targetKey => {
if (!remoteMutable.targets[targetKey]) {
throw new Error(`Conflict on annotation's target is missing ${targetKey}`);
}
const remoteMutableTarget = remoteMutable.targets[targetKey];
const localMutableTarget = localClonedAnnotation.targets[targetKey];
if (remoteMutableTarget.entryId !== localMutableTarget.entryId) {
throw new Error(`Conflict on annotation's entryID ${remoteMutableTarget.entryId} has a different entry Id ${localMutableTarget.entryId}`);
}
});
if (remoteMutable._deleted && (remoteMutable._deleted !== localClonedAnnotation._deleted)) {
// not deleting wins 😘
openmct.objects.mutate(remoteMutable, '_deleted', false);
}
openmct.objects.destroyMutable(remoteMutable);
return true;
}
async function resolveNotebookEntryConflicts(localMutable, openmct) {
if (localMutable.configuration.entries) {
const localEntries = structuredClone(localMutable.configuration.entries);
const remoteMutable = await openmct.objects.getMutable(localMutable.identifier);
return openmct.objects.getMutable(localMutable.identifier).then((remoteMutable) => {
applyLocalEntries(remoteMutable, localEntries, openmct);
openmct.objects.destroyMutable(remoteMutable);
}
return true;
openmct.objects.destroyMutable(remoteMutable);
return true;
});
}
function applyLocalEntries(mutable, entries, openmct) {

View File

@@ -1,6 +1,5 @@
export const NOTEBOOK_TYPE = 'notebook';
export const RESTRICTED_NOTEBOOK_TYPE = 'restricted-notebook';
export const ANNOTATION_TYPE = 'annotation';
export const EVENT_SNAPSHOTS_UPDATED = 'SNAPSHOTS_UPDATED';
export const NOTEBOOK_DEFAULT = 'DEFAULT';
export const NOTEBOOK_SNAPSHOT = 'SNAPSHOT';
@@ -10,18 +9,10 @@ export const NOTEBOOK_INSTALLED_KEY = '_NOTEBOOK_PLUGIN_INSTALLED';
export const RESTRICTED_NOTEBOOK_INSTALLED_KEY = '_RESTRICTED_NOTEBOOK_PLUGIN_INSTALLED';
// these only deals with constants, figured this could skip going into a utils file
export function isNotebookOrAnnotationType(domainObject) {
return (isNotebookType(domainObject) || isAnnotationType(domainObject));
}
export function isNotebookType(domainObject) {
return [NOTEBOOK_TYPE, RESTRICTED_NOTEBOOK_TYPE].includes(domainObject.type);
}
export function isAnnotationType(domainObject) {
return [ANNOTATION_TYPE].includes(domainObject.type);
}
export function isNotebookViewType(view) {
return [NOTEBOOK_VIEW_TYPE, RESTRICTED_NOTEBOOK_VIEW_TYPE].includes(view);
}

View File

@@ -27,7 +27,7 @@ export default class AbstractStatusIndicator {
#configuration;
/**
* @param {*} openmct the Open MCT API (proper typescript doc to come)
* @param {*} openmct the Open MCT API (proper jsdoc to come)
* @param {import('@/api/user/UserAPI').UserAPIConfiguration} configuration Per-deployment status styling. See the type definition in UserAPI
*/
constructor(openmct, configuration) {

View File

@@ -57,9 +57,8 @@ describe('the plugin', () => {
it('calculates an fps value', async () => {
await loopForABit();
// eslint-disable-next-line radix
const fps = parseInt(performanceIndicator.text().split(' fps')[0]);
expect(fps).toBeGreaterThan(0);
// eslint-disable-next-line
expect(parseInt(performanceIndicator.text().split(' fps')[0])).toBeGreaterThan(0);
});
function loopForABit() {
@@ -67,7 +66,7 @@ describe('the plugin', () => {
return new Promise(resolve => {
requestAnimationFrame(function loop() {
if (++frames > 90) {
if (++frames === 240) {
resolve();
} else {
requestAnimationFrame(loop);

View File

@@ -2,9 +2,6 @@
const connections = [];
let connected = false;
let couchEventSource;
let changesFeedUrl;
const keepAliveTime = 20 * 1000;
let keepAliveTimer;
const controller = new AbortController();
self.onconnect = function (e) {
@@ -38,8 +35,7 @@
return;
}
changesFeedUrl = event.data.url;
self.listenForChanges();
self.listenForChanges(event.data.url);
}
};
@@ -67,28 +63,17 @@
});
};
self.listenForChanges = function () {
if (keepAliveTimer) {
clearTimeout(keepAliveTimer);
}
self.listenForChanges = function (url) {
console.debug('⇿ Opening CouchDB change feed connection ⇿');
/**
* Once the connection has been opened, poll every 20 seconds to see if the EventSource has closed unexpectedly.
* If it has, attempt to reconnect.
*/
keepAliveTimer = setTimeout(self.listenForChanges, keepAliveTime);
couchEventSource = new EventSource(url);
couchEventSource.onerror = self.onerror;
couchEventSource.onopen = self.onopen;
if (!couchEventSource || couchEventSource.readyState === EventSource.CLOSED) {
console.debug('⇿ Opening CouchDB change feed connection ⇿');
couchEventSource = new EventSource(changesFeedUrl);
couchEventSource.onerror = self.onerror;
couchEventSource.onopen = self.onopen;
// start listening for events
couchEventSource.addEventListener('message', self.onCouchMessage);
connected = true;
console.debug('⇿ Opened connection ⇿');
}
// start listening for events
couchEventSource.addEventListener('message', self.onCouchMessage);
connected = true;
console.debug('⇿ Opened connection ⇿');
};
self.updateCouchStateIndicator = function () {

View File

@@ -23,7 +23,7 @@
import CouchDocument from "./CouchDocument";
import CouchObjectQueue from "./CouchObjectQueue";
import { PENDING, CONNECTED, DISCONNECTED, UNKNOWN } from "./CouchStatusIndicator";
import { isNotebookOrAnnotationType } from '../../notebook/notebook-constants.js';
import { isNotebookType } from '../../notebook/notebook-constants.js';
const REV = "_rev";
const ID = "_id";
@@ -71,7 +71,7 @@ class CouchObjectProvider {
}
onSharedWorkerMessageError(event) {
console.error('Error', event);
console.log('Error', event);
}
isSynchronizedObject(object) {
@@ -187,7 +187,6 @@ class CouchObjectProvider {
let fetchOptions = {
method,
body,
priority: 'high',
signal
};
@@ -290,7 +289,7 @@ class CouchObjectProvider {
this.objectQueue[key] = new CouchObjectQueue(undefined, response[REV]);
}
if (isNotebookOrAnnotationType(object)) {
if (isNotebookType(object) || object.type === 'annotation') {
//Temporary measure until object sync is supported for all object types
//Always update notebook revision number because we have realtime sync, so always assume it's the latest.
this.objectQueue[key].updateRevision(response[REV]);

View File

@@ -31,7 +31,7 @@ class CouchSearchProvider {
constructor(couchObjectProvider) {
this.couchObjectProvider = couchObjectProvider;
this.searchTypes = couchObjectProvider.openmct.objects.SEARCH_TYPES;
this.supportedSearchTypes = [this.searchTypes.OBJECTS, this.searchTypes.ANNOTATIONS, this.searchTypes.TAGS];
this.supportedSearchTypes = [this.searchTypes.OBJECTS, this.searchTypes.ANNOTATIONS, this.searchTypes.NOTEBOOK_ANNOTATIONS, this.searchTypes.TAGS];
}
supportsSearchType(searchType) {
@@ -43,6 +43,8 @@ class CouchSearchProvider {
return this.searchForObjects(query, abortSignal);
} else if (searchType === this.searchTypes.ANNOTATIONS) {
return this.searchForAnnotations(query, abortSignal);
} else if (searchType === this.searchTypes.NOTEBOOK_ANNOTATIONS) {
return this.searchForNotebookAnnotations(query, abortSignal);
} else if (searchType === this.searchTypes.TAGS) {
return this.searchForTags(query, abortSignal);
} else {
@@ -89,19 +91,46 @@ class CouchSearchProvider {
return this.couchObjectProvider.getObjectsByFilter(filter, abortSignal);
}
searchForTags(tagsArray, abortSignal) {
if (!tagsArray || !tagsArray.length) {
return [];
}
searchForNotebookAnnotations({targetKeyString, entryId}, abortSignal) {
const filter = {
"selector": {
"$and": [
{
"model.type": {
"$eq": "annotation"
}
},
{
"model.annotationType": {
"$eq": "NOTEBOOK"
}
},
{
"model": {
"targets": {
}
}
}
]
}
};
filter.selector.$and[2].model.targets[targetKeyString] = {
"entryId": {
"$eq": entryId
}
};
return this.couchObjectProvider.getObjectsByFilter(filter, abortSignal);
}
searchForTags(tagsArray, abortSignal) {
const filter = {
"selector": {
"$and": [
{
"model.tags": {
"$elemMatch": {
"$or": [
]
"$eq": `${tagsArray[0]}`
}
}
},
@@ -113,11 +142,6 @@ class CouchSearchProvider {
]
}
};
tagsArray.forEach(tag => {
filter.selector.$and[0]["model.tags"].$elemMatch.$or.push({
"$eq": `${tag}`
});
});
return this.couchObjectProvider.getObjectsByFilter(filter, abortSignal);
}

View File

@@ -109,12 +109,6 @@ describe('the plugin', () => {
expect(result.identifier.key).toEqual(mockDomainObject.identifier.key);
});
it('prioritizes couch requests above other requests', async () => {
await openmct.objects.get(mockDomainObject.identifier);
const fetchOptions = fetch.calls.mostRecent().args[1];
expect(fetchOptions.priority).toEqual('high');
});
it('creates an object and starts shared worker', async () => {
const result = await openmct.objects.save(mockDomainObject);
expect(provider.create).toHaveBeenCalled();

View File

@@ -1,117 +0,0 @@
# Plan view and domain objects
Plans provide a view for a list of activities grouped by categories.
## Plan category and activity JSON format
The JSON format for a plan consists of categories/groups and a list of activities for each category.
Activity properties include:
* name: Name of the activity
* start: Timestamps in milliseconds
* end: Timestamps in milliseconds
* type: Matches the name of the category it is in
* color: Background color for the activity
* textColor: Color of the name text for the activity
* The format of the json file is as follows:
```json
{
"TEST_GROUP": [{
"name": "Event 1 with a really long name",
"start": 1665323197000,
"end": 1665344921000,
"type": "TEST_GROUP",
"color": "orange",
"textColor": "white"
}],
"GROUP_2": [{
"name": "Event 2",
"start": 1665409597000,
"end": 1665456252000,
"type": "GROUP_2",
"color": "red",
"textColor": "white"
}]
}
```
## Plans using JSON file uploads
Plan domain objects can be created by uploading a JSON file with the format above to render categories and activities.
## Using Domain Objects directly
If uploading a JSON is not desired, it is possible to visualize domain objects of type 'plan'.
The standard model is as follows:
```javascript
{
identifier: {
namespace: ""
key: "test-plan"
}
name:"A plan object",
type:"plan",
location:"ROOT",
selectFile: {
body: {
SOME_CATEGORY: [{
name: "An activity",
start: 1665323197000,
end: 1665323197100,
type: "SOME_CATEGORY"
}
],
ANOTHER_CATEGORY: [{
name: "An activity",
start: 1665323197000,
end: 1665323197100,
type: "ANOTHER_CATEGORY"
}
]
}
}
}
```
If your data has non-standard keys for `start, end, type and activities` properties, use the `sourceMap` property mapping.
```javascript
{
identifier: {
namespace: ""
key: "another-test-plan"
}
name:"Another plan object",
type:"plan",
location:"ROOT",
sourceMap: {
start: 'start_time',
end: 'end_time',
activities: 'items',
groupId: 'category'
},
selectFile: {
body: {
items: [
{
name: "An activity",
start_time: 1665323197000,
end_time: 1665323197100,
category: "SOME_CATEGORY"
},
{
name: "Another activity",
start_time: 1665323198000,
end_time: 1665323198100,
category: "ANOTHER_CATEGORY"
}
]
}
}
}
```
## Rendering categories and activities:
The algorithm to render categories and activities on a canvas is as follows:
* Each category gets a swimlane.
* Activities within a category are rendered within it's swimlane.
* Render each activity on a given row if it's duration+label do not overlap (start/end times) with an existing activity on that row.
* Move to the next available row within a swimlane if there is overlap
* Labels for a given activity will be rendered within it's duration slot if it fits in that rectangular space.
* Labels that do not fit within an activity's duration slot are rendered outside, to the right of the duration slot.

View File

@@ -23,7 +23,6 @@
import {createOpenMct, resetApplicationState} from "utils/testing";
import PlanPlugin from "../plan/plugin";
import Vue from 'vue';
import Properties from "@/ui/inspector/details/Properties.vue";
describe('the plugin', function () {
let planDefinition;
@@ -213,63 +212,4 @@ describe('the plugin', function () {
});
});
});
describe('the plan version', () => {
let component;
let componentObject;
let testPlanObject = {
name: 'Plan',
type: 'plan',
identifier: {
key: 'test-plan',
namespace: ''
},
version: 'v1'
};
beforeEach(() => {
openmct.selection.select([{
element: element,
context: {
item: testPlanObject
}
}, {
element: openmct.layout.$refs.browseObject.$el,
context: {
item: testPlanObject,
supportsMultiSelect: false
}
}], false);
return Vue.nextTick().then(() => {
let viewContainer = document.createElement('div');
child.append(viewContainer);
component = new Vue({
el: viewContainer,
components: {
Properties
},
provide: {
openmct: openmct
},
template: '<properties/>'
});
});
});
afterEach(() => {
component.$destroy();
});
it('provides an inspector view with the version information if available', () => {
componentObject = component.$root.$children[0];
const propertiesEls = componentObject.$el.querySelectorAll('.c-inspect-properties__row');
expect(propertiesEls.length).toEqual(4);
const found = Array.from(propertiesEls).some((propertyEl) => {
return (propertyEl.children[0].innerHTML.trim() === 'Version'
&& propertyEl.children[1].innerHTML.trim() === 'v1');
});
expect(found).toBeTrue();
});
});
});

View File

@@ -122,7 +122,7 @@
<button
class="c-button icon-reset"
title="Reset pan/zoom"
@click="resumeRealtimeData()"
@click="clear()"
>
</button>
</div>
@@ -141,7 +141,7 @@
v-if="isFrozen"
class="c-button icon-arrow-right pause-play is-paused"
title="Resume displaying real-time data"
@click="resumeRealtimeData()"
@click="play()"
>
</button>
</div>
@@ -213,8 +213,6 @@ import XAxis from "./axis/XAxis.vue";
import YAxis from "./axis/YAxis.vue";
import _ from "lodash";
const OFFSET_THRESHOLD = 10;
export default {
components: {
XAxis,
@@ -331,8 +329,6 @@ export default {
}
},
mounted() {
this.offsetWidth = 0;
document.addEventListener('keydown', this.handleKeyDown);
document.addEventListener('keyup', this.handleKeyUp);
eventHelpers.extend(this);
@@ -442,8 +438,7 @@ export default {
});
},
removeSeries(plotSeries, index) {
this.seriesModels.splice(index, 1);
removeSeries(plotSeries) {
this.checkSameRangeValue();
this.stopListening(plotSeries);
},
@@ -581,8 +576,9 @@ export default {
};
this.config.xAxis.set('range', newRange);
if (!isTick) {
this.clearPanZoomHistory();
this.synchronizeIfBoundsMatch();
this.skipReloadOnInteraction = true;
this.clear();
this.skipReloadOnInteraction = false;
this.loadMoreData(newRange, true);
} else {
// If we're not panning or zooming (time conductor and plot x-axis times are not out of sync)
@@ -605,20 +601,16 @@ export default {
/**
* Handle end of user viewport change: load more data for current display
* bounds, and mark view as synchronized if necessary.
* bounds, and mark view as synchronized if bounds match configured bounds.
*/
userViewportChangeEnd() {
this.synchronizeIfBoundsMatch();
const xDisplayRange = this.config.xAxis.get('displayRange');
this.loadMoreData(xDisplayRange);
},
/**
* mark view as synchronized if bounds match configured bounds.
*/
synchronizeIfBoundsMatch() {
const xDisplayRange = this.config.xAxis.get('displayRange');
const xRange = this.config.xAxis.get('range');
if (!this.skipReloadOnInteraction) {
this.loadMoreData(xDisplayRange);
}
this.synchronized(xRange.min === xDisplayRange.min
&& xRange.max === xDisplayRange.max);
},
@@ -847,8 +839,7 @@ export default {
// needs to follow endMarquee so that plotHistory is pruned
const isAction = Boolean(this.plotHistory.length);
if (!isAction && !this.isFrozenOnMouseDown) {
this.clearPanZoomHistory();
this.synchronizeIfBoundsMatch();
return this.play();
}
},
@@ -1085,22 +1076,18 @@ export default {
this.setStatus();
},
resumeRealtimeData() {
this.clearPanZoomHistory();
this.userViewportChangeEnd();
},
clearPanZoomHistory() {
clear() {
this.config.yAxis.set('frozen', false);
this.config.xAxis.set('frozen', false);
this.setStatus();
this.plotHistory = [];
this.userViewportChangeEnd();
},
back() {
const previousAxisRanges = this.plotHistory.pop();
if (this.plotHistory.length === 0) {
this.resumeRealtimeData();
this.clear();
return;
}
@@ -1118,6 +1105,10 @@ export default {
this.freeze();
},
play() {
this.clear();
},
showSynchronizeDialog() {
const isLocalClock = this.timeContext.clock();
if (isLocalClock !== undefined) {
@@ -1181,9 +1172,7 @@ export default {
this.removeStatusListener();
}
if (this.plotContainerResizeObserver) {
this.plotContainerResizeObserver.disconnect();
}
this.plotContainerResizeObserver.disconnect();
this.stopFollowingTimeContext();
this.openmct.objectViews.off('clearData', this.clearData);
@@ -1192,12 +1181,9 @@ export default {
this.$emit('statusUpdated', status);
},
handleWindowResize() {
const newOffsetWidth = this.$parent.$refs.plotWrapper.offsetWidth;
//we ignore when width gets smaller
const offsetChange = newOffsetWidth - this.offsetWidth;
if (this.$parent.$refs.plotWrapper
&& offsetChange > OFFSET_THRESHOLD) {
this.offsetWidth = newOffsetWidth;
&& (this.offsetWidth !== this.$parent.$refs.plotWrapper.offsetWidth)) {
this.offsetWidth = this.$parent.$refs.plotWrapper.offsetWidth;
this.config.series.models.forEach(this.loadSeriesData, this);
}
},

View File

@@ -93,9 +93,6 @@ export default function PlotViewProvider(openmct) {
destroy: function () {
component.$destroy();
component = undefined;
},
getComponent() {
return component;
}
};
}

View File

@@ -102,8 +102,8 @@ export default class Collection extends Model {
throw new Error('model not found in collection.');
}
this.models.splice(index, 1);
this.emit('remove', model, index);
this.models.splice(index, 1);
}
destroy(model) {

View File

@@ -144,6 +144,12 @@ describe("the plugin", function () {
element.appendChild(child);
document.body.appendChild(element);
spyOn(window, 'ResizeObserver').and.returnValue({
observe() {},
unobserve() {},
disconnect() {}
});
openmct.types.addType("test-object", {
creatable: true
});
@@ -160,7 +166,7 @@ describe("the plugin", function () {
afterEach((done) => {
openmct.time.timeSystem('utc', {
start: 0,
end: 2
end: 1
});
configStore.deleteAll();
@@ -500,23 +506,6 @@ describe("the plugin", function () {
expect(playElAfterChartClick.length).toBe(1);
});
it("clicking the plot does not request historical data", async () => {
expect(openmct.telemetry.request).toHaveBeenCalledTimes(2);
// simulate an errant mouse click
// the second item is the canvas we need to use
const canvas = element.querySelectorAll("canvas")[1];
const mouseDownEvent = new MouseEvent('mousedown');
const mouseUpEvent = new MouseEvent('mouseup');
canvas.dispatchEvent(mouseDownEvent);
// mouseup event is bound to the window
window.dispatchEvent(mouseUpEvent);
await Vue.nextTick();
expect(openmct.telemetry.request).toHaveBeenCalledTimes(2);
});
});
describe('controls in time strip view', () => {
@@ -539,94 +528,6 @@ describe("the plugin", function () {
});
});
describe('resizing the plot', () => {
let plotContainerResizeObserver;
let resizePromiseResolve;
let testTelemetryObject;
let applicableViews;
let plotViewProvider;
let plotView;
let resizePromise;
beforeEach(() => {
testTelemetryObject = {
identifier: {
namespace: "",
key: "test-object"
},
type: "test-object",
name: "Test Object",
telemetry: {
values: [{
key: "utc",
format: "utc",
name: "Time",
hints: {
domain: 1
}
}, {
key: "some-key",
name: "Some attribute",
hints: {
range: 1
}
}, {
key: "some-other-key",
name: "Another attribute",
hints: {
range: 2
}
}]
}
};
openmct.router.path = [testTelemetryObject];
applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath);
plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === "plot-single");
plotView = plotViewProvider.view(testTelemetryObject, []);
plotView.show(child, true);
resizePromise = new Promise((resolve) => {
resizePromiseResolve = resolve;
});
const handlePlotResize = _.debounce(() => {
resizePromiseResolve(true);
}, 600);
plotContainerResizeObserver = new ResizeObserver(handlePlotResize);
plotContainerResizeObserver.observe(plotView.getComponent().$children[0].$children[1].$parent.$refs.plotWrapper);
return Vue.nextTick(() => {
plotView.getComponent().$children[0].$children[1].stopFollowingTimeContext();
spyOn(plotView.getComponent().$children[0].$children[1], 'loadSeriesData').and.callThrough();
});
});
afterEach(() => {
plotContainerResizeObserver.disconnect();
openmct.router.path = null;
});
it("requests historical data when over the threshold", (done) => {
element.style.width = '680px';
resizePromise.then(() => {
expect(plotView.getComponent().$children[0].$children[1].loadSeriesData).toHaveBeenCalledTimes(1);
done();
});
});
it("does not request historical data when under the threshold", (done) => {
element.style.width = '644px';
resizePromise.then(() => {
expect(plotView.getComponent().$children[0].$children[1].loadSeriesData).not.toHaveBeenCalled();
done();
});
});
});
describe('the inspector view', () => {
let component;
let viewComponentObject;

View File

@@ -31,7 +31,7 @@ define([
openmct.types.addType('tabs', {
name: "Tabs View",
description: 'Quickly navigate between multiple objects of any type using tabs.',
description: 'Add multiple objects of any type to this view, and quickly navigate between them with tabs',
creatable: true,
cssClass: 'icon-tabs-view',
initialize(domainObject) {

View File

@@ -23,9 +23,9 @@
define(function () {
return {
name: 'Telemetry Table',
description: 'Display values for one or more telemetry end points in a scrolling table. Each row is a time-stamped value.',
description: 'Display telemetry values for the current time bounds in tabular form. Supports filtering and sorting.',
creatable: true,
cssClass: 'icon-tabular-scrolling',
cssClass: 'icon-tabular-realtime',
initialize(domainObject) {
domainObject.composition = [];
domainObject.configuration = {

View File

@@ -36,7 +36,7 @@ export default function TelemetryTableViewProvider(openmct) {
return {
key: 'table',
name: 'Telemetry Table',
cssClass: 'icon-tabular-scrolling',
cssClass: 'icon-tabular-realtime',
canView(domainObject) {
return domainObject.type === 'table'
|| hasTelemetry(domainObject);

View File

@@ -26,7 +26,6 @@
>
<div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left">
<button
aria-label="Time Conductor History"
class="c-button--menu c-history-button icon-history"
@click.prevent.stop="showHistoryMenu"
>
@@ -145,11 +144,10 @@ export default {
this.initializeHistoryIfNoHistory();
},
getHistoryMenuItems() {
const descriptionDateFormat = 'YYYY-MM-DD HH:mm:ss.SSS';
const history = this.historyForCurrentTimeSystem.map(timespan => {
let name;
const startTime = this.formatTime(timespan.start);
const description = `${this.formatTime(timespan.start, descriptionDateFormat)} - ${this.formatTime(timespan.end, descriptionDateFormat)}`;
let startTime = this.formatTime(timespan.start);
let description = `${startTime} - ${this.formatTime(timespan.end)}`;
if (this.timeSystem.isUTCBased && !this.openmct.time.clock()) {
name = `${startTime} ${millisecondsToDHMS(timespan.end - timespan.start)}`;
@@ -255,7 +253,7 @@ export default {
return maxRecordsLength;
},
formatTime(time, utcDateFormat) {
formatTime(time) {
let format = this.timeSystem.timeFormat;
let isNegativeOffset = false;
@@ -276,8 +274,7 @@ export default {
let formattedDate;
if (formatter instanceof UTCTimeFormat) {
const formatString = formatter.isValidFormatString(utcDateFormat) ? utcDateFormat : formatter.DATE_FORMATS.PRECISION_SECONDS;
formattedDate = formatter.format(time, formatString);
formattedDate = formatter.format(time, formatter.DATE_FORMATS.PRECISION_SECONDS);
} else {
formattedDate = formatter.format(time);
}

View File

@@ -117,7 +117,7 @@ export default {
description: "Monitor streaming data in real-time. The Time "
+ "Conductor and displays will automatically advance themselves based on this clock. " + clock.description,
cssClass: clock.cssClass || 'icon-clock',
testId: 'conductor-modeOption-realtime',
testId: `conductor-modeOption-${clock.name.toLowerCase().replace(' ', '-')}`,
onItemClicked: () => this.setOption(key)
};
}

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