Compare commits
71 Commits
sprint-2.1
...
user-attri
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f4b38a061 | ||
|
|
d402162aed | ||
|
|
9e450892a7 | ||
|
|
feba5f6d3b | ||
|
|
4357d35f4a | ||
|
|
b06aaa7761 | ||
|
|
880547e9c0 | ||
|
|
62bfa44f79 | ||
|
|
5041f80e5b | ||
|
|
f59270c6b0 | ||
|
|
9e23f79bc8 | ||
|
|
bd1e869f6a | ||
|
|
e4a36532e7 | ||
|
|
2bc2316613 | ||
|
|
2fa36b2176 | ||
|
|
42aee096d2 | ||
|
|
033a45d600 | ||
|
|
efa38d779e | ||
|
|
6c5510f385 | ||
|
|
951cc6ec0d | ||
|
|
ef4b8a9934 | ||
|
|
c14b48917e | ||
|
|
26165d0a99 | ||
|
|
b2fabb9f00 | ||
|
|
b91176c331 | ||
|
|
f7cf3f72c2 | ||
|
|
cb8e09c9f9 | ||
|
|
026eb86f5f | ||
|
|
866859a937 | ||
|
|
afc54f41f6 | ||
|
|
72c980f991 | ||
|
|
4decb1ed00 | ||
|
|
9bf39a9cd4 | ||
|
|
fb0ea03378 | ||
|
|
33fd95cb2b | ||
|
|
8c92178895 | ||
|
|
89e5891192 | ||
|
|
1c00f1a731 | ||
|
|
35bbebbbc7 | ||
|
|
bd0217498e | ||
|
|
d9181e5c4c | ||
|
|
b7ed6619d2 | ||
|
|
212fa16f59 | ||
|
|
1a8d35fd0c | ||
|
|
caa27dbda2 | ||
|
|
4e48ce3872 | ||
|
|
ce463babff | ||
|
|
27c30132d2 | ||
|
|
b8be778b5a | ||
|
|
2bdac56505 | ||
|
|
35c42ba43d | ||
|
|
69a89037ab | ||
|
|
04f5684fa8 | ||
|
|
f05eb58f13 | ||
|
|
3d9f175234 | ||
|
|
f5ec58acf4 | ||
|
|
6bdb8c9e1c | ||
|
|
4a5467aba7 | ||
|
|
b85238d7d0 | ||
|
|
50c7e99bd3 | ||
|
|
888b7fcf90 | ||
|
|
f72daa7ce1 | ||
|
|
aea942ff73 | ||
|
|
7304a31fc3 | ||
|
|
d93da11129 | ||
|
|
f0b2936229 | ||
|
|
fc9663804b | ||
|
|
2e917aedfd | ||
|
|
b3820fbf46 | ||
|
|
0b7ea7cb1b | ||
|
|
f430505f88 |
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -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)
|
||||
|
||||
1
.github/codeql/codeql-config.yml
vendored
Normal file
1
.github/codeql/codeql-config.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
name: 'Custom CodeQL config'
|
||||
12
.github/dependabot.yml
vendored
12
.github/dependabot.yml
vendored
@@ -13,14 +13,12 @@ updates:
|
||||
- "pr:daveit"
|
||||
- "pr:platform"
|
||||
ignore:
|
||||
#We have to source the container which is not detected by Dependabot
|
||||
- dependency-name: "@playwright/test"
|
||||
#Lots of noise in these type patch releases.
|
||||
- dependency-name: "@babel/eslint-parser"
|
||||
- dependency-name: "@playwright/test" #We have to source the playwright container which is not detected by Dependabot
|
||||
- dependency-name: "playwright-core" #We have to source the playwright container which is not detected by Dependabot
|
||||
- dependency-name: "@babel/eslint-parser" #Lots of noise in these type patch releases.
|
||||
update-types: ["version-update:semver-patch"]
|
||||
- dependency-name: "eslint-plugin-vue" #Lots of noise in these type patch releases.
|
||||
update-types: ["version-update:semver-patch"]
|
||||
- dependency-name: "eslint-plugin-vue"
|
||||
update-types: ["version-update:semver-patch"]
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
|
||||
31
.github/workflows/codeql-analysis.yml
vendored
31
.github/workflows/codeql-analysis.yml
vendored
@@ -1,11 +1,10 @@
|
||||
|
||||
name: "CodeQL"
|
||||
name: 'CodeQL'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
branches: [master, 'release/*']
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
branches: [master, 'release/*']
|
||||
paths-ignore:
|
||||
- '**/*Spec.js'
|
||||
- '**/*.md'
|
||||
@@ -27,17 +26,19 @@ jobs:
|
||||
security-events: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: javascript
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
config-file: ./.github/codeql/codeql-config.yml
|
||||
languages: javascript
|
||||
queries: security-and-quality
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
|
||||
98
.github/workflows/lighthouse.yml
vendored
98
.github/workflows/lighthouse.yml
vendored
@@ -1,98 +0,0 @@
|
||||
name: lighthouse
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Which branch do you want to test?' # Limited to branch for now
|
||||
required: false
|
||||
default: 'master'
|
||||
pull_request:
|
||||
types:
|
||||
- labeled
|
||||
jobs:
|
||||
lighthouse-pr:
|
||||
if: ${{ github.event.label.name == 'pr:lighthouse' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Master for Baseline
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: master #explicitly checkout master for baseline
|
||||
- name: Install Node 16
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v2
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
|
||||
- name: npm install with lighthouse cli
|
||||
run: npm install && npm install -g @lhci/cli
|
||||
- name: Run lhci against master to generate baseline and ignore exit codes
|
||||
run: lhci autorun || true
|
||||
- name: Perform clean checkout of PR
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: true
|
||||
- name: Install Node version which is compatible with PR
|
||||
uses: actions/setup-node@v3
|
||||
- name: npm install with lighthouse cli
|
||||
run: npm install && npm install -g @lhci/cli
|
||||
- name: Run lhci with PR
|
||||
run: lhci autorun
|
||||
env:
|
||||
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
|
||||
lighthouse-nightly:
|
||||
if: ${{ github.event.schedule }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Node 16
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v2
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
|
||||
- name: npm install with lighthouse cli
|
||||
run: npm install && npm install -g @lhci/cli
|
||||
- name: Run lhci against master to generate baseline
|
||||
run: lhci autorun
|
||||
env:
|
||||
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
|
||||
lighthouse-dispatch:
|
||||
if: ${{ github.event.workflow_dispatch }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.inputs.version }}
|
||||
- name: Install Node 14
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v3
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
|
||||
- name: npm install with lighthouse cli
|
||||
run: npm install && npm install -g @lhci/cli
|
||||
- name: Run lhci against master to generate baseline
|
||||
run: lhci autorun
|
||||
|
||||
@@ -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
7
API.md
@@ -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
|
||||
|
||||
|
||||
12
README.md
12
README.md
@@ -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
|
||||
|
||||
@@ -100,7 +100,7 @@ To run the performance tests:
|
||||
The test suite is configured to all tests localed in `e2e/tests/` ending in `*.e2e.spec.js`. For more about the e2e test suite, please see the [README](./e2e/README.md)
|
||||
|
||||
### Security Tests
|
||||
Each commit is analyzed for known security vulnerabilities using [CodeQL](https://codeql.github.com/docs/codeql-language-guides/codeql-library-for-javascript/) and our overall security report is available on [LGTM](https://lgtm.com/projects/g/nasa/openmct/)
|
||||
Each commit is analyzed for known security vulnerabilities using [CodeQL](https://codeql.github.com/docs/codeql-language-guides/codeql-library-for-javascript/) and our overall security report is available on [LGTM](https://lgtm.com/projects/g/nasa/openmct/). The list of CWE coverage items is avaiable in the [CodeQL docs](https://codeql.github.com/codeql-query-help/javascript-cwe/). The CodeQL workflow is specified in the [CodeQL analysis file](./.github/workflows/codeql-analysis.yml) and the custom [CodeQL config](./.github/codeql/codeql-config.yml).
|
||||
|
||||
### Test Reporting and Code Coverage
|
||||
|
||||
|
||||
92
app.js
92
app.js
@@ -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);
|
||||
});
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<hr>
|
||||
</body>
|
||||
</html>
|
||||
209
docs/gendocs.js
209
docs/gendocs.js
@@ -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\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));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
}());
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -225,15 +225,14 @@ async function getHashUrlToDomainObject(page, uuid) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Utilizes the OpenMCT API to detect if the given object has an active transaction (is in Edit mode).
|
||||
* Utilizes the OpenMCT API to detect if the UI is in Edit mode.
|
||||
* @private
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string | import('../src/api/objects/ObjectAPI').Identifier} identifier
|
||||
* @return {Promise<boolean>} true if the object has an active transaction, false otherwise
|
||||
* @return {Promise<boolean>} true if the Open MCT is in Edit Mode
|
||||
*/
|
||||
async function _isInEditMode(page, identifier) {
|
||||
// eslint-disable-next-line no-return-await
|
||||
return await page.evaluate((objectIdentifier) => window.openmct.objects.isTransactionActive(objectIdentifier), identifier);
|
||||
return await page.evaluate(() => window.openmct.editor.isEditing());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
2207
e2e/test-data/ExampleLayouts.json
Normal file
2207
e2e/test-data/ExampleLayouts.json
Normal file
File diff suppressed because one or more lines are too long
@@ -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');
|
||||
|
||||
@@ -100,7 +100,7 @@ test.describe("CouchDB initialization @couchdb", () => {
|
||||
expect(bannerMessage).toEqual('Failed to retrieve object mine');
|
||||
|
||||
// Verify that a PUT request to create "My Items" folder was made
|
||||
expect.poll(() => createMineFolderRequests.length, {
|
||||
await expect.poll(() => createMineFolderRequests.length, {
|
||||
message: 'Verify that PUT request to create "mine" folder was made',
|
||||
timeout: 1000
|
||||
}).toBeGreaterThanOrEqual(1);
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
This test suite is dedicated to tests which verify the basic operations surrounding Notebooks with CouchDB.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../../../baseFixtures');
|
||||
const { createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||
|
||||
test.describe('Notebook Tests with CouchDB @couchdb', () => {
|
||||
let testNotebook;
|
||||
test.beforeEach(async ({ page }) => {
|
||||
//Navigate to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// Create Notebook
|
||||
testNotebook = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Notebook',
|
||||
name: "TestNotebook"
|
||||
});
|
||||
});
|
||||
|
||||
test('Inspect Notebook Entry Network Requests', async ({ page }) => {
|
||||
// Expand sidebar
|
||||
await page.locator('.c-notebook__toggle-nav-button').click();
|
||||
|
||||
// Collect all request events to count and assert after notebook action
|
||||
let addingNotebookElementsRequests = [];
|
||||
page.on('request', (request) => addingNotebookElementsRequests.push(request));
|
||||
|
||||
let [notebookUrlRequest, allDocsRequest] = await Promise.all([
|
||||
// Waits for the next request with the specified url
|
||||
page.waitForRequest(`**/openmct/${testNotebook.uuid}`),
|
||||
page.waitForRequest('**/openmct/_all_docs?include_docs=true'),
|
||||
// Triggers the request
|
||||
page.click('[aria-label="Add Page"]'),
|
||||
// Ensures that there are no other network requests
|
||||
page.waitForLoadState('networkidle')
|
||||
]);
|
||||
// Assert that only two requests are made
|
||||
// Network Requests are:
|
||||
// 1) The actual POST to create the page
|
||||
// 2) The shared worker event from 👆 request
|
||||
expect(addingNotebookElementsRequests.length).toBe(2);
|
||||
|
||||
// Assert on request object
|
||||
expect(notebookUrlRequest.postDataJSON().metadata.name).toBe('TestNotebook');
|
||||
expect(notebookUrlRequest.postDataJSON().model.persisted).toBeGreaterThanOrEqual(notebookUrlRequest.postDataJSON().model.modified);
|
||||
expect(allDocsRequest.postDataJSON().keys).toContain(testNotebook.uuid);
|
||||
|
||||
// Add an entry
|
||||
// Network Requests are:
|
||||
// 1) The actual POST to create the entry
|
||||
// 2) The shared worker event from 👆 POST request
|
||||
addingNotebookElementsRequests = [];
|
||||
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"]').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"]').fill(`First Entry`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
expect(addingNotebookElementsRequests.length).toBeLessThanOrEqual(2);
|
||||
|
||||
// Add some tags
|
||||
// Network Requests are for each tag creation are:
|
||||
// 1) Getting the original path of the parent object
|
||||
// 2) Getting the original path of the grandparent object (recursive call)
|
||||
// 3) Creating the annotation/tag object
|
||||
// 4) The shared worker event from 👆 POST request
|
||||
// 5) Mutate notebook domain object's annotationModified property
|
||||
// 6) The shared worker event from 👆 POST request
|
||||
// 7) Notebooks fetching new annotations due to annotationModified changed
|
||||
// 8) The update of the notebook domain's object's modified property
|
||||
// 9) The shared worker event from 👆 POST request
|
||||
// 10) Entry is timestamped
|
||||
// 11) The shared worker event from 👆 POST request
|
||||
|
||||
addingNotebookElementsRequests = [];
|
||||
await page.hover(`button:has-text("Add Tag")`);
|
||||
await page.locator(`button:has-text("Add Tag")`).click();
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click();
|
||||
await page.waitForSelector('[aria-label="Tag"]:has-text("Driving")');
|
||||
page.waitForLoadState('networkidle');
|
||||
expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(11);
|
||||
|
||||
addingNotebookElementsRequests = [];
|
||||
await page.hover(`button:has-text("Add Tag")`);
|
||||
await page.locator(`button:has-text("Add Tag")`).click();
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
await page.locator('[aria-label="Autocomplete Options"] >> text=Drilling').click();
|
||||
await page.waitForSelector('[aria-label="Tag"]:has-text("Drilling")');
|
||||
page.waitForLoadState('networkidle');
|
||||
expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(11);
|
||||
|
||||
addingNotebookElementsRequests = [];
|
||||
await page.hover(`button:has-text("Add Tag")`);
|
||||
await page.locator(`button:has-text("Add Tag")`).click();
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
await page.locator('[aria-label="Autocomplete Options"] >> text=Science').click();
|
||||
await page.waitForSelector('[aria-label="Tag"]:has-text("Science")');
|
||||
page.waitForLoadState('networkidle');
|
||||
expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(11);
|
||||
|
||||
// Delete all the tags
|
||||
// Network requests are:
|
||||
// 1) Send POST to mutate _delete property to true on annotation with tag
|
||||
// 2) The shared worker event from 👆 POST request
|
||||
// 3) Timestamp update on entry
|
||||
// 4) The shared worker event from 👆 POST request
|
||||
// This happens for 3 tags so 12 requests
|
||||
addingNotebookElementsRequests = [];
|
||||
await page.hover('[aria-label="Tag"]:has-text("Driving")');
|
||||
await page.locator('[aria-label="Tag"]:has-text("Driving") ~ .c-completed-tag-deletion').click();
|
||||
await page.waitForSelector('[aria-label="Tag"]:has-text("Driving")', {state: 'hidden'});
|
||||
await page.hover('[aria-label="Tag"]:has-text("Drilling")');
|
||||
await page.locator('[aria-label="Tag"]:has-text("Drilling") ~ .c-completed-tag-deletion').click();
|
||||
await page.waitForSelector('[aria-label="Tag"]:has-text("Drilling")', {state: 'hidden'});
|
||||
page.hover('[aria-label="Tag"]:has-text("Science")');
|
||||
await page.locator('[aria-label="Tag"]:has-text("Science") ~ .c-completed-tag-deletion').click();
|
||||
await page.waitForSelector('[aria-label="Tag"]:has-text("Science")', {state: 'hidden'});
|
||||
page.waitForLoadState('networkidle');
|
||||
expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(12);
|
||||
|
||||
// Add two more pages
|
||||
await page.click('[aria-label="Add Page"]');
|
||||
await page.click('[aria-label="Add Page"]');
|
||||
|
||||
// Add three entries
|
||||
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"]').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"]').fill(`First Entry`);
|
||||
|
||||
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"] >> nth=1').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"] >> nth=1').fill(`Second Entry`);
|
||||
|
||||
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"] >> nth=2').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"] >> nth=2').fill(`Third Entry`);
|
||||
|
||||
// Add three tags
|
||||
await page.hover(`button:has-text("Add Tag") >> nth=2`);
|
||||
await page.locator(`button:has-text("Add Tag") >> nth=2`).click();
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
await page.locator('[aria-label="Autocomplete Options"] >> text=Science').click();
|
||||
await page.waitForSelector('[aria-label="Tag"]:has-text("Science")');
|
||||
|
||||
await page.hover(`button:has-text("Add Tag") >> nth=2`);
|
||||
await page.locator(`button:has-text("Add Tag") >> nth=2`).click();
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
await page.locator('[aria-label="Autocomplete Options"] >> text=Drilling').click();
|
||||
await page.waitForSelector('[aria-label="Tag"]:has-text("Drilling")');
|
||||
|
||||
await page.hover(`button:has-text("Add Tag") >> nth=2`);
|
||||
await page.locator(`button:has-text("Add Tag") >> nth=2`).click();
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click();
|
||||
await page.waitForSelector('[aria-label="Tag"]:has-text("Driving")');
|
||||
page.waitForLoadState('networkidle');
|
||||
|
||||
// Add a fourth entry
|
||||
// Network requests are:
|
||||
// 1) Send POST to add new entry
|
||||
// 2) The shared worker event from 👆 POST request
|
||||
// 3) Timestamp update on entry
|
||||
// 4) The shared worker event from 👆 POST request
|
||||
addingNotebookElementsRequests = [];
|
||||
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"] >> nth=3').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"] >> nth=3').fill(`Fourth Entry`);
|
||||
await page.locator('[aria-label="Notebook Entry Input"] >> nth=3').press('Enter');
|
||||
page.waitForLoadState('networkidle');
|
||||
|
||||
expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(4);
|
||||
|
||||
// Add a fifth entry
|
||||
// Network requests are:
|
||||
// 1) Send POST to add new entry
|
||||
// 2) The shared worker event from 👆 POST request
|
||||
// 3) Timestamp update on entry
|
||||
// 4) The shared worker event from 👆 POST request
|
||||
addingNotebookElementsRequests = [];
|
||||
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"] >> nth=4').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"] >> nth=4').fill(`Fifth Entry`);
|
||||
await page.locator('[aria-label="Notebook Entry Input"] >> nth=4').press('Enter');
|
||||
page.waitForLoadState('networkidle');
|
||||
|
||||
expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(4);
|
||||
|
||||
// Add a sixth entry
|
||||
// 1) Send POST to add new entry
|
||||
// 2) The shared worker event from 👆 POST request
|
||||
// 3) Timestamp update on entry
|
||||
// 4) The shared worker event from 👆 POST request
|
||||
addingNotebookElementsRequests = [];
|
||||
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"] >> nth=5').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"] >> nth=5').fill(`Sixth Entry`);
|
||||
await page.locator('[aria-label="Notebook Entry Input"] >> nth=5').press('Enter');
|
||||
page.waitForLoadState('networkidle');
|
||||
|
||||
expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(4);
|
||||
});
|
||||
|
||||
test('Search tests', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/akhenry/openmct-yamcs/issues/69'
|
||||
});
|
||||
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"]').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"]').fill(`First Entry`);
|
||||
await page.locator('[aria-label="Notebook Entry Input"]').press('Enter');
|
||||
|
||||
// Add three tags
|
||||
await page.hover(`button:has-text("Add Tag")`);
|
||||
await page.locator(`button:has-text("Add Tag")`).click();
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
await page.locator('[aria-label="Autocomplete Options"] >> text=Science').click();
|
||||
await page.waitForSelector('[aria-label="Tag"]:has-text("Science")');
|
||||
|
||||
await page.hover(`button:has-text("Add Tag")`);
|
||||
await page.locator(`button:has-text("Add Tag")`).click();
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
await page.locator('[aria-label="Autocomplete Options"] >> text=Drilling').click();
|
||||
await page.waitForSelector('[aria-label="Tag"]:has-text("Drilling")');
|
||||
|
||||
await page.hover(`button:has-text("Add Tag")`);
|
||||
await page.locator(`button:has-text("Add Tag")`).click();
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click();
|
||||
await page.waitForSelector('[aria-label="Tag"]:has-text("Driving")');
|
||||
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Sc');
|
||||
await expect(page.locator('[aria-label="Search Result"]').first()).toContainText("Science");
|
||||
await expect(page.locator('[aria-label="Search Result"]').first()).not.toContainText("Driving");
|
||||
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Xq');
|
||||
await expect(page.locator('text=No results found')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
// Try to reduce indeterminism of browser requests by only returning fetch requests.
|
||||
// Filter out preflight CORS, fetching stylesheets, page icons, etc. that can occur during tests
|
||||
function filterNonFetchRequests(requests) {
|
||||
return requests.filter(request => {
|
||||
return (request.resourceType() === 'fetch');
|
||||
});
|
||||
}
|
||||
@@ -39,7 +39,7 @@ async function createNotebookAndEntry(page, iterations = 1) {
|
||||
createDomainObjectWithDefaults(page, { type: 'Notebook' });
|
||||
|
||||
for (let iteration = 0; iteration < iterations; iteration++) {
|
||||
// Click text=To start a new entry, click here or drag and drop any object
|
||||
// Create an entry
|
||||
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
|
||||
const entryLocator = `[aria-label="Notebook Entry Input"] >> nth = ${iteration}`;
|
||||
await page.locator(entryLocator).click();
|
||||
@@ -81,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,43 +104,56 @@ 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();
|
||||
await expect(page.locator('text=No results found')).toBeVisible();
|
||||
});
|
||||
|
||||
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");
|
||||
});
|
||||
|
||||
test('Can delete entries without tags', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/5823'
|
||||
});
|
||||
|
||||
await createNotebookEntryAndTags(page);
|
||||
|
||||
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
|
||||
const entryLocator = `[aria-label="Notebook Entry Input"] >> nth = 1`;
|
||||
await page.locator(entryLocator).click();
|
||||
await page.locator(entryLocator).fill(`An entry without tags`);
|
||||
await page.locator('[aria-label="Notebook Entry Input"] >> nth=1').press('Enter');
|
||||
|
||||
await page.hover('[aria-label="Notebook Entry Input"] >> nth=1');
|
||||
await page.locator('button[title="Delete this entry"]').last().click();
|
||||
await expect(page.locator('text=This action will permanently delete this entry. Do you wish to continue?')).toBeVisible();
|
||||
await page.locator('button:has-text("Ok")').click();
|
||||
await expect(page.locator('text=This action will permanently delete this entry. Do you wish to continue?')).toBeHidden();
|
||||
});
|
||||
|
||||
test('Can delete objects with tags and neither return in search', async ({ page }) => {
|
||||
await createNotebookEntryAndTags(page);
|
||||
// Delete Notebook
|
||||
@@ -153,7 +162,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');
|
||||
|
||||
54
e2e/tests/functional/plugins/plot/plotRendering.e2e.spec.js
Normal file
54
e2e/tests/functional/plugins/plot/plotRendering.e2e.spec.js
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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.
|
||||
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
12
jsdoc.json
12
jsdoc.json
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"source": {
|
||||
"include": [
|
||||
"src/"
|
||||
],
|
||||
"includePattern": "src/.+\\.js$",
|
||||
"excludePattern": ".+\\Spec\\.js$|lib/.+"
|
||||
},
|
||||
"plugins": [
|
||||
"plugins/markdown"
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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'
|
||||
51
package.json
51
package.json
@@ -1,35 +1,30 @@
|
||||
{
|
||||
"name": "openmct",
|
||||
"version": "2.1.0-SNAPSHOT",
|
||||
"version": "2.1.2-SNAPSHOT",
|
||||
"description": "The Open MCT core platform",
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "7.18.9",
|
||||
"@braintree/sanitize-url": "6.0.0",
|
||||
"@percy/cli": "1.10.3",
|
||||
"@percy/cli": "1.11.0",
|
||||
"@percy/playwright": "1.0.4",
|
||||
"@playwright/test": "1.25.2",
|
||||
"@types/eventemitter3": "^1.0.0",
|
||||
"@types/jasmine": "^4.0.1",
|
||||
"@types/karma": "^6.3.2",
|
||||
"@types/lodash": "^4.14.178",
|
||||
"@types/mocha": "^9.1.0",
|
||||
"@types/jasmine": "4.3.0",
|
||||
"@types/lodash": "4.14.186",
|
||||
"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.25.0",
|
||||
"eslint-plugin-compat": "4.0.2",
|
||||
"eslint-plugin-playwright": "0.11.1",
|
||||
"eslint-plugin-vue": "9.3.0",
|
||||
"eslint-plugin-playwright": "0.11.2",
|
||||
"eslint-plugin-vue": "9.6.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,14 +46,14 @@
|
||||
"moment": "2.29.4",
|
||||
"moment-duration-format": "2.3.2",
|
||||
"moment-timezone": "0.5.37",
|
||||
"nyc":"15.1.0",
|
||||
"nyc": "15.1.0",
|
||||
"painterro": "1.2.78",
|
||||
"playwright-core": "1.25.2",
|
||||
"plotly.js-basic-dist": "2.14.0",
|
||||
"plotly.js-gl2d-dist": "2.14.0",
|
||||
"printj": "1.3.1",
|
||||
"request": "2.88.2",
|
||||
"resolve-url-loader": "5.0.0",
|
||||
"sass": "1.54.9",
|
||||
"sass": "1.55.0",
|
||||
"sass-loader": "13.0.2",
|
||||
"sinon": "14.0.0",
|
||||
"style-loader": "^3.3.1",
|
||||
@@ -69,23 +64,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 +90,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 +105,6 @@
|
||||
"engines": {
|
||||
"node": ">=14.19.1"
|
||||
},
|
||||
"overrides": {
|
||||
"core-js": "3.21.1"
|
||||
},
|
||||
"browserslist": [
|
||||
"Firefox ESR",
|
||||
"not IE 11",
|
||||
|
||||
@@ -63,10 +63,9 @@ export default class Editor extends EventEmitter {
|
||||
.then(() => {
|
||||
this.editing = false;
|
||||
this.emit('isEditing', false);
|
||||
this.openmct.objects.endTransaction();
|
||||
}).catch(error => {
|
||||
throw error;
|
||||
}).finally(() => {
|
||||
this.openmct.objects.endTransaction();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
80
src/api/EditorSpec.js
Normal file
80
src/api/EditorSpec.js
Normal file
@@ -0,0 +1,80 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import {
|
||||
createOpenMct, resetApplicationState
|
||||
} from '../utils/testing';
|
||||
|
||||
describe('The Editor API', () => {
|
||||
let openmct;
|
||||
|
||||
beforeEach((done) => {
|
||||
openmct = createOpenMct();
|
||||
openmct.on('start', done);
|
||||
|
||||
spyOn(openmct.objects, 'endTransaction');
|
||||
|
||||
openmct.startHeadless();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
return resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
it('opens a transaction on edit', () => {
|
||||
expect(
|
||||
openmct.objects.isTransactionActive()
|
||||
).toBeFalse();
|
||||
openmct.editor.edit();
|
||||
expect(
|
||||
openmct.objects.isTransactionActive()
|
||||
).toBeTrue();
|
||||
});
|
||||
|
||||
it('closes an open transaction on successful save', async () => {
|
||||
spyOn(openmct.objects, 'getActiveTransaction')
|
||||
.and.returnValue({
|
||||
commit: () => Promise.resolve(true)
|
||||
});
|
||||
|
||||
openmct.editor.edit();
|
||||
await openmct.editor.save();
|
||||
|
||||
expect(
|
||||
openmct.objects.endTransaction
|
||||
).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not close an open transaction on failed save', async () => {
|
||||
spyOn(openmct.objects, 'getActiveTransaction')
|
||||
.and.returnValue({
|
||||
commit: () => Promise.reject()
|
||||
});
|
||||
|
||||
openmct.editor.edit();
|
||||
await openmct.editor.save().catch(() => {});
|
||||
|
||||
expect(
|
||||
openmct.objects.endTransaction
|
||||
).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -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,40 @@ export default class AnnotationAPI extends EventEmitter {
|
||||
return modelAddedToResults;
|
||||
}
|
||||
|
||||
#combineSameTargets(results) {
|
||||
const combinedResults = [];
|
||||
results.forEach(currentAnnotation => {
|
||||
const existingAnnotation = combinedResults.find((annotationToFind) => {
|
||||
return _.isEqual(currentAnnotation.targets, annotationToFind.targets);
|
||||
});
|
||||
if (!existingAnnotation) {
|
||||
combinedResults.push(currentAnnotation);
|
||||
} else {
|
||||
existingAnnotation.tags.push(...currentAnnotation.tags);
|
||||
}
|
||||
});
|
||||
|
||||
return combinedResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* @method searchForTags
|
||||
* @param {String} query A query to match against tags. E.g., "dr" will match the tags "drilling" and "driving"
|
||||
* @param {Object} abortController An optional abort method to stop the query
|
||||
* @param {Object} [abortController] An optional abort method to stop the query
|
||||
* @returns {Promise} returns a model of matching tags with their target domain objects attached
|
||||
*/
|
||||
async searchForTags(query, abortController) {
|
||||
const matchingTagKeys = this.#getMatchingTags(query);
|
||||
if (!matchingTagKeys.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const searchResults = (await Promise.all(this.openmct.objects.search(matchingTagKeys, abortController, this.openmct.objects.SEARCH_TYPES.TAGS))).flat();
|
||||
const 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);
|
||||
|
||||
@@ -94,7 +94,6 @@ describe("The Annotation API", () => {
|
||||
openmct.startHeadless();
|
||||
});
|
||||
afterEach(async () => {
|
||||
openmct.objects.providers = {};
|
||||
await resetApplicationState(openmct);
|
||||
});
|
||||
it("is defined", () => {
|
||||
@@ -126,34 +125,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 +184,10 @@ 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);
|
||||
it("returns no tags for empty search", async () => {
|
||||
const results = await openmct.annotation.searchForTags('q');
|
||||
expect(results).toBeDefined();
|
||||
expect(results.tags.length).toEqual(2);
|
||||
expect(results.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}());
|
||||
|
||||
@@ -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();
|
||||
@@ -88,7 +96,7 @@ export default class ObjectAPI {
|
||||
this.cache = {};
|
||||
this.interceptorRegistry = new InterceptorRegistry();
|
||||
|
||||
this.SYNCHRONIZED_OBJECT_TYPES = ['notebook', 'plan', 'annotation'];
|
||||
this.SYNCHRONIZED_OBJECT_TYPES = ['notebook', 'restricted-notebook', 'plan', 'annotation'];
|
||||
|
||||
this.errors = {
|
||||
Conflict: ConflictError
|
||||
@@ -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);
|
||||
|
||||
@@ -197,13 +204,13 @@ export default class ObjectAPI {
|
||||
}
|
||||
|
||||
identifier = utils.parseKeyString(identifier);
|
||||
let dirtyObject;
|
||||
if (this.isTransactionActive()) {
|
||||
dirtyObject = this.transaction.getDirtyObject(identifier);
|
||||
}
|
||||
|
||||
if (dirtyObject) {
|
||||
return Promise.resolve(dirtyObject);
|
||||
if (this.isTransactionActive()) {
|
||||
let dirtyObject = this.transaction.getDirtyObject(identifier);
|
||||
|
||||
if (dirtyObject) {
|
||||
return Promise.resolve(dirtyObject);
|
||||
}
|
||||
}
|
||||
|
||||
const provider = this.getProvider(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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -347,10 +354,8 @@ 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
|
||||
*/
|
||||
save(domainObject) {
|
||||
let provider = this.getProvider(domainObject.identifier);
|
||||
let savedResolve;
|
||||
let savedReject;
|
||||
async save(domainObject) {
|
||||
const provider = this.getProvider(domainObject.identifier);
|
||||
let result;
|
||||
|
||||
if (!this.isPersistable(domainObject.identifier)) {
|
||||
@@ -359,27 +364,37 @@ export default class ObjectAPI {
|
||||
result = Promise.resolve(true);
|
||||
} else {
|
||||
const persistedTime = Date.now();
|
||||
if (domainObject.persisted === undefined) {
|
||||
result = new Promise((resolve, reject) => {
|
||||
savedResolve = resolve;
|
||||
savedReject = reject;
|
||||
});
|
||||
domainObject.persisted = persistedTime;
|
||||
const newObjectPromise = provider.create(domainObject);
|
||||
if (newObjectPromise) {
|
||||
newObjectPromise.then(response => {
|
||||
this.mutate(domainObject, 'persisted', persistedTime);
|
||||
savedResolve(response);
|
||||
}).catch((error) => {
|
||||
savedReject(error);
|
||||
});
|
||||
} else {
|
||||
result = Promise.reject(`[ObjectAPI][save] Object provider returned ${newObjectPromise} when creating new object.`);
|
||||
}
|
||||
const username = await this.#getCurrentUsername();
|
||||
const isNewObject = domainObject.persisted === undefined;
|
||||
let savedResolve;
|
||||
let savedReject;
|
||||
let savedObjectPromise;
|
||||
|
||||
result = new Promise((resolve, reject) => {
|
||||
savedResolve = resolve;
|
||||
savedReject = reject;
|
||||
});
|
||||
|
||||
this.#mutate(domainObject, 'persisted', persistedTime);
|
||||
this.#mutate(domainObject, 'modifiedBy', username);
|
||||
|
||||
if (isNewObject) {
|
||||
this.#mutate(domainObject, 'created', persistedTime);
|
||||
this.#mutate(domainObject, 'createdBy', username);
|
||||
|
||||
savedObjectPromise = provider.create(domainObject);
|
||||
} else {
|
||||
domainObject.persisted = persistedTime;
|
||||
this.mutate(domainObject, 'persisted', persistedTime);
|
||||
result = provider.update(domainObject);
|
||||
savedObjectPromise = provider.update(domainObject);
|
||||
}
|
||||
|
||||
if (savedObjectPromise) {
|
||||
savedObjectPromise.then(response => {
|
||||
savedResolve(response);
|
||||
}).catch((error) => {
|
||||
savedReject(error);
|
||||
});
|
||||
} else {
|
||||
result = Promise.reject(`[ObjectAPI][save] Object provider returned ${savedObjectPromise} when ${isNewObject ? 'creating new' : 'updating'} object.`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -392,8 +407,21 @@ export default class ObjectAPI {
|
||||
});
|
||||
}
|
||||
|
||||
async #getCurrentUsername() {
|
||||
const user = await this.openmct.user.getCurrentUser();
|
||||
let username;
|
||||
|
||||
if (user !== undefined) {
|
||||
username = user.getName();
|
||||
}
|
||||
|
||||
return username;
|
||||
}
|
||||
|
||||
/**
|
||||
* After entering into edit mode, creates a new instance of Transaction to keep track of changes in Objects
|
||||
*
|
||||
* @returns {Transaction} a new Transaction that was just created
|
||||
*/
|
||||
startTransaction() {
|
||||
if (this.isTransactionActive()) {
|
||||
@@ -401,6 +429,8 @@ export default class ObjectAPI {
|
||||
}
|
||||
|
||||
this.transaction = new Transaction(this);
|
||||
|
||||
return this.transaction;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -473,14 +503,16 @@ export default class ObjectAPI {
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify a domain object.
|
||||
* Modify a domain object. Internal to ObjectAPI, won't call save after.
|
||||
* @private
|
||||
*
|
||||
* @param {module:openmct.DomainObject} object the object to mutate
|
||||
* @param {string} path the property to modify
|
||||
* @param {*} value the new value for this property
|
||||
* @method mutate
|
||||
* @memberof module:openmct.ObjectAPI#
|
||||
*/
|
||||
mutate(domainObject, path, value) {
|
||||
#mutate(domainObject, path, value) {
|
||||
if (!this.supportsMutation(domainObject.identifier)) {
|
||||
throw `Error: Attempted to mutate immutable object ${domainObject.name}`;
|
||||
}
|
||||
@@ -490,7 +522,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);
|
||||
@@ -501,6 +533,18 @@ export default class ObjectAPI {
|
||||
//Destroy temporary mutable object
|
||||
this.destroyMutable(mutableDomainObject);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify a domain object and save.
|
||||
* @param {module:openmct.DomainObject} object the object to mutate
|
||||
* @param {string} path the property to modify
|
||||
* @param {*} value the new value for this property
|
||||
* @method mutate
|
||||
* @memberof module:openmct.ObjectAPI#
|
||||
*/
|
||||
mutate(domainObject, path, value) {
|
||||
this.#mutate(domainObject, path, value);
|
||||
|
||||
if (this.isTransactionActive()) {
|
||||
this.transaction.add(domainObject);
|
||||
@@ -510,15 +554,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 +574,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 +632,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();
|
||||
@@ -671,12 +721,14 @@ export default class ObjectAPI {
|
||||
}
|
||||
|
||||
isTransactionActive() {
|
||||
return Boolean(this.transaction && this.openmct.editor.isEditing());
|
||||
return this.transaction !== undefined && this.transaction !== null;
|
||||
}
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
@@ -8,13 +8,27 @@ describe("The Object API", () => {
|
||||
let mockDomainObject;
|
||||
const TEST_NAMESPACE = "test-namespace";
|
||||
const TEST_KEY = "test-key";
|
||||
const USERNAME = 'Joan Q Public';
|
||||
const FIFTEEN_MINUTES = 15 * 60 * 1000;
|
||||
|
||||
beforeEach((done) => {
|
||||
typeRegistry = jasmine.createSpyObj('typeRegistry', [
|
||||
'get'
|
||||
]);
|
||||
const userProvider = {
|
||||
isLoggedIn() {
|
||||
return true;
|
||||
},
|
||||
getCurrentUser() {
|
||||
return Promise.resolve({
|
||||
getName() {
|
||||
return USERNAME;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
openmct = createOpenMct();
|
||||
openmct.user.setProvider(userProvider);
|
||||
objectAPI = openmct.objects;
|
||||
|
||||
openmct.editor = {};
|
||||
@@ -63,19 +77,34 @@ describe("The Object API", () => {
|
||||
mockProvider.update.and.returnValue(Promise.resolve(true));
|
||||
objectAPI.addProvider(TEST_NAMESPACE, mockProvider);
|
||||
});
|
||||
it("Calls 'create' on provider if object is new", () => {
|
||||
objectAPI.save(mockDomainObject);
|
||||
it("Adds a 'created' timestamp to new objects", async () => {
|
||||
await objectAPI.save(mockDomainObject);
|
||||
expect(mockDomainObject.created).not.toBeUndefined();
|
||||
});
|
||||
it("Calls 'create' on provider if object is new", async () => {
|
||||
await objectAPI.save(mockDomainObject);
|
||||
expect(mockProvider.create).toHaveBeenCalled();
|
||||
expect(mockProvider.update).not.toHaveBeenCalled();
|
||||
});
|
||||
it("Calls 'update' on provider if object is not new", () => {
|
||||
it("Calls 'update' on provider if object is not new", async () => {
|
||||
mockDomainObject.persisted = Date.now() - FIFTEEN_MINUTES;
|
||||
mockDomainObject.modified = Date.now();
|
||||
|
||||
objectAPI.save(mockDomainObject);
|
||||
await objectAPI.save(mockDomainObject);
|
||||
expect(mockProvider.create).not.toHaveBeenCalled();
|
||||
expect(mockProvider.update).toHaveBeenCalled();
|
||||
});
|
||||
it("Sets the current user for 'createdBy' on new objects", async () => {
|
||||
await objectAPI.save(mockDomainObject);
|
||||
expect(mockDomainObject.createdBy).toBe(USERNAME);
|
||||
});
|
||||
it("Sets the current user for 'modifedBy' on existing objects", async () => {
|
||||
mockDomainObject.persisted = Date.now() - FIFTEEN_MINUTES;
|
||||
mockDomainObject.modified = Date.now();
|
||||
|
||||
await objectAPI.save(mockDomainObject);
|
||||
expect(mockDomainObject.modifiedBy).toBe(USERNAME);
|
||||
});
|
||||
|
||||
it("Does not persist if the object is unchanged", () => {
|
||||
mockDomainObject.persisted =
|
||||
@@ -320,7 +349,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(() => {
|
||||
|
||||
@@ -27,7 +27,6 @@ import TelemetryMetadataManager from './TelemetryMetadataManager';
|
||||
import TelemetryValueFormatter from './TelemetryValueFormatter';
|
||||
import DefaultMetadataProvider from './DefaultMetadataProvider';
|
||||
import objectUtils from 'objectUtils';
|
||||
import _ from 'lodash';
|
||||
|
||||
export default class TelemetryAPI {
|
||||
|
||||
@@ -73,7 +72,7 @@ export default class TelemetryAPI {
|
||||
* @returns {boolean} true if the object is a telemetry object.
|
||||
*/
|
||||
isTelemetryObject(domainObject) {
|
||||
return Boolean(this.findMetadataProvider(domainObject));
|
||||
return Boolean(this.#findMetadataProvider(domainObject));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,7 +86,7 @@ export default class TelemetryAPI {
|
||||
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
|
||||
*/
|
||||
canProvideTelemetry(domainObject) {
|
||||
return Boolean(this.findSubscriptionProvider(domainObject))
|
||||
return Boolean(this.#findSubscriptionProvider(domainObject))
|
||||
|| Boolean(this.findRequestProvider(domainObject));
|
||||
}
|
||||
|
||||
@@ -120,7 +119,7 @@ export default class TelemetryAPI {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
findSubscriptionProvider() {
|
||||
#findSubscriptionProvider() {
|
||||
const args = Array.prototype.slice.apply(arguments);
|
||||
function supportsDomainObject(provider) {
|
||||
return provider.supportsSubscribe.apply(provider, args);
|
||||
@@ -130,9 +129,10 @@ export default class TelemetryAPI {
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Returns a telemetry request provider that supports
|
||||
* a given domain object and options.
|
||||
*/
|
||||
findRequestProvider(domainObject) {
|
||||
findRequestProvider() {
|
||||
const args = Array.prototype.slice.apply(arguments);
|
||||
function supportsDomainObject(provider) {
|
||||
return provider.supportsRequest.apply(provider, args);
|
||||
@@ -144,7 +144,7 @@ export default class TelemetryAPI {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
findMetadataProvider(domainObject) {
|
||||
#findMetadataProvider(domainObject) {
|
||||
return this.metadataProviders.filter(function (p) {
|
||||
return p.supportsMetadata(domainObject);
|
||||
})[0];
|
||||
@@ -153,7 +153,7 @@ export default class TelemetryAPI {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
findLimitEvaluator(domainObject) {
|
||||
#findLimitEvaluator(domainObject) {
|
||||
return this.limitProviders.filter(function (p) {
|
||||
return p.supportsLimits(domainObject);
|
||||
})[0];
|
||||
@@ -161,6 +161,7 @@ export default class TelemetryAPI {
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Though used in TelemetryCollection as well
|
||||
*/
|
||||
standardizeRequestOptions(options) {
|
||||
if (!Object.prototype.hasOwnProperty.call(options, 'start')) {
|
||||
@@ -174,6 +175,10 @@ export default class TelemetryAPI {
|
||||
if (!Object.prototype.hasOwnProperty.call(options, 'domain')) {
|
||||
options.domain = this.openmct.time.timeSystem().key;
|
||||
}
|
||||
|
||||
if (!Object.prototype.hasOwnProperty.call(options, 'timeContext')) {
|
||||
options.timeContext = this.openmct.time;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -241,7 +246,7 @@ export default class TelemetryAPI {
|
||||
/**
|
||||
* Request historical telemetry for a domain object.
|
||||
* The `options` argument allows you to specify filters
|
||||
* (start, end, etc.), sort order, and strategies for retrieving
|
||||
* (start, end, etc.), sort order, time context, and strategies for retrieving
|
||||
* telemetry (aggregation, latest available, etc.).
|
||||
*
|
||||
* @method request
|
||||
@@ -255,7 +260,7 @@ export default class TelemetryAPI {
|
||||
*/
|
||||
async request(domainObject) {
|
||||
if (this.noRequestProviderForAllObjects) {
|
||||
return Promise.resolve([]);
|
||||
return [];
|
||||
}
|
||||
|
||||
if (arguments.length === 1) {
|
||||
@@ -273,22 +278,24 @@ export default class TelemetryAPI {
|
||||
if (!provider) {
|
||||
this.requestAbortControllers.delete(abortController);
|
||||
|
||||
return this.handleMissingRequestProvider(domainObject);
|
||||
return this.#handleMissingRequestProvider(domainObject);
|
||||
}
|
||||
|
||||
arguments[1] = await this.applyRequestInterceptors(domainObject, arguments[1]);
|
||||
try {
|
||||
const telemetry = await provider.request(...arguments);
|
||||
|
||||
return provider.request.apply(provider, arguments)
|
||||
.catch((rejected) => {
|
||||
if (rejected.name !== 'AbortError') {
|
||||
this.openmct.notifications.error('Error requesting telemetry data, see console for details');
|
||||
console.error(rejected);
|
||||
}
|
||||
return telemetry;
|
||||
} catch (error) {
|
||||
if (error.name !== 'AbortError') {
|
||||
this.openmct.notifications.error('Error requesting telemetry data, see console for details');
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
return Promise.reject(rejected);
|
||||
}).finally(() => {
|
||||
this.requestAbortControllers.delete(abortController);
|
||||
});
|
||||
throw new Error(error);
|
||||
} finally {
|
||||
this.requestAbortControllers.delete(abortController);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -306,7 +313,7 @@ export default class TelemetryAPI {
|
||||
* the subscription
|
||||
*/
|
||||
subscribe(domainObject, callback, options) {
|
||||
const provider = this.findSubscriptionProvider(domainObject);
|
||||
const provider = this.#findSubscriptionProvider(domainObject);
|
||||
|
||||
if (!this.subscribeCache) {
|
||||
this.subscribeCache = {};
|
||||
@@ -353,7 +360,7 @@ export default class TelemetryAPI {
|
||||
*/
|
||||
getMetadata(domainObject) {
|
||||
if (!this.metadataCache.has(domainObject)) {
|
||||
const metadataProvider = this.findMetadataProvider(domainObject);
|
||||
const metadataProvider = this.#findMetadataProvider(domainObject);
|
||||
if (!metadataProvider) {
|
||||
return;
|
||||
}
|
||||
@@ -369,33 +376,6 @@ export default class TelemetryAPI {
|
||||
return this.metadataCache.get(domainObject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array of valueMetadatas that are common to all supplied
|
||||
* telemetry objects and match the requested hints.
|
||||
*
|
||||
*/
|
||||
commonValuesForHints(metadatas, hints) {
|
||||
const options = metadatas.map(function (metadata) {
|
||||
const values = metadata.valuesForHints(hints);
|
||||
|
||||
return _.keyBy(values, 'key');
|
||||
}).reduce(function (a, b) {
|
||||
const results = {};
|
||||
Object.keys(a).forEach(function (key) {
|
||||
if (Object.prototype.hasOwnProperty.call(b, key)) {
|
||||
results[key] = a[key];
|
||||
}
|
||||
});
|
||||
|
||||
return results;
|
||||
});
|
||||
const sortKeys = hints.map(function (h) {
|
||||
return 'hints.' + h;
|
||||
});
|
||||
|
||||
return _.sortBy(options, sortKeys);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a value formatter for a given valueMetadata.
|
||||
*
|
||||
@@ -450,7 +430,7 @@ export default class TelemetryAPI {
|
||||
*
|
||||
* @returns Promise
|
||||
*/
|
||||
handleMissingRequestProvider(domainObject) {
|
||||
#handleMissingRequestProvider(domainObject) {
|
||||
this.noRequestProviderForAllObjects = this.requestProviders.every(requestProvider => {
|
||||
const supportsRequest = requestProvider.supportsRequest.apply(requestProvider, arguments);
|
||||
const hasRequestProvider = Object.prototype.hasOwnProperty.call(requestProvider, 'request') && typeof requestProvider.request === 'function';
|
||||
@@ -540,7 +520,7 @@ export default class TelemetryAPI {
|
||||
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
|
||||
*/
|
||||
getLimitEvaluator(domainObject) {
|
||||
const provider = this.findLimitEvaluator(domainObject);
|
||||
const provider = this.#findLimitEvaluator(domainObject);
|
||||
if (!provider) {
|
||||
return {
|
||||
evaluate: function () {}
|
||||
@@ -578,7 +558,7 @@ export default class TelemetryAPI {
|
||||
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
|
||||
*/
|
||||
getLimits(domainObject) {
|
||||
const provider = this.findLimitEvaluator(domainObject);
|
||||
const provider = this.#findLimitEvaluator(domainObject);
|
||||
if (!provider || !provider.getLimits) {
|
||||
return {
|
||||
limits: function () {
|
||||
|
||||
@@ -23,11 +23,11 @@ import { createOpenMct, resetApplicationState } from 'utils/testing';
|
||||
import TelemetryAPI from './TelemetryAPI';
|
||||
import TelemetryCollection from './TelemetryCollection';
|
||||
|
||||
describe('Telemetry API', function () {
|
||||
describe('Telemetry API', () => {
|
||||
let openmct;
|
||||
let telemetryAPI;
|
||||
|
||||
beforeEach(function () {
|
||||
beforeEach(() => {
|
||||
openmct = {
|
||||
time: jasmine.createSpyObj('timeAPI', [
|
||||
'timeSystem',
|
||||
@@ -47,11 +47,11 @@ describe('Telemetry API', function () {
|
||||
|
||||
});
|
||||
|
||||
describe('telemetry providers', function () {
|
||||
describe('telemetry providers', () => {
|
||||
let telemetryProvider;
|
||||
let domainObject;
|
||||
|
||||
beforeEach(function () {
|
||||
beforeEach(() => {
|
||||
telemetryProvider = jasmine.createSpyObj('telemetryProvider', [
|
||||
'supportsSubscribe',
|
||||
'subscribe',
|
||||
@@ -73,19 +73,16 @@ describe('Telemetry API', function () {
|
||||
};
|
||||
});
|
||||
|
||||
it('provides consistent results without providers', function (done) {
|
||||
it('provides consistent results without providers', async () => {
|
||||
const unsubscribe = telemetryAPI.subscribe(domainObject);
|
||||
|
||||
expect(unsubscribe).toEqual(jasmine.any(Function));
|
||||
|
||||
telemetryAPI.request(domainObject)
|
||||
.then((data) => {
|
||||
expect(data).toEqual([]);
|
||||
})
|
||||
.finally(done);
|
||||
const data = await telemetryAPI.request(domainObject);
|
||||
expect(data).toEqual([]);
|
||||
});
|
||||
|
||||
it('skips providers that do not match', function (done) {
|
||||
it('skips providers that do not match', async () => {
|
||||
telemetryProvider.supportsSubscribe.and.returnValue(false);
|
||||
telemetryProvider.supportsRequest.and.returnValue(false);
|
||||
telemetryProvider.request.and.returnValue(Promise.resolve([]));
|
||||
@@ -98,14 +95,13 @@ describe('Telemetry API', function () {
|
||||
expect(telemetryProvider.subscribe).not.toHaveBeenCalled();
|
||||
expect(unsubscribe).toEqual(jasmine.any(Function));
|
||||
|
||||
telemetryAPI.request(domainObject).then((response) => {
|
||||
expect(telemetryProvider.supportsRequest)
|
||||
.toHaveBeenCalledWith(domainObject, jasmine.any(Object));
|
||||
expect(telemetryProvider.request).not.toHaveBeenCalled();
|
||||
}).finally(done);
|
||||
await telemetryAPI.request(domainObject);
|
||||
expect(telemetryProvider.supportsRequest)
|
||||
.toHaveBeenCalledWith(domainObject, jasmine.any(Object));
|
||||
expect(telemetryProvider.request).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('sends subscribe calls to matching providers', function () {
|
||||
it('sends subscribe calls to matching providers', () => {
|
||||
const unsubFunc = jasmine.createSpy('unsubscribe');
|
||||
telemetryProvider.subscribe.and.returnValue(unsubFunc);
|
||||
telemetryProvider.supportsSubscribe.and.returnValue(true);
|
||||
@@ -133,7 +129,7 @@ describe('Telemetry API', function () {
|
||||
expect(callback).not.toHaveBeenCalledWith('otherValue');
|
||||
});
|
||||
|
||||
it('subscribes once per object', function () {
|
||||
it('subscribes once per object', () => {
|
||||
const unsubFunc = jasmine.createSpy('unsubscribe');
|
||||
telemetryProvider.subscribe.and.returnValue(unsubFunc);
|
||||
telemetryProvider.supportsSubscribe.and.returnValue(true);
|
||||
@@ -164,7 +160,7 @@ describe('Telemetry API', function () {
|
||||
expect(callbacktwo).not.toHaveBeenCalledWith('anotherValue');
|
||||
});
|
||||
|
||||
it('only deletes subscription cache when there are no more subscribers', function () {
|
||||
it('only deletes subscription cache when there are no more subscribers', () => {
|
||||
const unsubFunc = jasmine.createSpy('unsubscribe');
|
||||
telemetryProvider.subscribe.and.returnValue(unsubFunc);
|
||||
telemetryProvider.supportsSubscribe.and.returnValue(true);
|
||||
@@ -187,7 +183,7 @@ describe('Telemetry API', function () {
|
||||
unsubscribeThree();
|
||||
});
|
||||
|
||||
it('does subscribe/unsubscribe', function () {
|
||||
it('does subscribe/unsubscribe', () => {
|
||||
const unsubFunc = jasmine.createSpy('unsubscribe');
|
||||
telemetryProvider.subscribe.and.returnValue(unsubFunc);
|
||||
telemetryProvider.supportsSubscribe.and.returnValue(true);
|
||||
@@ -203,7 +199,7 @@ describe('Telemetry API', function () {
|
||||
unsubscribe();
|
||||
});
|
||||
|
||||
it('subscribes for different object', function () {
|
||||
it('subscribes for different object', () => {
|
||||
const unsubFuncs = [];
|
||||
const notifiers = [];
|
||||
telemetryProvider.supportsSubscribe.and.returnValue(true);
|
||||
@@ -243,120 +239,120 @@ describe('Telemetry API', function () {
|
||||
expect(unsubFuncs[1]).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('sends requests to matching providers', function (done) {
|
||||
it('sends requests to matching providers', async () => {
|
||||
const telemPromise = Promise.resolve([]);
|
||||
telemetryProvider.supportsRequest.and.returnValue(true);
|
||||
telemetryProvider.request.and.returnValue(telemPromise);
|
||||
telemetryAPI.addProvider(telemetryProvider);
|
||||
|
||||
telemetryAPI.request(domainObject).then(() => {
|
||||
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(
|
||||
domainObject,
|
||||
jasmine.any(Object)
|
||||
);
|
||||
expect(telemetryProvider.request).toHaveBeenCalledWith(
|
||||
domainObject,
|
||||
jasmine.any(Object)
|
||||
);
|
||||
}).finally(done);
|
||||
await telemetryAPI.request(domainObject);
|
||||
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(
|
||||
domainObject,
|
||||
jasmine.any(Object)
|
||||
);
|
||||
expect(telemetryProvider.request).toHaveBeenCalledWith(
|
||||
domainObject,
|
||||
jasmine.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('generates default request options', function (done) {
|
||||
it('generates default request options', async () => {
|
||||
telemetryProvider.supportsRequest.and.returnValue(true);
|
||||
telemetryProvider.request.and.returnValue(Promise.resolve([]));
|
||||
telemetryAPI.addProvider(telemetryProvider);
|
||||
|
||||
telemetryAPI.request(domainObject).then(() => {
|
||||
const { signal } = new AbortController();
|
||||
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(
|
||||
jasmine.any(Object),
|
||||
{
|
||||
signal,
|
||||
start: 0,
|
||||
end: 1,
|
||||
domain: 'system'
|
||||
}
|
||||
);
|
||||
await telemetryAPI.request(domainObject);
|
||||
const { signal } = new AbortController();
|
||||
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(
|
||||
jasmine.any(Object),
|
||||
{
|
||||
signal,
|
||||
start: 0,
|
||||
end: 1,
|
||||
domain: 'system',
|
||||
timeContext: jasmine.any(Object)
|
||||
}
|
||||
);
|
||||
|
||||
expect(telemetryProvider.request).toHaveBeenCalledWith(
|
||||
jasmine.any(Object),
|
||||
{
|
||||
signal,
|
||||
start: 0,
|
||||
end: 1,
|
||||
domain: 'system'
|
||||
}
|
||||
);
|
||||
expect(telemetryProvider.request).toHaveBeenCalledWith(
|
||||
jasmine.any(Object),
|
||||
{
|
||||
signal,
|
||||
start: 0,
|
||||
end: 1,
|
||||
domain: 'system',
|
||||
timeContext: jasmine.any(Object)
|
||||
}
|
||||
);
|
||||
|
||||
telemetryProvider.supportsRequest.calls.reset();
|
||||
telemetryProvider.request.calls.reset();
|
||||
telemetryProvider.supportsRequest.calls.reset();
|
||||
telemetryProvider.request.calls.reset();
|
||||
|
||||
telemetryAPI.request(domainObject, {}).then(() => {
|
||||
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(
|
||||
jasmine.any(Object),
|
||||
{
|
||||
signal,
|
||||
start: 0,
|
||||
end: 1,
|
||||
domain: 'system'
|
||||
}
|
||||
);
|
||||
|
||||
expect(telemetryProvider.request).toHaveBeenCalledWith(
|
||||
jasmine.any(Object),
|
||||
{
|
||||
signal,
|
||||
start: 0,
|
||||
end: 1,
|
||||
domain: 'system'
|
||||
}
|
||||
);
|
||||
});
|
||||
}).finally(done);
|
||||
await telemetryAPI.request(domainObject, {});
|
||||
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(
|
||||
jasmine.any(Object),
|
||||
{
|
||||
signal,
|
||||
start: 0,
|
||||
end: 1,
|
||||
domain: 'system',
|
||||
timeContext: jasmine.any(Object)
|
||||
}
|
||||
);
|
||||
|
||||
expect(telemetryProvider.request).toHaveBeenCalledWith(
|
||||
jasmine.any(Object),
|
||||
{
|
||||
signal,
|
||||
start: 0,
|
||||
end: 1,
|
||||
domain: 'system',
|
||||
timeContext: jasmine.any(Object)
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('do not overwrite existing request options', function (done) {
|
||||
it('do not overwrite existing request options', async () => {
|
||||
telemetryProvider.supportsRequest.and.returnValue(true);
|
||||
telemetryProvider.request.and.returnValue(Promise.resolve([]));
|
||||
telemetryAPI.addProvider(telemetryProvider);
|
||||
|
||||
telemetryAPI.request(domainObject, {
|
||||
await telemetryAPI.request(domainObject, {
|
||||
start: 20,
|
||||
end: 30,
|
||||
domain: 'someDomain'
|
||||
}).then(() => {
|
||||
const { signal } = new AbortController();
|
||||
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(
|
||||
jasmine.any(Object),
|
||||
{
|
||||
start: 20,
|
||||
end: 30,
|
||||
domain: 'someDomain',
|
||||
signal
|
||||
}
|
||||
);
|
||||
});
|
||||
const { signal } = new AbortController();
|
||||
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(
|
||||
jasmine.any(Object),
|
||||
{
|
||||
start: 20,
|
||||
end: 30,
|
||||
domain: 'someDomain',
|
||||
signal,
|
||||
timeContext: jasmine.any(Object)
|
||||
}
|
||||
);
|
||||
|
||||
expect(telemetryProvider.request).toHaveBeenCalledWith(
|
||||
jasmine.any(Object),
|
||||
{
|
||||
start: 20,
|
||||
end: 30,
|
||||
domain: 'someDomain',
|
||||
signal
|
||||
}
|
||||
);
|
||||
|
||||
}).finally(done);
|
||||
expect(telemetryProvider.request).toHaveBeenCalledWith(
|
||||
jasmine.any(Object),
|
||||
{
|
||||
start: 20,
|
||||
end: 30,
|
||||
domain: 'someDomain',
|
||||
signal,
|
||||
timeContext: jasmine.any(Object)
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('metadata', function () {
|
||||
describe('metadata', () => {
|
||||
let mockMetadata = {};
|
||||
let mockObjectType = {
|
||||
definition: {}
|
||||
};
|
||||
beforeEach(function () {
|
||||
beforeEach(() => {
|
||||
telemetryAPI.addProvider({
|
||||
key: 'mockMetadataProvider',
|
||||
supportsMetadata() {
|
||||
@@ -369,7 +365,7 @@ describe('Telemetry API', function () {
|
||||
openmct.types.get.and.returnValue(mockObjectType);
|
||||
});
|
||||
|
||||
it('respects explicit priority', function () {
|
||||
it('respects explicit priority', () => {
|
||||
mockMetadata.values = [
|
||||
{
|
||||
key: "name",
|
||||
@@ -408,7 +404,7 @@ describe('Telemetry API', function () {
|
||||
expect(value.hints.priority).toBe(index + 1);
|
||||
});
|
||||
});
|
||||
it('if no explicit priority, defaults to order defined', function () {
|
||||
it('if no explicit priority, defaults to order defined', () => {
|
||||
mockMetadata.values = [
|
||||
{
|
||||
key: "name",
|
||||
@@ -435,7 +431,7 @@ describe('Telemetry API', function () {
|
||||
expect(value.key).toBe(mockMetadata.values[index].key);
|
||||
});
|
||||
});
|
||||
it('respects domain priority', function () {
|
||||
it('respects domain priority', () => {
|
||||
mockMetadata.values = [
|
||||
{
|
||||
key: "name",
|
||||
@@ -477,7 +473,7 @@ describe('Telemetry API', function () {
|
||||
expect(values[0].key).toBe('timestamp-local');
|
||||
expect(values[1].key).toBe('timestamp-utc');
|
||||
});
|
||||
it('respects range priority', function () {
|
||||
it('respects range priority', () => {
|
||||
mockMetadata.values = [
|
||||
{
|
||||
key: "name",
|
||||
@@ -519,7 +515,7 @@ describe('Telemetry API', function () {
|
||||
expect(values[0].key).toBe('cos');
|
||||
expect(values[1].key).toBe('sin');
|
||||
});
|
||||
it('respects priority and domain ordering', function () {
|
||||
it('respects priority and domain ordering', () => {
|
||||
mockMetadata.values = [
|
||||
{
|
||||
key: "id",
|
||||
@@ -588,7 +584,7 @@ describe('Telemetry API', function () {
|
||||
definition: {}
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
beforeEach(() => {
|
||||
openmct.telemetry = telemetryAPI;
|
||||
telemetryAPI.addProvider({
|
||||
key: 'mockMetadataProvider',
|
||||
@@ -644,16 +640,14 @@ describe('Telemetery', () => {
|
||||
return resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
it('should not abort request without navigation', function (done) {
|
||||
it('should not abort request without navigation', async () => {
|
||||
telemetryAPI.addProvider(telemetryProvider);
|
||||
|
||||
telemetryAPI.request({}).finally(() => {
|
||||
expect(watchedSignal.aborted).toBe(false);
|
||||
done();
|
||||
});
|
||||
await telemetryAPI.request({});
|
||||
expect(watchedSignal.aborted).toBe(false);
|
||||
});
|
||||
|
||||
it('should abort request on navigation', function (done) {
|
||||
it('should abort request on navigation', (done) => {
|
||||
telemetryAPI.addProvider(telemetryProvider);
|
||||
|
||||
telemetryAPI.request({}).finally(() => {
|
||||
|
||||
@@ -229,6 +229,25 @@ describe("The Time API", function () {
|
||||
expect(api.clock()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('Provides a default time context', () => {
|
||||
const timeContext = api.getContextForView([]);
|
||||
expect(timeContext).not.toBe(null);
|
||||
});
|
||||
|
||||
it("Without a clock, is in fixed time mode", () => {
|
||||
const timeContext = api.getContextForView([]);
|
||||
expect(timeContext.isRealTime()).toBe(false);
|
||||
});
|
||||
|
||||
it("Provided a clock, is in real-time mode", () => {
|
||||
const timeContext = api.getContextForView([]);
|
||||
timeContext.clock('mts', {
|
||||
start: 0,
|
||||
end: 1
|
||||
});
|
||||
expect(timeContext.isRealTime()).toBe(true);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it("on tick, observes offsets, and indicates tick in bounds callback", function () {
|
||||
|
||||
@@ -362,6 +362,18 @@ class TimeContext extends EventEmitter {
|
||||
this.boundsVal = newBounds;
|
||||
this.emit('bounds', this.boundsVal, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this time context is in real-time mode or not.
|
||||
* @returns {boolean} true if this context is in real-time mode, false if not
|
||||
*/
|
||||
isRealTime() {
|
||||
if (this.clock()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default TimeContext;
|
||||
|
||||
@@ -114,6 +114,8 @@ export default {
|
||||
this.formats = this.openmct.telemetry.getFormatMap(this.metadata);
|
||||
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
|
||||
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
|
||||
|
||||
this.limitEvaluator = this.openmct
|
||||
.telemetry
|
||||
.limitEvaluator(this.domainObject);
|
||||
@@ -134,7 +136,8 @@ export default {
|
||||
|
||||
this.telemetryCollection = this.openmct.telemetry.requestCollection(this.domainObject, {
|
||||
size: 1,
|
||||
strategy: 'latest'
|
||||
strategy: 'latest',
|
||||
timeContext: this.timeContext
|
||||
});
|
||||
this.telemetryCollection.on('add', this.setLatestValues);
|
||||
this.telemetryCollection.on('clear', this.resetValues);
|
||||
|
||||
@@ -30,6 +30,12 @@
|
||||
padding: $interiorMarginLg $interiorMarginLg * 2;
|
||||
}
|
||||
|
||||
.c-condition-widget__label {
|
||||
padding: $interiorMargin;
|
||||
text-align: center;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
a.c-condition-widget {
|
||||
// Widget is conditionally made into a <a> when URL property has been defined
|
||||
cursor: pointer !important;
|
||||
|
||||
@@ -282,12 +282,15 @@ export default {
|
||||
this.limitEvaluator = this.openmct.telemetry.limitEvaluator(this.domainObject);
|
||||
this.formats = this.openmct.telemetry.getFormatMap(this.metadata);
|
||||
|
||||
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
|
||||
|
||||
const valueMetadata = this.metadata ? this.metadata.value(this.item.value) : {};
|
||||
this.customStringformatter = this.openmct.telemetry.customStringFormatter(valueMetadata, this.item.format);
|
||||
|
||||
this.telemetryCollection = this.openmct.telemetry.requestCollection(this.domainObject, {
|
||||
size: 1,
|
||||
strategy: 'latest'
|
||||
strategy: 'latest',
|
||||
timeContext: this.timeContext
|
||||
});
|
||||
this.telemetryCollection.on('add', this.setLatestValues);
|
||||
this.telemetryCollection.on('clear', this.refreshData);
|
||||
|
||||
@@ -107,7 +107,7 @@ export default class CreateAction extends PropertiesAction {
|
||||
}
|
||||
|
||||
const url = '#/browse/' + objectPath
|
||||
.map(object => object && this.openmct.objects.makeKeyString(object.identifier.key))
|
||||
.map(object => object && this.openmct.objects.makeKeyString(object.identifier))
|
||||
.reverse()
|
||||
.join('/');
|
||||
|
||||
|
||||
@@ -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,10 @@ 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) {
|
||||
if (this.notebookAnnotations[entryId]) {
|
||||
this.openmct.annotation.deleteAnnotations(this.notebookAnnotations[entryId]);
|
||||
}
|
||||
},
|
||||
checkEntryPos(entry) {
|
||||
const entryPos = getEntryPosById(entry.id, this.domainObject, this.selectedSection, this.selectedPage);
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
@@ -163,6 +162,12 @@ export default {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
notebookAnnotations: {
|
||||
type: Array,
|
||||
default() {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
entry: {
|
||||
type: Object,
|
||||
default() {
|
||||
@@ -204,15 +209,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');
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
const connections = [];
|
||||
let connected = false;
|
||||
let couchEventSource;
|
||||
let changesFeedUrl;
|
||||
const keepAliveTime = 20 * 1000;
|
||||
let keepAliveTimer;
|
||||
const controller = new AbortController();
|
||||
|
||||
self.onconnect = function (e) {
|
||||
@@ -35,7 +38,8 @@
|
||||
return;
|
||||
}
|
||||
|
||||
self.listenForChanges(event.data.url);
|
||||
changesFeedUrl = event.data.url;
|
||||
self.listenForChanges();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -63,17 +67,28 @@
|
||||
});
|
||||
};
|
||||
|
||||
self.listenForChanges = function (url) {
|
||||
console.debug('⇿ Opening CouchDB change feed connection ⇿');
|
||||
self.listenForChanges = function () {
|
||||
if (keepAliveTimer) {
|
||||
clearTimeout(keepAliveTimer);
|
||||
}
|
||||
|
||||
couchEventSource = new EventSource(url);
|
||||
couchEventSource.onerror = self.onerror;
|
||||
couchEventSource.onopen = self.onopen;
|
||||
/**
|
||||
* Once the connection has been opened, poll every 20 seconds to see if the EventSource has closed unexpectedly.
|
||||
* If it has, attempt to reconnect.
|
||||
*/
|
||||
keepAliveTimer = setTimeout(self.listenForChanges, keepAliveTime);
|
||||
|
||||
// start listening for events
|
||||
couchEventSource.addEventListener('message', self.onCouchMessage);
|
||||
connected = true;
|
||||
console.debug('⇿ Opened connection ⇿');
|
||||
if (!couchEventSource || couchEventSource.readyState === EventSource.CLOSED) {
|
||||
console.debug('⇿ Opening CouchDB change feed connection ⇿');
|
||||
couchEventSource = new EventSource(changesFeedUrl);
|
||||
couchEventSource.onerror = self.onerror;
|
||||
couchEventSource.onopen = self.onopen;
|
||||
|
||||
// start listening for events
|
||||
couchEventSource.addEventListener('message', self.onCouchMessage);
|
||||
connected = true;
|
||||
console.debug('⇿ Opened connection ⇿');
|
||||
}
|
||||
};
|
||||
|
||||
self.updateCouchStateIndicator = function () {
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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,46 +89,19 @@ 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) {
|
||||
if (!tagsArray || !tagsArray.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const filter = {
|
||||
"selector": {
|
||||
"$and": [
|
||||
{
|
||||
"model.tags": {
|
||||
"$elemMatch": {
|
||||
"$eq": `${tagsArray[0]}`
|
||||
"$or": [
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -142,6 +113,11 @@ class CouchSearchProvider {
|
||||
]
|
||||
}
|
||||
};
|
||||
tagsArray.forEach(tag => {
|
||||
filter.selector.$and[0]["model.tags"].$elemMatch.$or.push({
|
||||
"$eq": `${tag}`
|
||||
});
|
||||
});
|
||||
|
||||
return this.couchObjectProvider.getObjectsByFilter(filter, abortSignal);
|
||||
}
|
||||
|
||||
117
src/plugins/plan/README.md
Normal file
117
src/plugins/plan/README.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# Plan view and domain objects
|
||||
Plans provide a view for a list of activities grouped by categories.
|
||||
|
||||
## Plan category and activity JSON format
|
||||
The JSON format for a plan consists of categories/groups and a list of activities for each category.
|
||||
Activity properties include:
|
||||
* name: Name of the activity
|
||||
* start: Timestamps in milliseconds
|
||||
* end: Timestamps in milliseconds
|
||||
* type: Matches the name of the category it is in
|
||||
* color: Background color for the activity
|
||||
* textColor: Color of the name text for the activity
|
||||
* The format of the json file is as follows:
|
||||
|
||||
```json
|
||||
{
|
||||
"TEST_GROUP": [{
|
||||
"name": "Event 1 with a really long name",
|
||||
"start": 1665323197000,
|
||||
"end": 1665344921000,
|
||||
"type": "TEST_GROUP",
|
||||
"color": "orange",
|
||||
"textColor": "white"
|
||||
}],
|
||||
"GROUP_2": [{
|
||||
"name": "Event 2",
|
||||
"start": 1665409597000,
|
||||
"end": 1665456252000,
|
||||
"type": "GROUP_2",
|
||||
"color": "red",
|
||||
"textColor": "white"
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
## Plans using JSON file uploads
|
||||
Plan domain objects can be created by uploading a JSON file with the format above to render categories and activities.
|
||||
|
||||
## Using Domain Objects directly
|
||||
If uploading a JSON is not desired, it is possible to visualize domain objects of type 'plan'.
|
||||
The standard model is as follows:
|
||||
```javascript
|
||||
{
|
||||
identifier: {
|
||||
namespace: ""
|
||||
key: "test-plan"
|
||||
}
|
||||
name:"A plan object",
|
||||
type:"plan",
|
||||
location:"ROOT",
|
||||
selectFile: {
|
||||
body: {
|
||||
SOME_CATEGORY: [{
|
||||
name: "An activity",
|
||||
start: 1665323197000,
|
||||
end: 1665323197100,
|
||||
type: "SOME_CATEGORY"
|
||||
}
|
||||
],
|
||||
ANOTHER_CATEGORY: [{
|
||||
name: "An activity",
|
||||
start: 1665323197000,
|
||||
end: 1665323197100,
|
||||
type: "ANOTHER_CATEGORY"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If your data has non-standard keys for `start, end, type and activities` properties, use the `sourceMap` property mapping.
|
||||
```javascript
|
||||
{
|
||||
identifier: {
|
||||
namespace: ""
|
||||
key: "another-test-plan"
|
||||
}
|
||||
name:"Another plan object",
|
||||
type:"plan",
|
||||
location:"ROOT",
|
||||
sourceMap: {
|
||||
start: 'start_time',
|
||||
end: 'end_time',
|
||||
activities: 'items',
|
||||
groupId: 'category'
|
||||
},
|
||||
selectFile: {
|
||||
body: {
|
||||
items: [
|
||||
{
|
||||
name: "An activity",
|
||||
start_time: 1665323197000,
|
||||
end_time: 1665323197100,
|
||||
category: "SOME_CATEGORY"
|
||||
},
|
||||
{
|
||||
name: "Another activity",
|
||||
start_time: 1665323198000,
|
||||
end_time: 1665323198100,
|
||||
category: "ANOTHER_CATEGORY"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Rendering categories and activities:
|
||||
The algorithm to render categories and activities on a canvas is as follows:
|
||||
* Each category gets a swimlane.
|
||||
* Activities within a category are rendered within it's swimlane.
|
||||
* Render each activity on a given row if it's duration+label do not overlap (start/end times) with an existing activity on that row.
|
||||
* Move to the next available row within a swimlane if there is overlap
|
||||
* Labels for a given activity will be rendered within it's duration slot if it fits in that rectangular space.
|
||||
* Labels that do not fit within an activity's duration slot are rendered outside, to the right of the duration slot.
|
||||
|
||||
@@ -264,7 +264,7 @@ describe('the plugin', function () {
|
||||
it('provides an inspector view with the version information if available', () => {
|
||||
componentObject = component.$root.$children[0];
|
||||
const propertiesEls = componentObject.$el.querySelectorAll('.c-inspect-properties__row');
|
||||
expect(propertiesEls.length).toEqual(4);
|
||||
expect(propertiesEls.length).toEqual(6);
|
||||
const found = Array.from(propertiesEls).some((propertyEl) => {
|
||||
return (propertyEl.children[0].innerHTML.trim() === 'Version'
|
||||
&& propertyEl.children[1].innerHTML.trim() === 'v1');
|
||||
|
||||
@@ -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);
|
||||
@@ -438,7 +442,8 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
removeSeries(plotSeries) {
|
||||
removeSeries(plotSeries, index) {
|
||||
this.seriesModels.splice(index, 1);
|
||||
this.checkSameRangeValue();
|
||||
this.stopListening(plotSeries);
|
||||
},
|
||||
@@ -576,9 +581,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 +605,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 +847,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 +1085,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 +1118,6 @@ export default {
|
||||
this.freeze();
|
||||
},
|
||||
|
||||
play() {
|
||||
this.clear();
|
||||
},
|
||||
|
||||
showSynchronizeDialog() {
|
||||
const isLocalClock = this.timeContext.clock();
|
||||
if (isLocalClock !== undefined) {
|
||||
@@ -1172,7 +1181,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 +1192,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);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -93,6 +93,9 @@ export default function PlotViewProvider(openmct) {
|
||||
destroy: function () {
|
||||
component.$destroy();
|
||||
component = undefined;
|
||||
},
|
||||
getComponent() {
|
||||
return component;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -382,7 +382,7 @@ export default {
|
||||
makeLimitLines(series) {
|
||||
this.clearLimitLines(series);
|
||||
|
||||
if (!series.get('limitLines')) {
|
||||
if (!series || !series.get('limitLines')) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -102,8 +102,8 @@ export default class Collection extends Model {
|
||||
throw new Error('model not found in collection.');
|
||||
}
|
||||
|
||||
this.emit('remove', model, index);
|
||||
this.models.splice(index, 1);
|
||||
this.emit('remove', model, index);
|
||||
}
|
||||
|
||||
destroy(model) {
|
||||
|
||||
@@ -151,16 +151,6 @@ export default class PlotSeries extends Model {
|
||||
this.limitEvaluator = this.openmct.telemetry.limitEvaluator(options.domainObject);
|
||||
this.limitDefinition = this.openmct.telemetry.limitDefinition(options.domainObject);
|
||||
this.limits = [];
|
||||
this.limitDefinition.limits().then(response => {
|
||||
this.limits = [];
|
||||
|
||||
if (response) {
|
||||
this.limits = response;
|
||||
}
|
||||
|
||||
this.emit('limits', this);
|
||||
|
||||
});
|
||||
this.openmct.time.on('bounds', this.updateLimits);
|
||||
this.removeMutationListener = this.openmct.objects.observe(
|
||||
this.domainObject,
|
||||
@@ -176,15 +166,6 @@ export default class PlotSeries extends Model {
|
||||
this.emit('limitBounds', bounds);
|
||||
}
|
||||
|
||||
locateOldObject(oldStyleParent) {
|
||||
return oldStyleParent.useCapability('composition')
|
||||
.then(function (children) {
|
||||
this.oldObject = children
|
||||
.filter(function (child) {
|
||||
return child.getId() === this.keyString;
|
||||
}, this)[0];
|
||||
}.bind(this));
|
||||
}
|
||||
/**
|
||||
* Fetch historical data and establish a realtime subscription. Returns
|
||||
* a promise that is resolved when all connections have been successfully
|
||||
@@ -192,7 +173,7 @@ export default class PlotSeries extends Model {
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
fetch(options) {
|
||||
async fetch(options) {
|
||||
let strategy;
|
||||
|
||||
if (this.model.interpolate !== 'none') {
|
||||
@@ -217,23 +198,19 @@ export default class PlotSeries extends Model {
|
||||
);
|
||||
}
|
||||
|
||||
/* eslint-disable you-dont-need-lodash-underscore/concat */
|
||||
return this.openmct
|
||||
.telemetry
|
||||
.request(this.domainObject, options)
|
||||
.then((points) => {
|
||||
const data = this.getSeriesData();
|
||||
const newPoints = _(data)
|
||||
.concat(points)
|
||||
.sortBy(this.getXVal)
|
||||
.uniq(true, point => [this.getXVal(point), this.getYVal(point)].join())
|
||||
.value();
|
||||
this.reset(newPoints);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn('Error fetching data', error);
|
||||
});
|
||||
/* eslint-enable you-dont-need-lodash-underscore/concat */
|
||||
try {
|
||||
const points = await this.openmct.telemetry.request(this.domainObject, options);
|
||||
const data = this.getSeriesData();
|
||||
// eslint-disable-next-line you-dont-need-lodash-underscore/concat
|
||||
const newPoints = _(data)
|
||||
.concat(points)
|
||||
.sortBy(this.getXVal)
|
||||
.uniq(true, point => [this.getXVal(point), this.getYVal(point)].join())
|
||||
.value();
|
||||
this.reset(newPoints);
|
||||
} catch (error) {
|
||||
console.warn('Error fetching data', error);
|
||||
}
|
||||
}
|
||||
|
||||
updateName(name) {
|
||||
@@ -334,16 +311,19 @@ export default class PlotSeries extends Model {
|
||||
* Override this to implement plot series loading functionality. Must return
|
||||
* a promise that is resolved when loading is completed.
|
||||
*
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
*/
|
||||
load(options) {
|
||||
return this.fetch(options)
|
||||
.then(function (res) {
|
||||
this.emit('load');
|
||||
async load(options) {
|
||||
await this.fetch(options);
|
||||
this.emit('load');
|
||||
const limitsResponse = await this.limitDefinition.limits();
|
||||
this.limits = [];
|
||||
if (limitsResponse) {
|
||||
this.limits = limitsResponse;
|
||||
}
|
||||
|
||||
return res;
|
||||
}.bind(this));
|
||||
this.emit('limits', this);
|
||||
this.emit('change:limitLines', this);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -19,8 +19,12 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
export default class RemoveAction {
|
||||
#transaction;
|
||||
|
||||
constructor(openmct) {
|
||||
|
||||
this.name = 'Remove';
|
||||
this.key = 'remove';
|
||||
this.description = 'Remove this object from its containing object.';
|
||||
@@ -29,17 +33,25 @@ export default class RemoveAction {
|
||||
this.priority = 1;
|
||||
|
||||
this.openmct = openmct;
|
||||
|
||||
this.removeFromComposition = this.removeFromComposition.bind(this); // for access to private transaction variable
|
||||
}
|
||||
|
||||
invoke(objectPath) {
|
||||
async invoke(objectPath) {
|
||||
let object = objectPath[0];
|
||||
let parent = objectPath[1];
|
||||
this.showConfirmDialog(object).then(() => {
|
||||
this.removeFromComposition(parent, object);
|
||||
if (this.inNavigationPath(object)) {
|
||||
this.navigateTo(objectPath.slice(1));
|
||||
}
|
||||
}).catch(() => {});
|
||||
|
||||
try {
|
||||
await this.showConfirmDialog(object);
|
||||
} catch (error) {
|
||||
return; // form canceled, exit invoke
|
||||
}
|
||||
|
||||
await this.removeFromComposition(parent, object);
|
||||
|
||||
if (this.inNavigationPath(object)) {
|
||||
this.navigateTo(objectPath.slice(1));
|
||||
}
|
||||
}
|
||||
|
||||
showConfirmDialog(object) {
|
||||
@@ -81,20 +93,21 @@ export default class RemoveAction {
|
||||
this.openmct.router.navigate('#/browse/' + urlPath);
|
||||
}
|
||||
|
||||
removeFromComposition(parent, child) {
|
||||
let composition = parent.composition.filter(id =>
|
||||
!this.openmct.objects.areIdsEqual(id, child.identifier)
|
||||
);
|
||||
async removeFromComposition(parent, child) {
|
||||
this.startTransaction();
|
||||
|
||||
this.openmct.objects.mutate(parent, 'composition', composition);
|
||||
const composition = this.openmct.composition.get(parent);
|
||||
composition.remove(child);
|
||||
|
||||
if (!this.isAlias(child, parent)) {
|
||||
this.openmct.objects.mutate(child, 'location', null);
|
||||
}
|
||||
|
||||
if (this.inNavigationPath(child) && this.openmct.editor.isEditing()) {
|
||||
this.openmct.editor.save();
|
||||
}
|
||||
|
||||
if (!this.isAlias(child, parent)) {
|
||||
this.openmct.objects.mutate(child, 'location', null);
|
||||
}
|
||||
await this.saveTransaction();
|
||||
}
|
||||
|
||||
isAlias(child, parent) {
|
||||
@@ -132,4 +145,23 @@ export default class RemoveAction {
|
||||
&& parentType.definition.creatable
|
||||
&& Array.isArray(parent.composition);
|
||||
}
|
||||
|
||||
startTransaction() {
|
||||
if (!this.openmct.objects.isTransactionActive()) {
|
||||
this.#transaction = this.openmct.objects.startTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
saveTransaction() {
|
||||
if (!this.#transaction) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.#transaction.commit()
|
||||
.catch(error => {
|
||||
throw error;
|
||||
}).finally(() => {
|
||||
this.openmct.objects.endTransaction();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
>
|
||||
<div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left">
|
||||
<button
|
||||
aria-label="Time Conductor History"
|
||||
class="c-button--menu c-history-button icon-history"
|
||||
@click.prevent.stop="showHistoryMenu"
|
||||
>
|
||||
@@ -144,10 +145,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 +255,7 @@ export default {
|
||||
|
||||
return maxRecordsLength;
|
||||
},
|
||||
formatTime(time) {
|
||||
formatTime(time, utcDateFormat) {
|
||||
let format = this.timeSystem.timeFormat;
|
||||
let isNegativeOffset = false;
|
||||
|
||||
@@ -274,7 +276,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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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');
|
||||
|
||||
return result;
|
||||
// Soft delete annotations that match tag instead
|
||||
const annotationsToDelete = this.annotations.filter((annotation) => {
|
||||
return annotation.tags.includes(tagToRemove);
|
||||
});
|
||||
if (annotationsToDelete) {
|
||||
await this.openmct.annotation.deleteAnnotations(annotationsToDelete);
|
||||
this.$emit('tags-updated', annotationsToDelete);
|
||||
}
|
||||
},
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -33,7 +33,8 @@
|
||||
class="c-tree__item c-elements-pool__item"
|
||||
:class="{
|
||||
'is-context-clicked': contextClickActive,
|
||||
'hover': hover
|
||||
'hover': hover,
|
||||
'is-alias': isAlias
|
||||
}"
|
||||
>
|
||||
<span
|
||||
@@ -55,6 +56,7 @@ export default {
|
||||
components: {
|
||||
ObjectLabel
|
||||
},
|
||||
inject: ['openmct'],
|
||||
props: {
|
||||
index: {
|
||||
type: Number,
|
||||
@@ -82,9 +84,12 @@ export default {
|
||||
}
|
||||
},
|
||||
data() {
|
||||
const isAlias = this.elementObject.location !== this.openmct.objects.makeKeyString(this.parentObject.identifier);
|
||||
|
||||
return {
|
||||
contextClickActive: false,
|
||||
hover: false
|
||||
hover: false,
|
||||
isAlias
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -38,6 +38,8 @@ describe('the inspector', () => {
|
||||
folderItem = {
|
||||
name: 'folder',
|
||||
type: 'folder',
|
||||
createdBy: 'John Q',
|
||||
modifiedBy: 'Public',
|
||||
id: 'mock-folder-key',
|
||||
identifier: {
|
||||
namespace: '',
|
||||
@@ -74,6 +76,8 @@ describe('the inspector', () => {
|
||||
const [
|
||||
title,
|
||||
type,
|
||||
createdBy,
|
||||
modifiedBy,
|
||||
notes,
|
||||
timestamp
|
||||
] = details;
|
||||
@@ -87,6 +91,14 @@ describe('the inspector', () => {
|
||||
.toEqual('Type');
|
||||
expect(type.value.toLowerCase())
|
||||
.toEqual(folderItem.type);
|
||||
expect(createdBy.name)
|
||||
.toEqual('Created By');
|
||||
expect(createdBy.value)
|
||||
.toEqual(folderItem.createdBy);
|
||||
expect(modifiedBy.name)
|
||||
.toEqual('Modified By');
|
||||
expect(modifiedBy.value)
|
||||
.toEqual(folderItem.modifiedBy);
|
||||
expect(notes.value)
|
||||
.toEqual('This object should have some notes');
|
||||
|
||||
|
||||
@@ -90,10 +90,13 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
const UNKNOWN_USER = 'Unknown';
|
||||
const title = this.domainObject.name;
|
||||
const typeName = this.type ? this.type.definition.name : `Unknown: ${this.domainObject.type}`;
|
||||
const timestampLabel = this.domainObject.modified ? 'Modified' : 'Created';
|
||||
const timestamp = this.domainObject.modified ? this.domainObject.modified : this.domainObject.created;
|
||||
const createdTimestamp = this.domainObject.created;
|
||||
const createdBy = this.domainObject.createdBy ? this.domainObject.createdBy : UNKNOWN_USER;
|
||||
const modifiedBy = this.domainObject.modifiedBy ? this.domainObject.modifiedBy : UNKNOWN_USER;
|
||||
const modifiedTimestamp = this.domainObject.modified ? this.domainObject.modified : this.domainObject.created;
|
||||
const notes = this.domainObject.notes;
|
||||
const version = this.domainObject.version;
|
||||
|
||||
@@ -105,6 +108,14 @@ export default {
|
||||
{
|
||||
name: 'Type',
|
||||
value: typeName
|
||||
},
|
||||
{
|
||||
name: 'Created By',
|
||||
value: createdBy
|
||||
},
|
||||
{
|
||||
name: 'Modified By',
|
||||
value: modifiedBy
|
||||
}
|
||||
];
|
||||
|
||||
@@ -115,15 +126,28 @@ export default {
|
||||
});
|
||||
}
|
||||
|
||||
if (timestamp !== undefined) {
|
||||
const formattedTimestamp = Moment.utc(timestamp)
|
||||
if (createdTimestamp !== undefined) {
|
||||
const formattedCreatedTimestamp = Moment.utc(createdTimestamp)
|
||||
.format('YYYY-MM-DD[\n]HH:mm:ss')
|
||||
+ ' UTC';
|
||||
|
||||
details.push(
|
||||
{
|
||||
name: timestampLabel,
|
||||
value: formattedTimestamp
|
||||
name: 'Created',
|
||||
value: formattedCreatedTimestamp
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (modifiedTimestamp !== undefined) {
|
||||
const formattedModifiedTimestamp = Moment.utc(modifiedTimestamp)
|
||||
.format('YYYY-MM-DD[\n]HH:mm:ss')
|
||||
+ ' UTC';
|
||||
|
||||
details.push(
|
||||
{
|
||||
name: 'Modified',
|
||||
value: formattedModifiedTimestamp
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,15 @@
|
||||
margin-top: $interiorMargin;
|
||||
}
|
||||
|
||||
&__item {
|
||||
&.is-alias {
|
||||
// Object is an alias to an original.
|
||||
[class*='__type-icon'] {
|
||||
@include isAlias();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__search {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
@@ -232,6 +232,8 @@ describe("GrandSearch", () => {
|
||||
it("should render an object search result if new object added", async () => {
|
||||
const composition = openmct.composition.get(mockFolderObject);
|
||||
composition.add(mockNewObject);
|
||||
// after adding, need to wait a beat for the folder to be indexed
|
||||
await Vue.nextTick();
|
||||
await grandSearchComponent.$children[0].searchEverything('apple');
|
||||
await Vue.nextTick();
|
||||
const searchResults = document.querySelectorAll('[aria-label="New Apple Test Folder folder result"]');
|
||||
@@ -271,6 +273,13 @@ describe("GrandSearch", () => {
|
||||
expect(annotationResults[1].innerText).toContain('Driving');
|
||||
});
|
||||
|
||||
it("should render no annotation search results if no match", async () => {
|
||||
await grandSearchComponent.$children[0].searchEverything('Qbert');
|
||||
await Vue.nextTick();
|
||||
const annotationResults = document.querySelectorAll('[aria-label="Search Result"]');
|
||||
expect(annotationResults.length).toBe(0);
|
||||
});
|
||||
|
||||
it("should preview object search results in edit mode if object clicked", async () => {
|
||||
await grandSearchComponent.$children[0].searchEverything('Folder');
|
||||
grandSearchComponent._provided.openmct.router.path = [mockDisplayLayout];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,25 @@ module.exports = merge(common, {
|
||||
__OPENMCT_ROOT_RELATIVE__: '"dist/"'
|
||||
})
|
||||
],
|
||||
devtool: 'eval-source-map'
|
||||
devtool: 'eval-source-map',
|
||||
devServer: {
|
||||
devMiddleware: {
|
||||
writeToDisk: (filePathString) => {
|
||||
const filePath = path.parse(filePathString);
|
||||
const shouldWrite = !(filePath.base.includes('hot-update'));
|
||||
|
||||
return shouldWrite;
|
||||
}
|
||||
},
|
||||
watchFiles: ['**/*.css'],
|
||||
static: {
|
||||
directory: path.join(__dirname, '/dist'),
|
||||
publicPath: '/dist',
|
||||
watch: false
|
||||
},
|
||||
client: {
|
||||
progress: true,
|
||||
overlay: true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user