Merge branch 'master' into nb-embed-enhance

This commit is contained in:
Jamie V 2022-10-03 13:48:10 -07:00 committed by GitHub
commit 945f220727
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
53 changed files with 902 additions and 831 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 unit tests included?
* [ ] Appropriate automated 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

@ -10,9 +10,6 @@
# 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 -->
# Building Applications With Open MCT
# Developing Applications With Open MCT
## Scope and purpose of this document
@ -72,8 +72,7 @@ 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
@ -86,7 +85,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

View File

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

View File

@ -30,6 +30,8 @@ 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/).
@ -43,11 +45,9 @@ 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).
## Building Applications With Open MCT
## Developing Applications With Open MCT
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).
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).
## 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](https://github.com/nasa/openmct/blob/master/API.md#plugins).
For information on writing plugins, please see [our API documentation](./API.md#plugins).
## Tests

92
app.js
View File

@ -1,92 +0,0 @@
/*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);
});

View File

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

View File

@ -1,209 +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.
*****************************************************************************/
/*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));
});
});
});
}());

View File

@ -1,9 +0,0 @@
<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/) document is generated from inline documentation
using [JSDoc](http://usejsdoc.org/), and describes the JavaScript objects and
* 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
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 app.js.
- `@addInit` - Initializes the browser with an injected and artificial state. Useful for loading non-default plugins. Likely will not work outside of `npm start`.
- `@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

@ -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: 'cross-env NODE_ENV=test npm run start',
command: 'npm run start:coverage',
url: 'http://localhost:8080/#',
timeout: 200 * 1000,
reuseExistingServer: false

View File

@ -12,10 +12,7 @@ const config = {
testIgnore: '**/*.perf.spec.js',
timeout: 30 * 1000,
webServer: {
env: {
NODE_ENV: 'test'
},
command: 'npm run start',
command: 'npm run start:coverage',
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 because trace is enabled only on first retry
retries: 1, //Only for debugging purposes for trace: 'on-first-retry'
testDir: 'tests/performance/',
timeout: 60 * 1000,
workers: 1, //Only run in serial with 1 worker
webServer: {
command: 'cross-env NODE_ENV=test npm run start',
command: 'npm run start', //coverage not generated
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: 1, // visual tests should never retry due to snapshot comparison errors. Leaving as a shim
retries: 0, // 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: 'cross-env NODE_ENV=test npm run start',
command: 'npm run start:coverage',
url: 'http://localhost:8080/#',
timeout: 200 * 1000,
reuseExistingServer: !process.env.CI
@ -31,7 +31,7 @@ const config = {
}
},
{
name: 'chrome-snow-theme',
name: 'chrome-snow-theme', //Runs the same visual tests but with snow-theme enabled
use: {
browserName: 'chromium',
theme: 'snow'

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
(app.js and ./e2e/webpack-dev-middleware.js)
(`npm start` and ./e2e/webpack-dev-middleware.js)
*/
const { test } = require('../../baseFixtures.js');

View File

@ -0,0 +1,232 @@
/*****************************************************************************
* 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 Network Request Inspection @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);
});
});
// 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

@ -81,10 +81,8 @@ 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");
@ -97,9 +95,7 @@ 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");
@ -108,39 +104,31 @@ 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"]')).toContainText("Driving");
await expect(page.locator('[aria-label="Search Result"]')).not.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"]')).toContainText("Driving");
await expect(page.locator('[aria-label="Search Result"]')).not.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('[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('.c-tag__label:has-text("Driving")');
await page.locator('.c-tag__label:has-text("Driving") ~ .c-completed-tag-deletion').click();
await page.hover('[aria-label="Tag"]:has-text("Driving")');
await page.locator('[aria-label="Tag"]:has-text("Driving") ~ .c-completed-tag-deletion').click();
await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Science");
await expect(page.locator('[aria-label="Notebook Entry"]')).not.toContainText("Driving");
// 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");
});
@ -153,7 +141,6 @@ 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 page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sci');

View File

@ -0,0 +1,54 @@
/*****************************************************************************
* 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

@ -168,3 +168,23 @@ 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

@ -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's app.js started by `npm run start` within the
are only meant to run against openmct started by `npm 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's app.js started by `npm run start` within the
are only meant to run against openmct started by `npm 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,12 +0,0 @@
{
"source": {
"include": [
"src/"
],
"includePattern": "src/.+\\.js$",
"excludePattern": ".+\\Spec\\.js$|lib/.+"
},
"plugins": [
"plugins/markdown"
]
}

View File

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

View File

@ -1,96 +0,0 @@
---
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

@ -15,21 +15,19 @@
"@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.23.1",
"eslint": "8.24.0",
"eslint-plugin-compat": "4.0.2",
"eslint-plugin-playwright": "0.11.2",
"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",
@ -51,12 +49,12 @@
"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.26.1",
"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-loader": "13.0.2",
@ -69,23 +67,23 @@
"vue-template-compiler": "2.6.14",
"webpack": "5.74.0",
"webpack-cli": "4.10.0",
"webpack-dev-middleware": "5.3.3",
"webpack-hot-middleware": "2.25.2",
"webpack-dev-server": "4.11.1",
"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": "node app.js",
"start": "npx webpack serve --config ./webpack.dev.js",
"start:coverage": "npx webpack serve --config ./webpack.coverage.js",
"lint": "eslint example src e2e --ext .js,.vue openmct.js --max-warnings=0",
"lint:fix": "eslint example src e2e --ext .js,.vue openmct.js --fix",
"build:prod": "cross-env webpack --config webpack.prod.js",
"build:prod": "webpack --config webpack.prod.js",
"build:dev": "webpack --config webpack.dev.js",
"build:coverage": "webpack --config webpack.coverage.js",
"build:watch": "webpack --config webpack.dev.js --watch",
"info": "npx envinfo --system --browsers --npmPackages --binaries --languages --markdown",
"test": "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": "karma start",
"test:debug": "KARMA_DEBUG=true karma start",
"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\"",
@ -95,13 +93,12 @@
"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": {
@ -111,9 +108,6 @@
"engines": {
"node": ">=14.19.1"
},
"overrides": {
"core-js": "3.21.1"
},
"browserslist": [
"Firefox ESR",
"not IE 11",

View File

@ -22,6 +22,7 @@
import { v4 as uuid } from 'uuid';
import EventEmitter from 'EventEmitter';
import _ from 'lodash';
/**
* @readonly
@ -42,19 +43,28 @@ 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',
@ -63,6 +73,7 @@ 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 || '';
@ -112,6 +123,7 @@ export default class AnnotationAPI extends EventEmitter {
namespace
},
tags,
_deleted: false,
annotationType,
contentText,
originalContextPath
@ -127,6 +139,7 @@ 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 {
@ -134,14 +147,32 @@ 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 => {
@ -157,18 +188,26 @@ export default class AnnotationAPI extends EventEmitter {
}
}
async getAnnotation(query, searchType) {
let foundAnnotation = null;
/**
* @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();
const searchResults = (await Promise.all(this.openmct.objects.search(query, null, searchType))).flat();
if (searchResults) {
foundAnnotation = searchResults[0];
}
return foundAnnotation;
return searchResults;
}
async addAnnotationTag(existingAnnotation, targetDomainObject, targetSpecificDetails, annotationType, tag) {
/**
* @method addSingleAnnotationTag
* @param {import('../objects/ObjectAPI').DomainObject=} existingAnnotation - An optional annotation to add the tag to. If not specified, we will create an annotation.
* @param {import('../objects/ObjectAPI').DomainObject} targetDomainObject - The domain object the annotation will point to.
* @param {Object=} targetSpecificDetails - Optional object to add to the target object. E.g., for notebooks this would be an entryID
* @param {AnnotationType} annotationType - The type of annotation this is for.
* @returns {import('../objects/ObjectAPI').DomainObject[]} Returns the annotation that was either created or passed as an existingAnnotation
*/
async addSingleAnnotationTag(existingAnnotation, targetDomainObject, targetSpecificDetails, annotationType, tag) {
if (!existingAnnotation) {
const targets = {};
const targetKeyString = this.openmct.objects.makeKeyString(targetDomainObject.identifier);
@ -186,27 +225,44 @@ export default class AnnotationAPI extends EventEmitter {
return newAnnotation;
} else {
const tagArray = [tag, ...existingAnnotation.tags];
this.openmct.objects.mutate(existingAnnotation, 'tags', tagArray);
if (!existingAnnotation.tags.includes(tag)) {
throw new Error(`Existing annotation did not contain tag ${tag}`);
}
if (existingAnnotation._deleted) {
this.unDeleteAnnotation(existingAnnotation);
}
return existingAnnotation;
}
}
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);
/**
* @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! 🙅‍♂️');
}
annotations.forEach(annotation => {
if (!annotation._deleted) {
this.openmct.objects.mutate(annotation, '_deleted', true);
}
});
}
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', []);
/**
* @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! 🙅‍♂️');
}
this.openmct.objects.mutate(annotation, '_deleted', false);
}
#getMatchingTags(query) {
@ -266,16 +322,36 @@ 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);
const searchResults = (await Promise.all(this.openmct.objects.search(matchingTagKeys, abortController, this.openmct.objects.SEARCH_TYPES.TAGS))).flat();
const appliedTagSearchResults = this.#addTagMetaInformationToResults(searchResults, matchingTagKeys);
const filteredDeletedResults = searchResults.filter((result) => {
return !(result._deleted);
});
const combinedSameTargets = this.#combineSameTargets(filteredDeletedResults);
const appliedTagSearchResults = this.#addTagMetaInformationToResults(combinedSameTargets, matchingTagKeys);
const appliedTargetsModels = await this.#addTargetModelsToResults(appliedTagSearchResults);
const resultsWithValidPath = appliedTargetsModels.filter(result => {
return this.openmct.objects.isReachable(result.targetModels?.[0]?.originalPath);

View File

@ -126,34 +126,44 @@ describe("The Annotation API", () => {
describe("Tagging", () => {
it("can create a tag", async () => {
const annotationObject = await openmct.annotation.addAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
const annotationObject = await openmct.annotation.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
expect(annotationObject).toBeDefined();
expect(annotationObject.type).toEqual('annotation');
expect(annotationObject.tags).toContain('aWonderfulTag');
});
it("can delete a tag", async () => {
const 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');
const annotationObject = await openmct.annotation.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
expect(annotationObject).toBeDefined();
openmct.annotation.removeAnnotationTag(annotationObject, 'anotherTagToRemove');
expect(annotationObject.tags).toEqual(['aWonderfulTag']);
openmct.annotation.removeAnnotationTag(annotationObject, 'aWonderfulTag');
expect(annotationObject.tags).toEqual([]);
openmct.annotation.deleteAnnotations([annotationObject]);
expect(annotationObject._deleted).toBeTrue();
});
it("throws an error if deleting non-existent tag", async () => {
const annotationObject = await openmct.annotation.addAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
const annotationObject = await openmct.annotation.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
expect(annotationObject).toBeDefined();
expect(() => {
openmct.annotation.removeAnnotationTag(annotationObject, 'ThisTagShouldNotExist');
}).toThrow();
});
it("can remove all tags", async () => {
const annotationObject = await openmct.annotation.addAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
const annotationObject = await openmct.annotation.addSingleAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
expect(annotationObject).toBeDefined();
expect(() => {
openmct.annotation.removeAnnotationTags(annotationObject);
openmct.annotation.deleteAnnotations([annotationObject]);
}).not.toThrow();
expect(annotationObject.tags).toEqual([]);
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();
});
});
@ -175,16 +185,5 @@ describe("The Annotation API", () => {
expect(results).toBeDefined();
expect(results.length).toEqual(1);
});
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.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

@ -42,7 +42,6 @@ class InMemorySearchProvider {
this.openmct = openmct;
this.indexedIds = {};
this.indexedCompositions = {};
this.indexedTags = {};
this.idsToIndex = [];
this.pendingIndex = {};
this.pendingRequests = 0;
@ -61,7 +60,6 @@ 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);
@ -93,7 +91,7 @@ class InMemorySearchProvider {
this.searchTypes = this.openmct.objects.SEARCH_TYPES;
this.supportedSearchTypes = [this.searchTypes.OBJECTS, this.searchTypes.ANNOTATIONS, this.searchTypes.NOTEBOOK_ANNOTATIONS, this.searchTypes.TAGS];
this.supportedSearchTypes = [this.searchTypes.OBJECTS, this.searchTypes.ANNOTATIONS, this.searchTypes.TAGS];
this.scheduleForIndexing(rootObject.identifier);
@ -163,8 +161,6 @@ 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 {
@ -281,13 +277,6 @@ 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,
@ -342,14 +331,6 @@ 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')) {
@ -581,43 +562,6 @@ 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,8 +43,6 @@
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}`);
}
@ -204,33 +202,4 @@
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,6 +64,15 @@ 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
@ -76,7 +85,6 @@ export default class ObjectAPI {
this.SEARCH_TYPES = Object.freeze({
OBJECTS: 'OBJECTS',
ANNOTATIONS: 'ANNOTATIONS',
NOTEBOOK_ANNOTATIONS: 'NOTEBOOK_ANNOTATIONS',
TAGS: 'TAGS'
});
this.eventEmitter = new EventEmitter();
@ -188,7 +196,6 @@ 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);
@ -223,7 +230,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);
}
@ -300,7 +307,7 @@ export default class ObjectAPI {
}
return this.get(identifier).then((object) => {
return this._toMutable(object);
return this.toMutable(object);
});
}
@ -490,7 +497,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);
@ -510,15 +517,19 @@ export default class ObjectAPI {
}
/**
* @private
* 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#
*/
_toMutable(object) {
toMutable(domainObject) {
let mutableObject;
if (object.isMutable) {
mutableObject = object;
if (domainObject.isMutable) {
mutableObject = domainObject;
} else {
mutableObject = MutableDomainObject.createMutable(object, this.eventEmitter);
mutableObject = MutableDomainObject.createMutable(domainObject, this.eventEmitter);
// Check if provider supports realtime updates
let identifier = utils.parseKeyString(mutableObject.identifier);
@ -526,9 +537,11 @@ export default class ObjectAPI {
if (provider !== undefined
&& provider.observe !== undefined
&& this.SYNCHRONIZED_OBJECT_TYPES.includes(object.type)) {
&& this.SYNCHRONIZED_OBJECT_TYPES.includes(domainObject.type)) {
let unobserve = provider.observe(identifier, (updatedModel) => {
if (updatedModel.persisted > mutableObject.modified) {
// modified can sometimes be undefined, so make it 0 in this case
const mutableObjectModification = mutableObject.modified ?? Number.MIN_SAFE_INTEGER;
if (updatedModel.persisted > mutableObjectModification) {
//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);
@ -582,7 +595,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();
@ -675,8 +688,10 @@ 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 >= domainObject.modified;
&& domainObject.persisted >= 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

@ -151,6 +151,7 @@
:key="entry.id"
:entry="entry"
:domain-object="domainObject"
:notebook-annotations="notebookAnnotations[entry.id]"
:selected-page="selectedPage"
:selected-section="selectedSection"
:read-only="false"
@ -219,10 +220,12 @@ export default {
isRestricted: this.domainObject.type === RESTRICTED_NOTEBOOK_TYPE,
search: '',
searchResults: [],
lastLocalAnnotationCreation: 0,
showTime: this.domainObject.configuration.showTime || 0,
showNav: false,
sidebarCoversEntries: false,
filteredAndSortedEntries: []
filteredAndSortedEntries: [],
notebookAnnotations: {}
};
},
computed: {
@ -289,7 +292,8 @@ export default {
this.getSearchResults = debounce(this.getSearchResults, 500);
this.syncUrlWithPageAndSection = debounce(this.syncUrlWithPageAndSection, 100);
},
mounted() {
async mounted() {
await this.loadAnnotations();
this.formatSidebar();
this.setSectionAndPageFromUrl();
@ -307,6 +311,13 @@ 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);
},
@ -338,6 +349,32 @@ 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) || [];
@ -350,6 +387,10 @@ 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 => {
@ -473,14 +514,8 @@ export default {
]
});
},
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);
removeAnnotations(entryId) {
this.openmct.annotation.deleteAnnotations(this.notebookAnnotations[entryId]);
},
checkEntryPos(entry) {
const entryPos = getEntryPosById(entry.id, this.domainObject, this.selectedSection, this.selectedPage);

View File

@ -84,9 +84,8 @@
<TagEditor
:domain-object="domainObject"
:annotation-query="annotationQuery"
:annotations="notebookAnnotations"
: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"
/>
@ -170,6 +169,12 @@ export default {
return {};
}
},
notebookAnnotations: {
type: Array,
default() {
return [];
}
},
entry: {
type: Object,
default() {
@ -216,15 +221,6 @@ 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,6 +6,7 @@
<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>
@ -33,6 +34,7 @@
<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,22 +1,23 @@
import { isNotebookType } from './notebook-constants';
import { isAnnotationType, isNotebookType, isNotebookOrAnnotationType } from './notebook-constants';
import _ from 'lodash';
export default function (openmct) {
const apiSave = openmct.objects.save.bind(openmct.objects);
openmct.objects.save = async (domainObject) => {
if (!isNotebookType(domainObject)) {
if (!isNotebookOrAnnotationType(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 = resolveConflicts(localMutable, openmct);
result = await resolveConflicts(domainObject, localMutable, openmct);
} else {
result = Promise.reject(error);
}
@ -30,16 +31,56 @@ export default function (openmct) {
};
}
function resolveConflicts(localMutable, openmct) {
const localEntries = JSON.parse(JSON.stringify(localMutable.configuration.entries));
function resolveConflicts(domainObject, localMutable, openmct) {
if (isNotebookType(domainObject)) {
return resolveNotebookEntryConflicts(localMutable, openmct);
} else if (isAnnotationType(domainObject)) {
return resolveNotebookTagConflicts(localMutable, openmct);
}
}
return openmct.objects.getMutable(localMutable.identifier).then((remoteMutable) => {
applyLocalEntries(remoteMutable, localEntries, openmct);
async function resolveNotebookTagConflicts(localAnnotation, openmct) {
const localClonedAnnotation = structuredClone(localAnnotation);
const remoteMutable = await openmct.objects.getMutable(localClonedAnnotation.identifier);
openmct.objects.destroyMutable(remoteMutable);
// 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');
}
return true;
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);
applyLocalEntries(remoteMutable, localEntries, openmct);
openmct.objects.destroyMutable(remoteMutable);
}
return true;
}
function applyLocalEntries(mutable, entries, openmct) {

View File

@ -1,5 +1,6 @@
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';
@ -9,10 +10,18 @@ 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 jsdoc to come)
* @param {*} openmct the Open MCT API (proper typescript doc 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,8 +57,9 @@ describe('the plugin', () => {
it('calculates an fps value', async () => {
await loopForABit();
// eslint-disable-next-line
expect(parseInt(performanceIndicator.text().split(' fps')[0])).toBeGreaterThan(0);
// eslint-disable-next-line radix
const fps = parseInt(performanceIndicator.text().split(' fps')[0]);
expect(fps).toBeGreaterThan(0);
});
function loopForABit() {
@ -66,7 +67,7 @@ describe('the plugin', () => {
return new Promise(resolve => {
requestAnimationFrame(function loop() {
if (++frames === 240) {
if (++frames > 90) {
resolve();
} else {
requestAnimationFrame(loop);

View File

@ -23,7 +23,7 @@
import CouchDocument from "./CouchDocument";
import CouchObjectQueue from "./CouchObjectQueue";
import { PENDING, CONNECTED, DISCONNECTED, UNKNOWN } from "./CouchStatusIndicator";
import { isNotebookType } from '../../notebook/notebook-constants.js';
import { isNotebookOrAnnotationType } from '../../notebook/notebook-constants.js';
const REV = "_rev";
const ID = "_id";
@ -71,7 +71,7 @@ class CouchObjectProvider {
}
onSharedWorkerMessageError(event) {
console.log('Error', event);
console.error('Error', event);
}
isSynchronizedObject(object) {
@ -290,7 +290,7 @@ class CouchObjectProvider {
this.objectQueue[key] = new CouchObjectQueue(undefined, response[REV]);
}
if (isNotebookType(object) || object.type === 'annotation') {
if (isNotebookOrAnnotationType(object)) {
//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.NOTEBOOK_ANNOTATIONS, this.searchTypes.TAGS];
this.supportedSearchTypes = [this.searchTypes.OBJECTS, this.searchTypes.ANNOTATIONS, this.searchTypes.TAGS];
}
supportsSearchType(searchType) {
@ -43,8 +43,6 @@ 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 {
@ -91,38 +89,6 @@ class CouchSearchProvider {
return this.couchObjectProvider.getObjectsByFilter(filter, abortSignal);
}
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": {
@ -130,7 +96,8 @@ class CouchSearchProvider {
{
"model.tags": {
"$elemMatch": {
"$eq": `${tagsArray[0]}`
"$or": [
]
}
}
},
@ -142,6 +109,11 @@ class CouchSearchProvider {
]
}
};
tagsArray.forEach(tag => {
filter.selector.$and[0]["model.tags"].$elemMatch.$or.push({
"$eq": `${tag}`
});
});
return this.couchObjectProvider.getObjectsByFilter(filter, abortSignal);
}

View File

@ -122,7 +122,7 @@
<button
class="c-button icon-reset"
title="Reset pan/zoom"
@click="clear()"
@click="resumeRealtimeData()"
>
</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="play()"
@click="resumeRealtimeData()"
>
</button>
</div>
@ -213,6 +213,8 @@ import XAxis from "./axis/XAxis.vue";
import YAxis from "./axis/YAxis.vue";
import _ from "lodash";
const OFFSET_THRESHOLD = 10;
export default {
components: {
XAxis,
@ -329,6 +331,8 @@ export default {
}
},
mounted() {
this.offsetWidth = 0;
document.addEventListener('keydown', this.handleKeyDown);
document.addEventListener('keyup', this.handleKeyUp);
eventHelpers.extend(this);
@ -576,9 +580,8 @@ export default {
};
this.config.xAxis.set('range', newRange);
if (!isTick) {
this.skipReloadOnInteraction = true;
this.clear();
this.skipReloadOnInteraction = false;
this.clearPanZoomHistory();
this.synchronizeIfBoundsMatch();
this.loadMoreData(newRange, true);
} else {
// If we're not panning or zooming (time conductor and plot x-axis times are not out of sync)
@ -601,16 +604,20 @@ export default {
/**
* Handle end of user viewport change: load more data for current display
* bounds, and mark view as synchronized if bounds match configured bounds.
* bounds, and mark view as synchronized if necessary.
*/
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);
},
@ -839,7 +846,8 @@ export default {
// needs to follow endMarquee so that plotHistory is pruned
const isAction = Boolean(this.plotHistory.length);
if (!isAction && !this.isFrozenOnMouseDown) {
return this.play();
this.clearPanZoomHistory();
this.synchronizeIfBoundsMatch();
}
},
@ -1076,18 +1084,22 @@ export default {
this.setStatus();
},
clear() {
resumeRealtimeData() {
this.clearPanZoomHistory();
this.userViewportChangeEnd();
},
clearPanZoomHistory() {
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.clear();
this.resumeRealtimeData();
return;
}
@ -1105,10 +1117,6 @@ export default {
this.freeze();
},
play() {
this.clear();
},
showSynchronizeDialog() {
const isLocalClock = this.timeContext.clock();
if (isLocalClock !== undefined) {
@ -1172,7 +1180,9 @@ export default {
this.removeStatusListener();
}
this.plotContainerResizeObserver.disconnect();
if (this.plotContainerResizeObserver) {
this.plotContainerResizeObserver.disconnect();
}
this.stopFollowingTimeContext();
this.openmct.objectViews.off('clearData', this.clearData);
@ -1181,9 +1191,12 @@ 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
&& (this.offsetWidth !== this.$parent.$refs.plotWrapper.offsetWidth)) {
this.offsetWidth = this.$parent.$refs.plotWrapper.offsetWidth;
&& offsetChange > OFFSET_THRESHOLD) {
this.offsetWidth = newOffsetWidth;
this.config.series.models.forEach(this.loadSeriesData, this);
}
},

View File

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

View File

@ -144,12 +144,6 @@ 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
});
@ -166,7 +160,7 @@ describe("the plugin", function () {
afterEach((done) => {
openmct.time.timeSystem('utc', {
start: 0,
end: 1
end: 2
});
configStore.deleteAll();
@ -506,6 +500,23 @@ 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', () => {
@ -528,6 +539,94 @@ 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

@ -144,10 +144,11 @@ export default {
this.initializeHistoryIfNoHistory();
},
getHistoryMenuItems() {
const descriptionDateFormat = 'YYYY-MM-DD HH:mm:ss.SSS';
const history = this.historyForCurrentTimeSystem.map(timespan => {
let name;
let startTime = this.formatTime(timespan.start);
let description = `${startTime} - ${this.formatTime(timespan.end)}`;
const startTime = this.formatTime(timespan.start);
const description = `${this.formatTime(timespan.start, descriptionDateFormat)} - ${this.formatTime(timespan.end, descriptionDateFormat)}`;
if (this.timeSystem.isUTCBased && !this.openmct.time.clock()) {
name = `${startTime} ${millisecondsToDHMS(timespan.end - timespan.start)}`;
@ -253,7 +254,7 @@ export default {
return maxRecordsLength;
},
formatTime(time) {
formatTime(time, utcDateFormat) {
let format = this.timeSystem.timeFormat;
let isNegativeOffset = false;
@ -274,7 +275,8 @@ export default {
let formattedDate;
if (formatter instanceof UTCTimeFormat) {
formattedDate = formatter.format(time, formatter.DATE_FORMATS.PRECISION_SECONDS);
const formatString = formatter.isValidFormatString(utcDateFormat) ? utcDateFormat : formatter.DATE_FORMATS.PRECISION_SECONDS;
formattedDate = formatter.format(time, formatString);
} else {
formattedDate = formatter.format(time);
}

View File

@ -244,7 +244,7 @@ define(
if (context.item && context.item.isMutable !== true) {
removeMutable = true;
context.item = this.openmct.objects._toMutable(context.item);
context.item = this.openmct.objects.toMutable(context.item);
}
if (select) {

View File

@ -51,18 +51,14 @@ export default {
},
inject: ['openmct'],
props: {
annotationQuery: {
type: Object,
annotations: {
type: Array,
required: true
},
annotationType: {
type: String,
required: true
},
annotationSearchType: {
type: String,
required: true
},
targetSpecificDetails: {
type: Object,
required: true
@ -76,7 +72,6 @@ export default {
},
data() {
return {
annontation: null,
addedTags: [],
userAddingTag: false
};
@ -92,57 +87,50 @@ export default {
}
},
watch: {
annotation: {
annotations: {
handler() {
this.tagsChanged(this.annotation.tags);
},
deep: true
},
annotationQuery: {
handler() {
this.unloadAnnotation();
this.loadAnnotation();
this.annotationsChanged();
},
deep: true
}
},
mounted() {
this.loadAnnotation();
},
destroyed() {
if (this.removeTagsListener) {
this.removeTagsListener();
}
this.annotationsChanged();
},
methods: {
addAnnotationListener(annotation) {
if (annotation && !this.removeTagsListener) {
this.removeTagsListener = this.openmct.objects.observe(annotation, '*', (newAnnotation) => {
this.tagsChanged(newAnnotation.tags);
this.annotation = newAnnotation;
});
annotationsChanged() {
if (this.annotations && this.annotations.length) {
this.tagsChanged();
}
},
async loadAnnotation() {
this.annotation = await this.openmct.annotation.getAnnotation(this.annotationQuery, this.annotationSearchType);
this.addAnnotationListener(this.annotation);
if (this.annotation && this.annotation.tags) {
this.tagsChanged(this.annotation.tags);
annotationDeletionListener(changedAnnotation) {
const matchingAnnotation = this.annotations.find((possibleMatchingAnnotation) => {
return this.openmct.objects.areIdsEqual(possibleMatchingAnnotation.identifier, changedAnnotation.identifier);
});
if (matchingAnnotation) {
matchingAnnotation._deleted = changedAnnotation._deleted;
this.userAddingTag = false;
this.tagsChanged();
}
},
unloadAnnotation() {
if (this.removeTagsListener) {
this.removeTagsListener();
this.removeTagsListener = undefined;
}
},
tagsChanged(newTags) {
if (newTags.length < this.addedTags.length) {
this.addedTags = this.addedTags.slice(0, newTags.length);
tagsChanged() {
// gather tags from annotations
const tagsFromAnnotations = this.annotations.flatMap((annotation) => {
if (annotation._deleted) {
return [];
} else {
return annotation.tags;
}
}).filter((tag, index, array) => {
return array.indexOf(tag) === index;
});
if (tagsFromAnnotations.length !== this.addedTags.length) {
this.addedTags = this.addedTags.slice(0, tagsFromAnnotations.length);
}
for (let index = 0; index < newTags.length; index += 1) {
this.$set(this.addedTags, index, newTags[index]);
for (let index = 0; index < tagsFromAnnotations.length; index += 1) {
this.$set(this.addedTags, index, tagsFromAnnotations[index]);
}
},
addTag() {
@ -153,23 +141,27 @@ export default {
this.userAddingTag = true;
},
async tagRemoved(tagToRemove) {
const result = await this.openmct.annotation.removeAnnotationTag(this.annotation, tagToRemove);
this.$emit('tags-updated');
// Soft delete annotations that match tag instead
const annotationsToDelete = this.annotations.filter((annotation) => {
return annotation.tags.includes(tagToRemove);
});
const result = await this.openmct.annotation.deleteAnnotations(annotationsToDelete);
this.$emit('tags-updated', annotationsToDelete);
return result;
},
async tagAdded(newTag) {
const annotationWasCreated = this.annotation === null || this.annotation === undefined;
this.annotation = await this.openmct.annotation.addAnnotationTag(this.annotation,
this.domainObject, this.targetSpecificDetails, this.annotationType, newTag);
if (annotationWasCreated) {
this.addAnnotationListener(this.annotation);
}
// Either undelete an annotation, or create one (1) new annotation
const existingAnnotation = this.annotations.find((annotation) => {
return annotation.tags.includes(newTag);
});
const createdAnnotation = await this.openmct.annotation.addSingleAnnotationTag(existingAnnotation,
this.domainObject, this.targetSpecificDetails, this.annotationType, newTag);
this.tagsChanged(this.annotation.tags);
this.userAddingTag = false;
this.$emit('tags-updated');
this.$emit('tags-updated', createdAnnotation);
}
}
};

View File

@ -37,7 +37,10 @@
class="c-tag"
:style="{ background: selectedBackgroundColor, color: selectedForegroundColor }"
>
<div class="c-tag__label">{{ selectedTagLabel }} </div>
<div
class="c-tag__label"
aria-label="Tag"
>{{ selectedTagLabel }} </div>
<button
class="c-completed-tag-deletion c-tag__remove-btn icon-x-in-circle"
@click="removeTag"

View File

@ -1,5 +1,13 @@
/* global __dirname */
/* global __dirname module */
/*
This is the OpenMCT common webpack file. It is imported by the other three webpack configurations:
- webpack.prod.js - the production configuration for OpenMCT (default)
- webpack.dev.js - the development configuration for OpenMCT
- webpack.coverage.js - imports webpack.dev.js and adds code coverage
There are separate npm scripts to use these configurations, though simply running `npm install`
will use the default production configuration.
*/
const path = require('path');
const packageDefinition = require('./package.json');
const CopyWebpackPlugin = require('copy-webpack-plugin');
@ -140,8 +148,13 @@ const config = {
}
]
},
stats: 'errors-warnings'
stats: 'errors-warnings',
performance: {
// We should eventually consider chunking to decrease
// these values
maxEntrypointSize: 25000000,
maxAssetSize: 25000000
}
};
// eslint-disable-next-line no-undef
module.exports = config;

View File

@ -1,34 +1,21 @@
// This file extends the webpack.dev.js config to add istanbul coverage
// instrumentation using babel-plugin-istanbul (see babel.coverage.js)
/* global module */
/*
This file extends the webpack.dev.js config to add babel istanbul coverage.
OpenMCT Continuous Integration servers use this configuration to add code coverage
information to pull requests.
*/
const config = require('./webpack.dev');
const vueLoaderRule = config.module.rules.find(r => r.use === 'vue-loader');
// eslint-disable-next-line no-undef
const CI = process.env.CI === 'true';
config.devtool = CI ? false : undefined;
vueLoaderRule.use = {
loader: 'vue-loader'
// Attempt to use Babel with babel-plugin-istanbul
// TODO The purpose of this was to try to add coverage to JS expressions
// inside `<template>` markup, but it seems to add only coverage inside
// `<script>` tags.
// Issue: https://github.com/nasa/openmct/issues/4973
//
// options: {
// compiler: require('vue-template-babel-compiler'),
// compilerOptions: {
// babelOptions: require('./babel.coverage')
// }
// }
};
config.devServer.hot = false;
config.module.rules.push({
test: /\.js$/,
// test: /(\.js$)|(\?vue&type=template)/,
// exclude: /node_modules(?!.*\.vue)/,
exclude: /(Spec\.js$)|(node_modules)/,
use: {
loader: 'babel-loader',

View File

@ -1,3 +1,10 @@
/* global __dirname module */
/*
This configuration should be used for development purposes. It contains full source map, a
devServer (which be invoked using by `npm start`), and a non-minified Vue.js distribution.
If OpenMCT is to be used for a production server, use webpack.prod.js instead.
*/
const { merge } = require('webpack-merge');
const common = require('./webpack.common');
@ -12,7 +19,7 @@ module.exports = merge(common, {
// See: https://webpack.js.org/configuration/watch/#watchoptions-exclude
ignored: [
'**/{node_modules,dist,docs,e2e}', // All files in node_modules, dist, docs, e2e,
'**/{*.yml,Procfile,webpack*.js,babel*.js,package*.json,tsconfig.json,jsdoc.json}', // Config files
'**/{*.yml,Procfile,webpack*.js,babel*.js,package*.json,tsconfig.json}', // Config files
'**/*.{sh,md,png,ttf,woff,svg}', // Non source files
'**/.*' // dotfiles and dotfolders
]
@ -27,5 +34,15 @@ module.exports = merge(common, {
__OPENMCT_ROOT_RELATIVE__: '"dist/"'
})
],
devtool: 'eval-source-map'
devtool: 'eval-source-map',
devServer: {
static: {
directory: path.join(__dirname, '/dist/'),
publicPath: '/dist'
},
client: {
progress: true,
overlay: true
}
}
});

View File

@ -1,3 +1,9 @@
/* global __dirname module */
/*
This configuration should be used for production installs.
It is the default webpack configuration.
*/
const { merge } = require('webpack-merge');
const common = require('./webpack.common');