Compare commits
167 Commits
fix-2909
...
joel/plot-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1d1301542 | ||
|
|
aef8589872 | ||
|
|
38f3e60884 | ||
|
|
331c086a60 | ||
|
|
0890ebfd0f | ||
|
|
1547ea5cf7 | ||
|
|
b1551478e4 | ||
|
|
213c469758 | ||
|
|
03ac43d306 | ||
|
|
f3de6f2548 | ||
|
|
62dfb7d8f9 | ||
|
|
0a33cbeea2 | ||
|
|
4908d5afb0 | ||
|
|
aa4bf5eaf6 | ||
|
|
4eb4cbfffc | ||
|
|
eda01abcbc | ||
|
|
694b8f4666 | ||
|
|
4e4031e700 | ||
|
|
46a20bb76d | ||
|
|
1d482f318e | ||
|
|
6826b579a6 | ||
|
|
bbb271a678 | ||
|
|
fec1438806 | ||
|
|
28f19ec310 | ||
|
|
3d4c721232 | ||
|
|
190d34c939 | ||
|
|
47bc0e9793 | ||
|
|
35115aaa50 | ||
|
|
f934454c25 | ||
|
|
b252051c83 | ||
|
|
4ca1181eb6 | ||
|
|
eb49ffae02 | ||
|
|
5751012872 | ||
|
|
f973f42729 | ||
|
|
4788561631 | ||
|
|
e8699d54d5 | ||
|
|
14bc49451b | ||
|
|
dd2e8c0460 | ||
|
|
031753a9a8 | ||
|
|
8edae5f02c | ||
|
|
7ed3de01b9 | ||
|
|
aa041e04cf | ||
|
|
24e7ea143a | ||
|
|
79d5d9c4d0 | ||
|
|
b5bfdc4418 | ||
|
|
59730c60ec | ||
|
|
4a87a5d847 | ||
|
|
4a1931f594 | ||
|
|
f174dcc2f6 | ||
|
|
0c741c67f8 | ||
|
|
85da5e43b3 | ||
|
|
d26e45f636 | ||
|
|
0bd6814097 | ||
|
|
4e7410b4bf | ||
|
|
436550adba | ||
|
|
421c09ec2c | ||
|
|
f6b1be0486 | ||
|
|
eca12201c7 | ||
|
|
68ff69664b | ||
|
|
0679b246b8 | ||
|
|
51fbb9cee6 | ||
|
|
437156e04c | ||
|
|
d244ee622a | ||
|
|
043d6aa9c3 | ||
|
|
ecfab8f7f3 | ||
|
|
05c38c37aa | ||
|
|
ce78925119 | ||
|
|
b30018c315 | ||
|
|
26aca0f433 | ||
|
|
eb5eb7d540 | ||
|
|
609a47c62c | ||
|
|
83f9c6c528 | ||
|
|
41259bbd40 | ||
|
|
580640ff47 | ||
|
|
a5f3ba6259 | ||
|
|
a70facf0c8 | ||
|
|
447fe94325 | ||
|
|
8e2b666766 | ||
|
|
dcbfbdbb89 | ||
|
|
4c76bf34ab | ||
|
|
688a03c8ac | ||
|
|
81b7a9d3e0 | ||
|
|
dc573c479c | ||
|
|
4f12f41685 | ||
|
|
a4aec5d492 | ||
|
|
23303c910e | ||
|
|
00b3f3ac0b | ||
|
|
3282934cf6 | ||
|
|
c157fab081 | ||
|
|
7c07b66cc9 | ||
|
|
7a906ccf5c | ||
|
|
ff7debfb81 | ||
|
|
92ba103f45 | ||
|
|
2c2d8d6b56 | ||
|
|
189882afc8 | ||
|
|
cfadb9f4fd | ||
|
|
c185f77a15 | ||
|
|
396817b2d1 | ||
|
|
96eb6d6b74 | ||
|
|
2b3541a323 | ||
|
|
7c9a140481 | ||
|
|
5e9b313ee9 | ||
|
|
c64db6c07d | ||
|
|
9e2751acf7 | ||
|
|
51fb72dc04 | ||
|
|
cb5d47f66f | ||
|
|
d51052ab46 | ||
|
|
ea90d02d66 | ||
|
|
95f73d8eb8 | ||
|
|
0dff431f4a | ||
|
|
f62010fb99 | ||
|
|
a37c686993 | ||
|
|
f12166097c | ||
|
|
61d238a097 | ||
|
|
ba4ef43673 | ||
|
|
f6c16b7483 | ||
|
|
58e63e649f | ||
|
|
d103a22fa0 | ||
|
|
04a60cfcbb | ||
|
|
8d723960f4 | ||
|
|
6d3cd2c699 | ||
|
|
87bf94fe0a | ||
|
|
af93823b6f | ||
|
|
7a04aea90e | ||
|
|
0bb475327c | ||
|
|
c474b998f0 | ||
|
|
f9deb80350 | ||
|
|
4a39ddf425 | ||
|
|
83c273b976 | ||
|
|
a02d421093 | ||
|
|
9026099fd2 | ||
|
|
295ccea195 | ||
|
|
021d730814 | ||
|
|
7dd81beb03 | ||
|
|
1842d3923c | ||
|
|
26838635b6 | ||
|
|
ae62b15abf | ||
|
|
ba41c1a30e | ||
|
|
b9a85d9c4d | ||
|
|
11f2c35bb2 | ||
|
|
80eab8bad1 | ||
|
|
b2d8d640ae | ||
|
|
766f48c1ba | ||
|
|
da7b93f9b3 | ||
|
|
56e6fa66c2 | ||
|
|
99c095a69f | ||
|
|
f885e83505 | ||
|
|
928bc4c68a | ||
|
|
d5539c7ae4 | ||
|
|
c86a104fb6 | ||
|
|
9fa4707c82 | ||
|
|
7e2cfa36de | ||
|
|
aaa60a1545 | ||
|
|
717231fed2 | ||
|
|
7fb2bc9729 | ||
|
|
addeb635e9 | ||
|
|
608d63a7b0 | ||
|
|
10679e5f4f | ||
|
|
38b8f03b1a | ||
|
|
779a42c28c | ||
|
|
80c2504768 | ||
|
|
80359e3f16 | ||
|
|
3b6ef9b44b | ||
|
|
66aa4f099f | ||
|
|
aa6c6cb88b | ||
|
|
4e5cc840d7 | ||
|
|
c68edd9b7d |
@@ -20,8 +20,8 @@ jobs:
|
||||
paths:
|
||||
- node_modules
|
||||
- run:
|
||||
name: npm run test
|
||||
command: npm run test
|
||||
name: npm run test:coverage
|
||||
command: npm run test:coverage
|
||||
- run:
|
||||
name: npm run lint
|
||||
command: npm run lint
|
||||
|
||||
4
API.md
4
API.md
@@ -427,8 +427,8 @@ Each telemetry value description has an object defining hints. Keys in this thi
|
||||
|
||||
Known hints:
|
||||
|
||||
* `domain`: Indicates that the value represents the "input" of a datum. Values with a `domain` hint will be used for the x-axis of a plot, and tables will render columns for these values first.
|
||||
* `range`: Indicates that the value is the "output" of a datum. Values with a `range` hint will be used as the y-axis on a plot, and tables will render columns for these values after the `domain` values.
|
||||
* `domain`: Values with a `domain` hint will be used for the x-axis of a plot, and tables will render columns for these values first.
|
||||
* `range`: Values with a `range` hint will be used as the y-axis on a plot, and tables will render columns for these values after the `domain` values.
|
||||
* `image`: Indicates that the value may be interpreted as the URL to an image file, in which case appropriate views will be made available.
|
||||
|
||||
##### The Time Conductor and Telemetry
|
||||
|
||||
@@ -103,7 +103,7 @@ the name chosen could not be mistaken for a topic or master branch.
|
||||
### Merging
|
||||
|
||||
When development is complete on an issue, the first step toward merging it
|
||||
back into the master branch is to file a Pull Request. The contributions
|
||||
back into the master branch is to file a Pull Request (PR). The contributions
|
||||
should meet code, test, and commit message standards as described below,
|
||||
and the pull request should include a completed author checklist, also
|
||||
as described below. Pull requests may be assigned to specific team
|
||||
@@ -114,6 +114,15 @@ request. When the reviewer is satisfied, they should add a comment to
|
||||
the pull request containing the reviewer checklist (from below) and complete
|
||||
the merge back to the master branch.
|
||||
|
||||
Additionally:
|
||||
* Every pull request must link to the issue that it addresses. Eg. “Addresses #1234” or “Closes #1234”. This is the responsibility of the pull request’s __author__. If no issue exists, create one.
|
||||
* Every __author__ must include testing instructions. These instructions should identify the areas of code affected, and some minimal test steps. If addressing a bug, reproduction steps should be included, if they were not included in the original issue. If reproduction steps were included on the original issue, and are sufficient, refer to them.
|
||||
* A pull request that closes an issue should say so in the description. Including the text “Closes #1234” will cause the linked issue to be automatically closed when the pull request is merged. This is the responsibility of the pull request’s __author__.
|
||||
* When a pull request is merged, and the corresponding issue closed, the __reviewer__ must add the tag “unverified” to the original issue. This will indicate that although the issue is closed, it has not been tested yet.
|
||||
* Every PR must have two reviewers assigned, though only one approval is necessary for merge.
|
||||
* Changes to API require approval by a senior developer.
|
||||
* When creating a PR, it is the author's responsibility to apply any priority label from the issue to the PR as well. This helps with prioritization.
|
||||
|
||||
## Standards
|
||||
|
||||
Contributions to Open MCT are expected to meet the following standards.
|
||||
@@ -292,6 +301,7 @@ checklist).
|
||||
2. Unit tests included and/or updated with changes?
|
||||
3. Command line build passes?
|
||||
4. Changes have been smoke-tested?
|
||||
5. Testing instructions included?
|
||||
|
||||
### Reviewer Checklist
|
||||
|
||||
@@ -299,3 +309,4 @@ checklist).
|
||||
2. Appropriate unit tests included?
|
||||
3. Code style and in-line documentation are appropriate?
|
||||
4. Commit messages meet standards?
|
||||
5. Has associated issue been labelled `unverified`? (only applicable if this PR closes the issue)
|
||||
|
||||
@@ -50,7 +50,8 @@ define([
|
||||
values: [
|
||||
{
|
||||
key: "name",
|
||||
name: "Name"
|
||||
name: "Name",
|
||||
format: "string"
|
||||
},
|
||||
{
|
||||
key: "utc",
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<template>
|
||||
<div class="example">{{ msg }}</div>
|
||||
<div class="example">{{ msg }}</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
msg: 'Hello world!'
|
||||
data() {
|
||||
return {
|
||||
msg: 'Hello world!'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!--
|
||||
Open MCT, Copyright (c) 2014-2017, United States Government
|
||||
Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
as represented by the Administrator of the National Aeronautics and Space
|
||||
Administration. All rights reserved.
|
||||
|
||||
@@ -43,9 +43,9 @@
|
||||
openmct.legacyRegistry.enable.bind(openmct.legacyRegistry)
|
||||
);
|
||||
|
||||
openmct.install(openmct.plugins.LocalStorage());
|
||||
openmct.install(openmct.plugins.Espresso());
|
||||
openmct.install(openmct.plugins.MyItems());
|
||||
openmct.install(openmct.plugins.LocalStorage());
|
||||
openmct.install(openmct.plugins.Generator());
|
||||
openmct.install(openmct.plugins.ExampleImagery());
|
||||
openmct.install(openmct.plugins.UTCTimeSystem());
|
||||
|
||||
@@ -24,16 +24,27 @@
|
||||
|
||||
const devMode = process.env.NODE_ENV !== 'production';
|
||||
const browsers = [process.env.NODE_ENV === 'debug' ? 'ChromeDebugging' : 'ChromeHeadless'];
|
||||
const coverageEnabled = process.env.COVERAGE === 'true';
|
||||
const reporters = ['progress', 'html'];
|
||||
|
||||
if (coverageEnabled) {
|
||||
reporters.push('coverage-istanbul');
|
||||
}
|
||||
|
||||
module.exports = (config) => {
|
||||
const webpackConfig = require('./webpack.config.js');
|
||||
delete webpackConfig.output;
|
||||
|
||||
if (!devMode) {
|
||||
if (!devMode || coverageEnabled) {
|
||||
webpackConfig.module.rules.push({
|
||||
test: /\.js$/,
|
||||
exclude: /node_modules|example/,
|
||||
use: 'istanbul-instrumenter-loader'
|
||||
exclude: /node_modules|example|lib|dist/,
|
||||
use: {
|
||||
loader: 'istanbul-instrumenter-loader',
|
||||
options: {
|
||||
esModules: true
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -45,11 +56,7 @@ module.exports = (config) => {
|
||||
'src/**/*Spec.js'
|
||||
],
|
||||
port: 9876,
|
||||
reporters: [
|
||||
'progress',
|
||||
'coverage',
|
||||
'html'
|
||||
],
|
||||
reporters: reporters,
|
||||
browsers: browsers,
|
||||
customLaunchers: {
|
||||
ChromeDebugging: {
|
||||
@@ -61,27 +68,27 @@ module.exports = (config) => {
|
||||
colors: true,
|
||||
logLevel: config.LOG_INFO,
|
||||
autoWatch: true,
|
||||
coverageReporter: {
|
||||
dir: process.env.CIRCLE_ARTIFACTS ?
|
||||
process.env.CIRCLE_ARTIFACTS + '/coverage' :
|
||||
"dist/reports/coverage",
|
||||
check: {
|
||||
global: {
|
||||
lines: 80,
|
||||
excludes: ['src/plugins/plot/**/*.js']
|
||||
}
|
||||
}
|
||||
},
|
||||
// HTML test reporting.
|
||||
htmlReporter: {
|
||||
outputDir: "dist/reports/tests",
|
||||
preserveDescribeNesting: true,
|
||||
foldAll: false
|
||||
},
|
||||
coverageIstanbulReporter: {
|
||||
fixWebpackSourcePaths: true,
|
||||
dir: process.env.CIRCLE_ARTIFACTS ?
|
||||
process.env.CIRCLE_ARTIFACTS + '/coverage' :
|
||||
"dist/reports/coverage",
|
||||
reports: ['html', 'lcovonly', 'text-summary'],
|
||||
thresholds: {
|
||||
global: {
|
||||
lines: 62
|
||||
}
|
||||
}
|
||||
},
|
||||
preprocessors: {
|
||||
// add webpack as preprocessor
|
||||
'platform/**/*Spec.js': [ 'webpack', 'sourcemap' ],
|
||||
'src/**/*Spec.js': [ 'webpack', 'sourcemap' ]
|
||||
'platform/**/*Spec.js': ['webpack', 'sourcemap'],
|
||||
'src/**/*Spec.js': ['webpack', 'sourcemap']
|
||||
},
|
||||
webpack: webpackConfig,
|
||||
webpackMiddleware: {
|
||||
|
||||
20
package.json
20
package.json
@@ -2,10 +2,11 @@
|
||||
"name": "openmct",
|
||||
"version": "1.0.0-snapshot",
|
||||
"description": "The Open MCT core platform",
|
||||
"dependencies": {},
|
||||
"dependencies": {
|
||||
"plotly.js-dist": "^1.54.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"acorn": "6.2.0",
|
||||
"angular": "1.4.14",
|
||||
"angular": "1.7.9",
|
||||
"angular-route": "1.4.14",
|
||||
"babel-eslint": "8.2.6",
|
||||
"comma-separated-values": "^3.6.4",
|
||||
@@ -43,6 +44,7 @@
|
||||
"karma-chrome-launcher": "^2.2.0",
|
||||
"karma-cli": "^1.0.1",
|
||||
"karma-coverage": "^1.1.2",
|
||||
"karma-coverage-istanbul-reporter": "^2.1.1",
|
||||
"karma-html-reporter": "^0.2.7",
|
||||
"karma-jasmine": "^1.1.2",
|
||||
"karma-sourcemap-loader": "^0.3.7",
|
||||
@@ -53,9 +55,9 @@
|
||||
"marked": "^0.3.5",
|
||||
"mini-css-extract-plugin": "^0.4.1",
|
||||
"minimist": "^1.1.1",
|
||||
"moment": "^2.11.1",
|
||||
"moment": "2.25.3",
|
||||
"moment-duration-format": "^2.2.2",
|
||||
"moment-timezone": "^0.5.21",
|
||||
"moment-timezone": "0.5.28",
|
||||
"node-bourbon": "^4.2.3",
|
||||
"node-sass": "^4.9.2",
|
||||
"painterro": "^0.2.65",
|
||||
@@ -76,14 +78,16 @@
|
||||
"zepto": "^1.2.0"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rm -rf ./dist",
|
||||
"start": "node app.js",
|
||||
"lint": "eslint platform example src/**/*.{js,vue} openmct.js",
|
||||
"lint:fix": "eslint platform example src/**/*.{js,vue} openmct.js --fix",
|
||||
"lint": "eslint platform example src --ext .js,.vue openmct.js",
|
||||
"lint:fix": "eslint platform example src --ext .js,.vue openmct.js --fix",
|
||||
"build:prod": "cross-env NODE_ENV=production webpack",
|
||||
"build:dev": "webpack",
|
||||
"build:watch": "webpack --watch",
|
||||
"test": "karma start --single-run",
|
||||
"test-debug": "cross-env NODE_ENV=debug karma start --no-single-run",
|
||||
"test:debug": "cross-env NODE_ENV=debug karma start --no-single-run",
|
||||
"test:coverage": "./scripts/test-coverage.sh",
|
||||
"test:watch": "karma start --no-single-run",
|
||||
"verify": "concurrently 'npm:test' 'npm:lint'",
|
||||
"jsdoc": "jsdoc -c jsdoc.json -R API.md -r -d dist/docs/api",
|
||||
|
||||
@@ -87,6 +87,11 @@ define([
|
||||
bootstrapper
|
||||
);
|
||||
|
||||
// Override of angular1.6 ! hashPrefix
|
||||
app.config(['$locationProvider', function ($locationProvider) {
|
||||
$locationProvider.hashPrefix('');
|
||||
}]);
|
||||
|
||||
// Apply logging levels; this must be done now, before the
|
||||
// first log statement.
|
||||
new LogLevel(logLevel).configure(app, $log);
|
||||
|
||||
@@ -71,7 +71,7 @@ define([
|
||||
},
|
||||
{
|
||||
"key": "ELASTIC_PATH",
|
||||
"value": "mct/domain_object",
|
||||
"value": "mct/_doc",
|
||||
"priority": "fallback"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -32,9 +32,9 @@ define(
|
||||
// JSLint doesn't like underscore-prefixed properties,
|
||||
// so hide them here.
|
||||
var SRC = "_source",
|
||||
REV = "_version",
|
||||
ID = "_id",
|
||||
CONFLICT = 409;
|
||||
CONFLICT = 409,
|
||||
SEQ_NO = "_seq_no",
|
||||
PRIMARY_TERM = "_primary_term";
|
||||
|
||||
/**
|
||||
* The ElasticPersistenceProvider reads and writes JSON documents
|
||||
@@ -104,7 +104,8 @@ define(
|
||||
// Get a domain object model out of ElasticSearch's response
|
||||
ElasticPersistenceProvider.prototype.getModel = function (response) {
|
||||
if (response && response[SRC]) {
|
||||
this.revs[response[ID]] = response[REV];
|
||||
this.revs[response[SEQ_NO]] = response[SEQ_NO];
|
||||
this.revs[response[PRIMARY_TERM]] = response[PRIMARY_TERM];
|
||||
return response[SRC];
|
||||
} else {
|
||||
return undefined;
|
||||
@@ -116,7 +117,8 @@ define(
|
||||
// indicate that the request failed.
|
||||
ElasticPersistenceProvider.prototype.checkResponse = function (response, key) {
|
||||
if (response && !response.error) {
|
||||
this.revs[key] = response[REV];
|
||||
this.revs[SEQ_NO] = response[SEQ_NO];
|
||||
this.revs[PRIMARY_TERM] = response[PRIMARY_TERM];
|
||||
return response;
|
||||
} else {
|
||||
return this.handleError(response, key);
|
||||
@@ -147,7 +149,7 @@ define(
|
||||
function checkUpdate(response) {
|
||||
return self.checkResponse(response, key);
|
||||
}
|
||||
return this.put(key, value, { version: this.revs[key] })
|
||||
return this.put(key, value)
|
||||
.then(checkUpdate);
|
||||
};
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ define(
|
||||
it("allows object creation", function () {
|
||||
var model = { someKey: "some value" };
|
||||
mockHttp.and.returnValue(mockPromise({
|
||||
data: { "_id": "abc", "_version": 1 }
|
||||
data: { "_id": "abc", "_seq_no": 1, "_primary_term": 1 }
|
||||
}));
|
||||
provider.createObject("testSpace", "abc", model).then(capture);
|
||||
expect(mockHttp).toHaveBeenCalledWith({
|
||||
@@ -100,7 +100,7 @@ define(
|
||||
it("allows object models to be read back", function () {
|
||||
var model = { someKey: "some value" };
|
||||
mockHttp.and.returnValue(mockPromise({
|
||||
data: { "_id": "abc", "_version": 1, "_source": model }
|
||||
data: { "_id": "abc", "_seq_no": 1, "_primary_term": 1, "_source": model }
|
||||
}));
|
||||
provider.readObject("testSpace", "abc").then(capture);
|
||||
expect(mockHttp).toHaveBeenCalledWith({
|
||||
@@ -117,19 +117,19 @@ define(
|
||||
|
||||
// First do a read to populate rev tags...
|
||||
mockHttp.and.returnValue(mockPromise({
|
||||
data: { "_id": "abc", "_version": 42, "_source": {} }
|
||||
data: { "_id": "abc", "_source": {} }
|
||||
}));
|
||||
provider.readObject("testSpace", "abc");
|
||||
|
||||
// Now perform an update
|
||||
mockHttp.and.returnValue(mockPromise({
|
||||
data: { "_id": "abc", "_version": 43, "_source": {} }
|
||||
data: { "_id": "abc", "_seq_no": 1, "_source": {} }
|
||||
}));
|
||||
provider.updateObject("testSpace", "abc", model).then(capture);
|
||||
expect(mockHttp).toHaveBeenCalledWith({
|
||||
url: "/test/db/abc",
|
||||
method: "PUT",
|
||||
params: { version: 42 },
|
||||
params: undefined,
|
||||
data: model
|
||||
});
|
||||
expect(capture.calls.mostRecent().args[0]).toBeTruthy();
|
||||
@@ -138,13 +138,13 @@ define(
|
||||
it("allows object deletion", function () {
|
||||
// First do a read to populate rev tags...
|
||||
mockHttp.and.returnValue(mockPromise({
|
||||
data: { "_id": "abc", "_version": 42, "_source": {} }
|
||||
data: { "_id": "abc", "_source": {} }
|
||||
}));
|
||||
provider.readObject("testSpace", "abc");
|
||||
|
||||
// Now perform an update
|
||||
mockHttp.and.returnValue(mockPromise({
|
||||
data: { "_id": "abc", "_version": 42, "_source": {} }
|
||||
data: { "_id": "abc", "_source": {} }
|
||||
}));
|
||||
provider.deleteObject("testSpace", "abc", {}).then(capture);
|
||||
expect(mockHttp).toHaveBeenCalledWith({
|
||||
@@ -167,13 +167,13 @@ define(
|
||||
expect(capture).toHaveBeenCalledWith(undefined);
|
||||
});
|
||||
|
||||
it("handles rejection due to version", function () {
|
||||
it("handles rejection due to _seq_no", function () {
|
||||
var model = { someKey: "some value" },
|
||||
mockErrorCallback = jasmine.createSpy('error');
|
||||
|
||||
// First do a read to populate rev tags...
|
||||
mockHttp.and.returnValue(mockPromise({
|
||||
data: { "_id": "abc", "_version": 42, "_source": {} }
|
||||
data: { "_id": "abc", "_seq_no": 1, "_source": {} }
|
||||
}));
|
||||
provider.readObject("testSpace", "abc");
|
||||
|
||||
@@ -196,7 +196,7 @@ define(
|
||||
|
||||
// First do a read to populate rev tags...
|
||||
mockHttp.and.returnValue(mockPromise({
|
||||
data: { "_id": "abc", "_version": 42, "_source": {} }
|
||||
data: { "_id": "abc", "_seq_no": 1, "_source": {} }
|
||||
}));
|
||||
provider.readObject("testSpace", "abc");
|
||||
|
||||
|
||||
2
scripts/test-coverage.sh
Executable file
2
scripts/test-coverage.sh
Executable file
@@ -0,0 +1,2 @@
|
||||
export NODE_OPTIONS=--max_old_space_size=4096
|
||||
cross-env COVERAGE=true karma start --single-run
|
||||
@@ -252,6 +252,7 @@ define([
|
||||
// Plugin's that are installed by default
|
||||
|
||||
this.install(this.plugins.Plot());
|
||||
this.install(this.plugins.PlotlyPlot());
|
||||
this.install(this.plugins.TelemetryTable());
|
||||
this.install(PreviewPlugin.default());
|
||||
this.install(LegacyIndicatorsPlugin());
|
||||
|
||||
39
src/plugins/LADTable/LADTableCompositionPolicy.js
Normal file
39
src/plugins/LADTable/LADTableCompositionPolicy.js
Normal file
@@ -0,0 +1,39 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2020, 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.
|
||||
*****************************************************************************/
|
||||
|
||||
export default class LADTableCompositionPolicy {
|
||||
|
||||
constructor(openmct) {
|
||||
this.openmct = openmct;
|
||||
return this.allow.bind(this);
|
||||
}
|
||||
|
||||
allow(parent, child) {
|
||||
if(parent.type === 'LadTable') {
|
||||
return this.openmct.telemetry.isTelemetryObject(child);
|
||||
} else if(parent.type === 'LadTableSet') {
|
||||
return child.type === 'LadTable';
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -19,53 +19,46 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import LadTableSet from './components/LadTableSet.vue';
|
||||
import Vue from 'vue';
|
||||
|
||||
define([
|
||||
'./components/LadTableSet.vue',
|
||||
'vue'
|
||||
], function (
|
||||
LadTableSet,
|
||||
Vue
|
||||
) {
|
||||
function LADTableSetViewProvider(openmct) {
|
||||
return {
|
||||
key: 'LadTableSet',
|
||||
name: 'LAD Table Set',
|
||||
cssClass: 'icon-tabular-lad-set',
|
||||
canView: function (domainObject) {
|
||||
return domainObject.type === 'LadTableSet';
|
||||
},
|
||||
canEdit: function (domainObject) {
|
||||
return domainObject.type === 'LadTableSet';
|
||||
},
|
||||
view: function (domainObject, objectPath) {
|
||||
let component;
|
||||
export default function LADTableSetViewProvider(openmct) {
|
||||
return {
|
||||
key: 'LadTableSet',
|
||||
name: 'LAD Table Set',
|
||||
cssClass: 'icon-tabular-lad-set',
|
||||
canView: function (domainObject) {
|
||||
return domainObject.type === 'LadTableSet';
|
||||
},
|
||||
canEdit: function (domainObject) {
|
||||
return domainObject.type === 'LadTableSet';
|
||||
},
|
||||
view: function (domainObject, objectPath) {
|
||||
let component;
|
||||
|
||||
return {
|
||||
show: function (element) {
|
||||
component = new Vue({
|
||||
el: element,
|
||||
components: {
|
||||
LadTableSet: LadTableSet.default
|
||||
},
|
||||
provide: {
|
||||
openmct,
|
||||
domainObject,
|
||||
objectPath
|
||||
},
|
||||
template: '<lad-table-set></lad-table-set>'
|
||||
});
|
||||
},
|
||||
destroy: function (element) {
|
||||
component.$destroy();
|
||||
component = undefined;
|
||||
}
|
||||
};
|
||||
},
|
||||
priority: function () {
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
}
|
||||
return LADTableSetViewProvider;
|
||||
});
|
||||
return {
|
||||
show: function (element) {
|
||||
component = new Vue({
|
||||
el: element,
|
||||
components: {
|
||||
LadTableSet: LadTableSet
|
||||
},
|
||||
provide: {
|
||||
openmct,
|
||||
domainObject,
|
||||
objectPath
|
||||
},
|
||||
template: '<lad-table-set></lad-table-set>'
|
||||
});
|
||||
},
|
||||
destroy: function (element) {
|
||||
component.$destroy();
|
||||
component = undefined;
|
||||
}
|
||||
};
|
||||
},
|
||||
priority: function () {
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -19,53 +19,46 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import LadTable from './components/LADTable.vue';
|
||||
import Vue from 'vue';
|
||||
|
||||
define([
|
||||
'./components/LADTable.vue',
|
||||
'vue'
|
||||
], function (
|
||||
LadTableComponent,
|
||||
Vue
|
||||
) {
|
||||
function LADTableViewProvider(openmct) {
|
||||
return {
|
||||
key: 'LadTable',
|
||||
name: 'LAD Table',
|
||||
cssClass: 'icon-tabular-lad',
|
||||
canView: function (domainObject) {
|
||||
return domainObject.type === 'LadTable';
|
||||
},
|
||||
canEdit: function (domainObject) {
|
||||
return domainObject.type === 'LadTable';
|
||||
},
|
||||
view: function (domainObject, objectPath) {
|
||||
let component;
|
||||
export default function LADTableViewProvider(openmct) {
|
||||
return {
|
||||
key: 'LadTable',
|
||||
name: 'LAD Table',
|
||||
cssClass: 'icon-tabular-lad',
|
||||
canView: function (domainObject) {
|
||||
return domainObject.type === 'LadTable';
|
||||
},
|
||||
canEdit: function (domainObject) {
|
||||
return domainObject.type === 'LadTable';
|
||||
},
|
||||
view: function (domainObject, objectPath) {
|
||||
let component;
|
||||
|
||||
return {
|
||||
show: function (element) {
|
||||
component = new Vue({
|
||||
el: element,
|
||||
components: {
|
||||
LadTableComponent: LadTableComponent.default
|
||||
},
|
||||
provide: {
|
||||
openmct,
|
||||
domainObject,
|
||||
objectPath
|
||||
},
|
||||
template: '<lad-table-component></lad-table-component>'
|
||||
});
|
||||
},
|
||||
destroy: function (element) {
|
||||
component.$destroy();
|
||||
component = undefined;
|
||||
}
|
||||
};
|
||||
},
|
||||
priority: function () {
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
}
|
||||
return LADTableViewProvider;
|
||||
});
|
||||
return {
|
||||
show: function (element) {
|
||||
component = new Vue({
|
||||
el: element,
|
||||
components: {
|
||||
LadTableComponent: LadTable
|
||||
},
|
||||
provide: {
|
||||
openmct,
|
||||
domainObject,
|
||||
objectPath
|
||||
},
|
||||
template: '<lad-table-component></lad-table-component>'
|
||||
});
|
||||
},
|
||||
destroy: function (element) {
|
||||
component.$destroy();
|
||||
component = undefined;
|
||||
}
|
||||
};
|
||||
},
|
||||
priority: function () {
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2018, United States Government
|
||||
* Open MCT, Copyright (c) 2014-2020, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
@@ -24,10 +24,8 @@
|
||||
<template>
|
||||
<tr @contextmenu.prevent="showContextMenu">
|
||||
<td>{{ name }}</td>
|
||||
<td>{{ timestamp }}</td>
|
||||
<td :class="valueClass">
|
||||
{{ value }}
|
||||
</td>
|
||||
<td>{{ formattedTimestamp }}</td>
|
||||
<td :class="valueClass">{{ value }}</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
@@ -58,10 +56,16 @@ export default {
|
||||
currentObjectPath
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
formattedTimestamp() {
|
||||
return this.timestamp !== '---' ? this.formats[this.timestampKey].format(this.timestamp) : this.timestamp;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
|
||||
this.formats = this.openmct.telemetry.getFormatMap(this.metadata);
|
||||
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
this.bounds = this.openmct.time.bounds();
|
||||
|
||||
this.limitEvaluator = this.openmct
|
||||
.telemetry
|
||||
@@ -76,6 +80,7 @@ export default {
|
||||
);
|
||||
|
||||
this.openmct.time.on('timeSystem', this.updateTimeSystem);
|
||||
this.openmct.time.on('bounds', this.updateBounds);
|
||||
|
||||
this.timestampKey = this.openmct.time.timeSystem().key;
|
||||
|
||||
@@ -89,43 +94,64 @@ export default {
|
||||
.telemetry
|
||||
.subscribe(this.domainObject, this.updateValues);
|
||||
|
||||
this.openmct
|
||||
.telemetry
|
||||
.request(this.domainObject, {strategy: 'latest'})
|
||||
.then((array) => this.updateValues(array[array.length - 1]));
|
||||
this.requestHistory();
|
||||
},
|
||||
destroyed() {
|
||||
this.stopWatchingMutation();
|
||||
this.unsubscribe();
|
||||
this.openmct.off('timeSystem', this.updateTimeSystem);
|
||||
this.openmct.time.off('timeSystem', this.updateTimeSystem);
|
||||
this.openmct.time.off('bounds', this.updateBounds);
|
||||
},
|
||||
methods: {
|
||||
updateValues(datum) {
|
||||
this.timestamp = this.formats[this.timestampKey].format(datum);
|
||||
this.value = this.formats[this.valueKey].format(datum);
|
||||
let newTimestamp = this.formats[this.timestampKey].parse(datum),
|
||||
shouldUpdate = this.timestamp === '---' || newTimestamp >= this.timestamp,
|
||||
limit;
|
||||
|
||||
var limit = this.limitEvaluator.evaluate(datum, this.valueMetadata);
|
||||
|
||||
if (limit) {
|
||||
this.valueClass = limit.cssClass;
|
||||
} else {
|
||||
this.valueClass = '';
|
||||
if(!this.inBounds(newTimestamp)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(shouldUpdate) {
|
||||
this.timestamp = this.formats[this.timestampKey].parse(datum);
|
||||
this.value = this.formats[this.valueKey].format(datum);
|
||||
limit = this.limitEvaluator.evaluate(datum, this.valueMetadata);
|
||||
|
||||
if (limit) {
|
||||
this.valueClass = limit.cssClass;
|
||||
} else {
|
||||
this.valueClass = '';
|
||||
}
|
||||
}
|
||||
},
|
||||
requestHistory() {
|
||||
this.timestamp = '---';
|
||||
this.openmct
|
||||
.telemetry
|
||||
.request(this.domainObject, {
|
||||
start: this.bounds.start,
|
||||
end: this.bounds.end,
|
||||
strategy: 'latest'
|
||||
})
|
||||
.then((data) => this.updateValues(data[data.length - 1]));
|
||||
},
|
||||
updateName(name) {
|
||||
this.name = name;
|
||||
},
|
||||
updateBounds(bounds, isTick) {
|
||||
this.bounds = bounds;
|
||||
if(!isTick) {
|
||||
this.requestHistory();
|
||||
}
|
||||
},
|
||||
inBounds(timestamp) {
|
||||
return timestamp >= this.bounds.start && timestamp <= this.bounds.end;
|
||||
},
|
||||
updateTimeSystem(timeSystem) {
|
||||
this.value = '---';
|
||||
this.timestamp = '---';
|
||||
this.valueClass = '';
|
||||
this.timestampKey = timeSystem.key;
|
||||
|
||||
this.openmct
|
||||
.telemetry
|
||||
.request(this.domainObject, {strategy: 'latest'})
|
||||
.then((array) => this.updateValues(array[array.length - 1]));
|
||||
|
||||
},
|
||||
showContextMenu(event) {
|
||||
this.openmct.contextMenu._showContextMenuForObjectPath(this.currentObjectPath, event.x, event.y, CONTEXT_MENU_ACTIONS);
|
||||
|
||||
@@ -88,4 +88,3 @@ export default {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -19,38 +19,36 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import LADTableViewProvider from './LADTableViewProvider';
|
||||
import LADTableSetViewProvider from './LADTableSetViewProvider';
|
||||
import LADTableCompositionPolicy from './LADTableCompositionPolicy';
|
||||
|
||||
define([
|
||||
'./LADTableViewProvider',
|
||||
'./LADTableSetViewProvider'
|
||||
], function (
|
||||
LADTableViewProvider,
|
||||
LADTableSetViewProvider
|
||||
) {
|
||||
return function plugin() {
|
||||
return function install(openmct) {
|
||||
openmct.objectViews.addProvider(new LADTableViewProvider(openmct));
|
||||
openmct.objectViews.addProvider(new LADTableSetViewProvider(openmct));
|
||||
export default function plugin() {
|
||||
return function install(openmct) {
|
||||
|
||||
openmct.types.addType('LadTable', {
|
||||
name: "LAD Table",
|
||||
creatable: true,
|
||||
description: "A Latest Available Data tabular view in which each row displays the values for one or more contained telemetry objects.",
|
||||
cssClass: 'icon-tabular-lad',
|
||||
initialize(domainObject) {
|
||||
domainObject.composition = [];
|
||||
}
|
||||
});
|
||||
openmct.objectViews.addProvider(new LADTableViewProvider(openmct));
|
||||
openmct.objectViews.addProvider(new LADTableSetViewProvider(openmct));
|
||||
|
||||
openmct.types.addType('LadTableSet', {
|
||||
name: "LAD Table Set",
|
||||
creatable: true,
|
||||
description: "A Latest Available Data tabular view in which each row displays the values for one or more contained telemetry objects.",
|
||||
cssClass: 'icon-tabular-lad-set',
|
||||
initialize(domainObject) {
|
||||
domainObject.composition = [];
|
||||
}
|
||||
});
|
||||
};
|
||||
openmct.types.addType('LadTable', {
|
||||
name: "LAD Table",
|
||||
creatable: true,
|
||||
description: "A Latest Available Data tabular view in which each row displays the values for one or more contained telemetry objects.",
|
||||
cssClass: 'icon-tabular-lad',
|
||||
initialize(domainObject) {
|
||||
domainObject.composition = [];
|
||||
}
|
||||
});
|
||||
|
||||
openmct.types.addType('LadTableSet', {
|
||||
name: "LAD Table Set",
|
||||
creatable: true,
|
||||
description: "A Latest Available Data tabular view in which each row displays the values for one or more contained telemetry objects.",
|
||||
cssClass: 'icon-tabular-lad-set',
|
||||
initialize(domainObject) {
|
||||
domainObject.composition = [];
|
||||
}
|
||||
});
|
||||
|
||||
openmct.composition.addPolicy(new LADTableCompositionPolicy(openmct));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
356
src/plugins/LADTable/pluginSpec.js
Normal file
356
src/plugins/LADTable/pluginSpec.js
Normal file
@@ -0,0 +1,356 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2018, 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 LadPlugin from './plugin.js';
|
||||
import Vue from 'vue';
|
||||
import {
|
||||
createOpenMct,
|
||||
getMockObjects,
|
||||
getMockTelemetry,
|
||||
getLatestTelemetry
|
||||
} from 'testTools';
|
||||
|
||||
let openmct,
|
||||
ladPlugin,
|
||||
parent,
|
||||
child;
|
||||
|
||||
let selectors = {};
|
||||
selectors.ladTableClass = '.c-table.c-lad-table';
|
||||
selectors.ladTableBodyRows = selectors.ladTableClass + ' tbody tr';
|
||||
selectors.ladTableBodyRowsFirstData = selectors.ladTableBodyRows + ' td:first-child';
|
||||
selectors.ladTableBodyRowsSecondtData = selectors.ladTableBodyRows + ' td:nth-child(2)';
|
||||
selectors.ladTableBodyRowsThirdData = selectors.ladTableBodyRows + ' td:nth-child(3)';
|
||||
selectors.ladTableFirstBodyRow = selectors.ladTableClass + ' tbody tr:first-child';
|
||||
selectors.ladTableFirstRowFirstData = selectors.ladTableBodyRows + ' td:first-child';
|
||||
selectors.ladTableFirstRowSecondData = selectors.ladTableBodyRows + ' td:nth-child(2)';
|
||||
selectors.ladTableFirstRowThirdData = selectors.ladTableBodyRows + ' td:nth-child(3)';
|
||||
|
||||
selectors.ladTableSetTableHeaders = selectors.ladTableClass + ' .c-table__group-header';
|
||||
|
||||
function utcTimeFormat(value) {
|
||||
return new Date(value).toISOString().replace('T', ' ')
|
||||
}
|
||||
|
||||
describe("The LAD Table", () => {
|
||||
|
||||
const ladTableKey = 'LadTable';
|
||||
let telemetryCount = 3,
|
||||
timeFormat = 'utc',
|
||||
mockTelemetry = getMockTelemetry({ count: telemetryCount, format: timeFormat }),
|
||||
mockObj = getMockObjects({
|
||||
objectKeyStrings: ['ladTable', 'telemetry'],
|
||||
format: timeFormat
|
||||
}),
|
||||
bounds = {
|
||||
start: 0,
|
||||
end: 4
|
||||
};
|
||||
|
||||
// add telemetry object as composition in lad table
|
||||
mockObj.ladTable.composition.push(mockObj.telemetry.identifier);
|
||||
|
||||
// this setups up the app
|
||||
beforeEach((done) => {
|
||||
const appHolder = document.createElement('div');
|
||||
appHolder.style.width = '640px';
|
||||
appHolder.style.height = '480px';
|
||||
|
||||
openmct = createOpenMct();
|
||||
|
||||
parent = document.createElement('div');
|
||||
child = document.createElement('div');
|
||||
parent.appendChild(child);
|
||||
|
||||
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([]));
|
||||
|
||||
ladPlugin = new LadPlugin();
|
||||
openmct.install(ladPlugin);
|
||||
|
||||
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({}));
|
||||
|
||||
openmct.time.bounds({ start: bounds.start, end: bounds.end });
|
||||
|
||||
openmct.on('start', done);
|
||||
openmct.start(appHolder);
|
||||
});
|
||||
|
||||
it("should provide a table view only for lad table objects", () => {
|
||||
let applicableViews = openmct.objectViews.get(mockObj.ladTable),
|
||||
ladTableView = applicableViews.find(
|
||||
(viewProvider) => viewProvider.key === ladTableKey
|
||||
);
|
||||
|
||||
expect(applicableViews.length).toEqual(1);
|
||||
expect(ladTableView).toBeDefined();
|
||||
});
|
||||
|
||||
describe('composition', () => {
|
||||
let ladTableCompositionCollection;
|
||||
|
||||
beforeEach(() => {
|
||||
ladTableCompositionCollection = openmct.composition.get(mockObj.ladTable);
|
||||
ladTableCompositionCollection.load();
|
||||
});
|
||||
|
||||
it("should accept telemetry producing objects", () => {
|
||||
expect(() => {
|
||||
ladTableCompositionCollection.add(mockObj.telemetry);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("should reject non-telemtry producing objects", () => {
|
||||
expect(()=> {
|
||||
ladTableCompositionCollection.add(mockObj.ladTable);
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("table view", () => {
|
||||
let applicableViews,
|
||||
ladTableViewProvider,
|
||||
ladTableView,
|
||||
anotherTelemetryObj = getMockObjects({
|
||||
objectKeyStrings: ['telemetry'],
|
||||
overwrite: {
|
||||
telemetry: {
|
||||
name: "New Telemetry Object",
|
||||
identifier: { namespace: "", key: "another-telemetry-object" }
|
||||
}
|
||||
}
|
||||
}).telemetry;
|
||||
|
||||
// add another telemetry object as composition in lad table to test multi rows
|
||||
mockObj.ladTable.composition.push(anotherTelemetryObj.identifier);
|
||||
|
||||
beforeEach(async () => {
|
||||
let telemetryRequestResolve,
|
||||
telemetryObjectResolve,
|
||||
anotherTelemetryObjectResolve;
|
||||
let telemetryRequestPromise = new Promise((resolve) => {
|
||||
telemetryRequestResolve = resolve;
|
||||
}),
|
||||
telemetryObjectPromise = new Promise((resolve) => {
|
||||
telemetryObjectResolve = resolve;
|
||||
}),
|
||||
anotherTelemetryObjectPromise = new Promise((resolve) => {
|
||||
anotherTelemetryObjectResolve = resolve;
|
||||
})
|
||||
openmct.telemetry.request.and.callFake(() => {
|
||||
telemetryRequestResolve(mockTelemetry);
|
||||
return telemetryRequestPromise;
|
||||
});
|
||||
openmct.objects.get.and.callFake((obj) => {
|
||||
if(obj.key === 'telemetry-object') {
|
||||
telemetryObjectResolve(mockObj.telemetry);
|
||||
return telemetryObjectPromise;
|
||||
} else {
|
||||
anotherTelemetryObjectResolve(anotherTelemetryObj);
|
||||
return anotherTelemetryObjectPromise;
|
||||
}
|
||||
});
|
||||
|
||||
openmct.time.bounds({ start: bounds.start, end: bounds.end });
|
||||
|
||||
applicableViews = openmct.objectViews.get(mockObj.ladTable);
|
||||
ladTableViewProvider = applicableViews.find((viewProvider) => viewProvider.key === ladTableKey);
|
||||
ladTableView = ladTableViewProvider.view(mockObj.ladTable, [mockObj.ladTable]);
|
||||
ladTableView.show(child, true);
|
||||
|
||||
await Promise.all([telemetryRequestPromise, telemetryObjectPromise, anotherTelemetryObjectPromise]);
|
||||
return await Vue.nextTick();
|
||||
});
|
||||
|
||||
it("should show one row per object in the composition", () => {
|
||||
const rowCount = parent.querySelectorAll(selectors.ladTableBodyRows).length;
|
||||
expect(rowCount).toBe(mockObj.ladTable.composition.length);
|
||||
});
|
||||
|
||||
it("should show the most recent datum from the telemetry producing object", async () => {
|
||||
const latestDatum = getLatestTelemetry(mockTelemetry, { timeFormat });
|
||||
const expectedDate = utcTimeFormat(latestDatum[timeFormat]);
|
||||
await Vue.nextTick();
|
||||
const latestDate = parent.querySelector(selectors.ladTableFirstRowSecondData).innerText;
|
||||
expect(latestDate).toBe(expectedDate);
|
||||
});
|
||||
|
||||
it("should show the name provided for the the telemetry producing object", () => {
|
||||
const rowName = parent.querySelector(selectors.ladTableFirstRowFirstData).innerText,
|
||||
expectedName = mockObj.telemetry.name;
|
||||
expect(rowName).toBe(expectedName);
|
||||
});
|
||||
|
||||
it("should show the correct values for the datum based on domain and range hints", async () => {
|
||||
const range = mockObj.telemetry.telemetry.values.find((val) => {
|
||||
return val.hints && val.hints.range !== undefined;
|
||||
}).key;
|
||||
const domain = mockObj.telemetry.telemetry.values.find((val) => {
|
||||
return val.hints && val.hints.domain !== undefined;
|
||||
}).key;
|
||||
const mostRecentTelemetry = getLatestTelemetry(mockTelemetry, { timeFormat });
|
||||
const rangeValue = mostRecentTelemetry[range];
|
||||
const domainValue = utcTimeFormat(mostRecentTelemetry[domain]);
|
||||
await Vue.nextTick();
|
||||
const actualDomainValue = parent.querySelector(selectors.ladTableFirstRowSecondData).innerText;
|
||||
const actualRangeValue = parent.querySelector(selectors.ladTableFirstRowThirdData).innerText;
|
||||
expect(actualRangeValue).toBe(rangeValue);
|
||||
expect(actualDomainValue).toBe(domainValue);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("The LAD Table Set", () => {
|
||||
const ladTableSetKey = 'LadTableSet';
|
||||
let telemetryCount = 3,
|
||||
timeFormat = 'utc',
|
||||
mockTelemetry = getMockTelemetry({ count: telemetryCount, format: timeFormat }),
|
||||
mockObj = getMockObjects({
|
||||
objectKeyStrings: ['ladTable', 'ladTableSet', 'telemetry']
|
||||
}),
|
||||
bounds = {
|
||||
start: 0,
|
||||
end: 4
|
||||
};
|
||||
// add mock telemetry to lad table and lad table to lad table set (composition)
|
||||
mockObj.ladTable.composition.push(mockObj.telemetry.identifier);
|
||||
mockObj.ladTableSet.composition.push(mockObj.ladTable.identifier);
|
||||
|
||||
beforeEach((done) => {
|
||||
const appHolder = document.createElement('div');
|
||||
appHolder.style.width = '640px';
|
||||
appHolder.style.height = '480px';
|
||||
|
||||
openmct = createOpenMct();
|
||||
|
||||
parent = document.createElement('div');
|
||||
child = document.createElement('div');
|
||||
parent.appendChild(child);
|
||||
|
||||
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([]));
|
||||
|
||||
ladPlugin = new LadPlugin();
|
||||
openmct.install(ladPlugin);
|
||||
|
||||
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({}));
|
||||
|
||||
openmct.time.bounds({ start: bounds.start, end: bounds.end });
|
||||
|
||||
openmct.on('start', done);
|
||||
openmct.start(appHolder);
|
||||
});
|
||||
|
||||
it("should provide a lad table set view only for lad table set objects", () => {
|
||||
let applicableViews = openmct.objectViews.get(mockObj.ladTableSet),
|
||||
ladTableSetView = applicableViews.find(
|
||||
(viewProvider) => viewProvider.key === ladTableSetKey
|
||||
);
|
||||
|
||||
expect(applicableViews.length).toEqual(1);
|
||||
expect(ladTableSetView).toBeDefined();
|
||||
});
|
||||
|
||||
describe('composition', () => {
|
||||
let ladTableSetCompositionCollection;
|
||||
|
||||
beforeEach(() => {
|
||||
ladTableSetCompositionCollection = openmct.composition.get(mockObj.ladTableSet);
|
||||
ladTableSetCompositionCollection.load();
|
||||
});
|
||||
|
||||
it("should accept lad table objects", () => {
|
||||
expect(() => {
|
||||
ladTableSetCompositionCollection.add(mockObj.ladTable);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("should reject non lad table objects", () => {
|
||||
expect(()=> {
|
||||
ladTableSetCompositionCollection.add(mockObj.telemetry);
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("table view", () => {
|
||||
let applicableViews,
|
||||
ladTableSetViewProvider,
|
||||
ladTableSetView,
|
||||
otherObj = getMockObjects({
|
||||
objectKeyStrings: ['ladTable'],
|
||||
overwrite: {
|
||||
ladTable: {
|
||||
name: "New LAD Table Object",
|
||||
identifier: { namespace: "", key: "another-lad-object" }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// add another lad table (with telemetry object) object to the lad table set for multi row test
|
||||
otherObj.ladTable.composition.push(mockObj.telemetry.identifier);
|
||||
mockObj.ladTableSet.composition.push(otherObj.ladTable.identifier);
|
||||
|
||||
beforeEach(async () => {
|
||||
let telemetryRequestResolve,
|
||||
ladObjectResolve,
|
||||
anotherLadObjectResolve;
|
||||
let telemetryRequestPromise = new Promise((resolve) => {
|
||||
telemetryRequestResolve = resolve;
|
||||
}),
|
||||
ladObjectPromise = new Promise((resolve) => {
|
||||
ladObjectResolve = resolve;
|
||||
}),
|
||||
anotherLadObjectPromise = new Promise((resolve) => {
|
||||
anotherLadObjectResolve = resolve;
|
||||
})
|
||||
openmct.telemetry.request.and.callFake(() => {
|
||||
telemetryRequestResolve(mockTelemetry);
|
||||
return telemetryRequestPromise;
|
||||
});
|
||||
openmct.objects.get.and.callFake((obj) => {
|
||||
if(obj.key === 'lad-object') {
|
||||
ladObjectResolve(mockObj.ladObject);
|
||||
return ladObjectPromise;
|
||||
} else if(obj.key === 'another-lad-object') {
|
||||
anotherLadObjectResolve(otherObj.ladObject);
|
||||
return anotherLadObjectPromise;
|
||||
}
|
||||
|
||||
return Promise.resolve({});
|
||||
});
|
||||
|
||||
openmct.time.bounds({ start: bounds.start, end: bounds.end });
|
||||
|
||||
applicableViews = openmct.objectViews.get(mockObj.ladTableSet);
|
||||
ladTableSetViewProvider = applicableViews.find((viewProvider) => viewProvider.key === ladTableSetKey);
|
||||
ladTableSetView = ladTableSetViewProvider.view(mockObj.ladTableSet, [mockObj.ladTableSet]);
|
||||
ladTableSetView.show(child, true);
|
||||
|
||||
await Promise.all([telemetryRequestPromise, ladObjectPromise, anotherLadObjectPromise]);
|
||||
return await Vue.nextTick();
|
||||
});
|
||||
|
||||
it("should show one row per lad table object in the composition", () => {
|
||||
const rowCount = parent.querySelectorAll(selectors.ladTableSetTableHeaders).length;
|
||||
expect(rowCount).toBe(mockObj.ladTableSet.composition.length);
|
||||
pending();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,8 +23,7 @@
|
||||
import EventEmitter from 'EventEmitter';
|
||||
import uuid from 'uuid';
|
||||
import TelemetryCriterion from "./criterion/TelemetryCriterion";
|
||||
import { TRIGGER } from "./utils/constants";
|
||||
import {computeCondition, computeConditionByLimit} from "./utils/evaluator";
|
||||
import { evaluateResults } from './utils/evaluator';
|
||||
import { getLatestTimestamp } from './utils/time';
|
||||
import AllTelemetryCriterion from "./criterion/AllTelemetryCriterion";
|
||||
|
||||
@@ -57,28 +56,41 @@ export default class ConditionClass extends EventEmitter {
|
||||
this.conditionManager = conditionManager;
|
||||
this.id = conditionConfiguration.id;
|
||||
this.criteria = [];
|
||||
this.criteriaResults = {};
|
||||
this.result = undefined;
|
||||
this.latestTimestamp = {};
|
||||
this.timeSystems = this.openmct.time.getAllTimeSystems();
|
||||
if (conditionConfiguration.configuration.criteria) {
|
||||
this.createCriteria(conditionConfiguration.configuration.criteria);
|
||||
}
|
||||
this.trigger = conditionConfiguration.configuration.trigger;
|
||||
this.conditionManager.on('broadcastTelemetry', this.handleBroadcastTelemetry, this);
|
||||
}
|
||||
|
||||
handleBroadcastTelemetry(datum) {
|
||||
getResult(datum) {
|
||||
if (!datum || !datum.id) {
|
||||
console.log('no data received');
|
||||
return;
|
||||
}
|
||||
this.criteria.forEach(criterion => {
|
||||
if (criterion.telemetry && (criterion.telemetry === 'all' || criterion.telemetry === 'any')) {
|
||||
criterion.handleSubscription(datum, this.conditionManager.telemetryObjects);
|
||||
} else {
|
||||
criterion.emit(`subscription:${datum.id}`, datum);
|
||||
}
|
||||
|
||||
if (this.isTelemetryUsed(datum.id)) {
|
||||
|
||||
this.criteria.forEach(criterion => {
|
||||
if (this.isAnyOrAllTelemetry(criterion)) {
|
||||
criterion.getResult(datum, this.conditionManager.telemetryObjects);
|
||||
} else {
|
||||
criterion.getResult(datum);
|
||||
}
|
||||
});
|
||||
|
||||
this.result = evaluateResults(this.criteria.map(criterion => criterion.result), this.trigger);
|
||||
}
|
||||
}
|
||||
|
||||
isAnyOrAllTelemetry(criterion) {
|
||||
return (criterion.telemetry && (criterion.telemetry === 'all' || criterion.telemetry === 'any'));
|
||||
}
|
||||
|
||||
isTelemetryUsed(id) {
|
||||
return this.criteria.some(criterion => {
|
||||
return this.isAnyOrAllTelemetry(criterion) || criterion.telemetryObjectIdAsString === id;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -90,7 +102,6 @@ export default class ConditionClass extends EventEmitter {
|
||||
updateTrigger(trigger) {
|
||||
if (this.trigger !== trigger) {
|
||||
this.trigger = trigger;
|
||||
this.handleConditionUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +145,6 @@ export default class ConditionClass extends EventEmitter {
|
||||
criterion = new TelemetryCriterion(criterionConfigurationWithId, this.openmct);
|
||||
}
|
||||
criterion.on('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
|
||||
criterion.on('criterionResultUpdated', (obj) => this.handleCriterionResult(obj));
|
||||
if (!this.criteria) {
|
||||
this.criteria = [];
|
||||
}
|
||||
@@ -163,20 +173,11 @@ export default class ConditionClass extends EventEmitter {
|
||||
const newCriterionConfiguration = this.generateCriterion(criterionConfiguration);
|
||||
let newCriterion = new TelemetryCriterion(newCriterionConfiguration, this.openmct);
|
||||
newCriterion.on('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
|
||||
newCriterion.on('criterionResultUpdated', (obj) => this.handleCriterionResult(obj));
|
||||
|
||||
let criterion = found.item;
|
||||
criterion.unsubscribe();
|
||||
criterion.off('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
|
||||
criterion.off('criterionResultUpdated', (obj) => this.handleCriterionResult(obj));
|
||||
this.criteria.splice(found.index, 1, newCriterion);
|
||||
delete this.criteriaResults[criterion.id];
|
||||
}
|
||||
}
|
||||
|
||||
removeCriterion(id) {
|
||||
if (this.destroyCriterion(id)) {
|
||||
this.handleConditionUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,7 +190,6 @@ export default class ConditionClass extends EventEmitter {
|
||||
});
|
||||
criterion.destroy();
|
||||
this.criteria.splice(found.index, 1);
|
||||
delete this.criteriaResults[criterion.id];
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -200,32 +200,14 @@ export default class ConditionClass extends EventEmitter {
|
||||
let found = this.findCriterion(criterion.id);
|
||||
if (found) {
|
||||
this.criteria[found.index] = criterion.data;
|
||||
// TODO nothing is listening to this
|
||||
this.emitEvent('conditionUpdated', {
|
||||
trigger: this.trigger,
|
||||
criteria: this.criteria
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
updateCriteriaResults(eventData) {
|
||||
const id = eventData.id;
|
||||
|
||||
if (this.findCriterion(id)) {
|
||||
this.criteriaResults[id] = !!eventData.data.result;
|
||||
}
|
||||
}
|
||||
|
||||
handleCriterionResult(eventData) {
|
||||
this.updateCriteriaResults(eventData);
|
||||
this.handleConditionUpdated(eventData.data);
|
||||
}
|
||||
|
||||
requestLADConditionResult() {
|
||||
let latestTimestamp;
|
||||
let criteriaResults = {};
|
||||
const criteriaRequests = this.criteria
|
||||
.map(criterion => criterion.requestLAD({telemetryObjects: this.conditionManager.telemetryObjects}));
|
||||
.map(criterion => criterion.requestLAD(this.conditionManager.telemetryObjects));
|
||||
|
||||
return Promise.all(criteriaRequests)
|
||||
.then(results => {
|
||||
@@ -237,28 +219,21 @@ export default class ConditionClass extends EventEmitter {
|
||||
latestTimestamp = getLatestTimestamp(
|
||||
latestTimestamp,
|
||||
data,
|
||||
this.timeSystems
|
||||
this.timeSystems,
|
||||
this.openmct.time.timeSystem()
|
||||
);
|
||||
});
|
||||
return {
|
||||
id: this.id,
|
||||
data: Object.assign({}, latestTimestamp, { result: this.evaluate(criteriaResults) })
|
||||
}
|
||||
data: Object.assign(
|
||||
{},
|
||||
latestTimestamp,
|
||||
{ result: evaluateResults(Object.values(criteriaResults), this.trigger) }
|
||||
)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
getTelemetrySubscriptions() {
|
||||
return this.criteria.map(criterion => criterion.telemetryObjectIdAsString);
|
||||
}
|
||||
|
||||
handleConditionUpdated(datum) {
|
||||
// trigger an updated event so that consumers can react accordingly
|
||||
this.result = this.evaluate(this.criteriaResults);
|
||||
this.emitEvent('conditionResultUpdated',
|
||||
Object.assign({}, datum, { result: this.result })
|
||||
);
|
||||
}
|
||||
|
||||
getCriteria() {
|
||||
return this.criteria;
|
||||
}
|
||||
@@ -272,28 +247,7 @@ export default class ConditionClass extends EventEmitter {
|
||||
return success;
|
||||
}
|
||||
|
||||
evaluate(results) {
|
||||
if (this.trigger && this.trigger === TRIGGER.XOR) {
|
||||
return computeConditionByLimit(results, 1);
|
||||
} else if (this.trigger && this.trigger === TRIGGER.NOT) {
|
||||
return computeConditionByLimit(results, 0);
|
||||
} else {
|
||||
return computeCondition(results, this.trigger === TRIGGER.ALL);
|
||||
}
|
||||
}
|
||||
|
||||
emitEvent(eventName, data) {
|
||||
this.emit(eventName, {
|
||||
id: this.id,
|
||||
data: data
|
||||
});
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.conditionManager.off('broadcastTelemetry', this.handleBroadcastTelemetry, this);
|
||||
if (typeof this.stopObservingForChanges === 'function') {
|
||||
this.stopObservingForChanges();
|
||||
}
|
||||
this.destroyCriteria();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ export default class ConditionManager extends EventEmitter {
|
||||
super();
|
||||
this.openmct = openmct;
|
||||
this.conditionSetDomainObject = conditionSetDomainObject;
|
||||
this.timeAPI = this.openmct.time;
|
||||
this.timeSystems = this.openmct.time.getAllTimeSystems();
|
||||
this.composition = this.openmct.composition.get(conditionSetDomainObject);
|
||||
this.composition.on('add', this.subscribeToTelemetry, this);
|
||||
@@ -56,8 +55,9 @@ export default class ConditionManager extends EventEmitter {
|
||||
this.telemetryObjects[id] = Object.assign({}, endpoint, {telemetryMetaData: this.openmct.telemetry.getMetadata(endpoint).valueMetadatas});
|
||||
this.subscriptions[id] = this.openmct.telemetry.subscribe(
|
||||
endpoint,
|
||||
this.broadcastTelemetry.bind(this, id)
|
||||
this.telemetryReceived.bind(this, endpoint)
|
||||
);
|
||||
// TODO check if this is needed
|
||||
this.updateConditionTelemetry();
|
||||
}
|
||||
|
||||
@@ -71,10 +71,10 @@ export default class ConditionManager extends EventEmitter {
|
||||
this.subscriptions[id]();
|
||||
delete this.subscriptions[id];
|
||||
delete this.telemetryObjects[id];
|
||||
this.removeConditionTelemetry();
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.conditionResults = {};
|
||||
this.conditionClassCollection = [];
|
||||
if (this.conditionSetDomainObject.configuration.conditionCollection.length) {
|
||||
this.conditionSetDomainObject.configuration.conditionCollection.forEach((conditionConfiguration, index) => {
|
||||
@@ -87,6 +87,30 @@ export default class ConditionManager extends EventEmitter {
|
||||
this.conditionClassCollection.forEach((condition) => condition.updateTelemetry());
|
||||
}
|
||||
|
||||
removeConditionTelemetry() {
|
||||
let conditionsChanged = false;
|
||||
this.conditionSetDomainObject.configuration.conditionCollection.forEach((conditionConfiguration) => {
|
||||
conditionConfiguration.configuration.criteria.forEach((criterion, index) => {
|
||||
const isAnyAllTelemetry = criterion.telemetry && (criterion.telemetry === 'any' || criterion.telemetry === 'all');
|
||||
if (!isAnyAllTelemetry) {
|
||||
const found = Object.values(this.telemetryObjects).find((telemetryObject) => {
|
||||
return this.openmct.objects.areIdsEqual(telemetryObject.identifier, criterion.telemetry);
|
||||
});
|
||||
if (!found) {
|
||||
criterion.telemetry = '';
|
||||
criterion.metadata = '';
|
||||
criterion.input = [];
|
||||
criterion.operation = '';
|
||||
conditionsChanged = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
if (conditionsChanged) {
|
||||
this.persistConditions();
|
||||
}
|
||||
}
|
||||
|
||||
updateCondition(conditionConfiguration, index) {
|
||||
let condition = this.conditionClassCollection[index];
|
||||
condition.update(conditionConfiguration);
|
||||
@@ -96,7 +120,6 @@ export default class ConditionManager extends EventEmitter {
|
||||
|
||||
initCondition(conditionConfiguration, index) {
|
||||
let condition = new Condition(conditionConfiguration, this.openmct, this);
|
||||
condition.on('conditionResultUpdated', this.handleConditionResult.bind(this));
|
||||
if (index !== undefined) {
|
||||
this.conditionClassCollection.splice(index + 1, 0, condition);
|
||||
} else {
|
||||
@@ -160,13 +183,10 @@ export default class ConditionManager extends EventEmitter {
|
||||
|
||||
removeCondition(index) {
|
||||
let condition = this.conditionClassCollection[index];
|
||||
condition.destroyCriteria();
|
||||
condition.off('conditionResultUpdated', this.handleConditionResult.bind(this));
|
||||
condition.destroy();
|
||||
this.conditionClassCollection.splice(index, 1);
|
||||
this.conditionSetDomainObject.configuration.conditionCollection.splice(index, 1);
|
||||
delete this.conditionResults[condition.id];
|
||||
this.persistConditions();
|
||||
this.handleConditionResult();
|
||||
}
|
||||
|
||||
findConditionById(id) {
|
||||
@@ -184,7 +204,23 @@ export default class ConditionManager extends EventEmitter {
|
||||
this.persistConditions();
|
||||
}
|
||||
|
||||
getCurrentCondition(conditionResults) {
|
||||
getCurrentCondition() {
|
||||
const conditionCollection = this.conditionSetDomainObject.configuration.conditionCollection;
|
||||
let currentCondition = conditionCollection[conditionCollection.length-1];
|
||||
|
||||
for (let i = 0; i < conditionCollection.length - 1; i++) {
|
||||
const condition = this.findConditionById(conditionCollection[i].id)
|
||||
if (condition.result) {
|
||||
//first condition to be true wins
|
||||
currentCondition = conditionCollection[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return currentCondition;
|
||||
}
|
||||
|
||||
getCurrentConditionLAD(conditionResults) {
|
||||
const conditionCollection = this.conditionSetDomainObject.configuration.conditionCollection;
|
||||
let currentCondition = conditionCollection[conditionCollection.length-1];
|
||||
|
||||
@@ -198,36 +234,6 @@ export default class ConditionManager extends EventEmitter {
|
||||
return currentCondition;
|
||||
}
|
||||
|
||||
updateConditionResults(resultObj) {
|
||||
if (!resultObj) {
|
||||
return;
|
||||
}
|
||||
|
||||
const id = resultObj.id;
|
||||
|
||||
if (this.findConditionById(id)) {
|
||||
this.conditionResults[id] = resultObj.data.result;
|
||||
}
|
||||
}
|
||||
|
||||
handleConditionResult(resultObj) {
|
||||
this.updateConditionResults(resultObj);
|
||||
const currentCondition = this.getCurrentCondition(this.conditionResults);
|
||||
const timestamp = JSON.parse(JSON.stringify(resultObj.data))
|
||||
delete timestamp.result
|
||||
|
||||
this.emit('conditionSetResultUpdated',
|
||||
Object.assign(
|
||||
{
|
||||
output: currentCondition.configuration.output,
|
||||
id: this.conditionSetDomainObject.identifier,
|
||||
conditionId: currentCondition.id
|
||||
},
|
||||
timestamp
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
requestLADConditionSetOutput() {
|
||||
if (!this.conditionClassCollection.length) {
|
||||
return Promise.resolve([]);
|
||||
@@ -249,12 +255,17 @@ export default class ConditionManager extends EventEmitter {
|
||||
latestTimestamp = getLatestTimestamp(
|
||||
latestTimestamp,
|
||||
data,
|
||||
this.timeSystems
|
||||
this.timeSystems,
|
||||
this.openmct.time.timeSystem()
|
||||
);
|
||||
});
|
||||
const currentCondition = this.getCurrentCondition(conditionResults);
|
||||
|
||||
return Object.assign(
|
||||
if (!Object.values(latestTimestamp).some(timeSystem => timeSystem)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const currentCondition = this.getCurrentConditionLAD(conditionResults);
|
||||
const currentOutput = Object.assign(
|
||||
{
|
||||
output: currentCondition.configuration.output,
|
||||
id: this.conditionSetDomainObject.identifier,
|
||||
@@ -262,12 +273,50 @@ export default class ConditionManager extends EventEmitter {
|
||||
},
|
||||
latestTimestamp
|
||||
);
|
||||
|
||||
return [currentOutput];
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
broadcastTelemetry(id, datum) {
|
||||
this.emit(`broadcastTelemetry`, Object.assign({}, this.createNormalizedDatum(datum, id), {id: id}));
|
||||
isTelemetryUsed(endpoint) {
|
||||
const id = this.openmct.objects.makeKeyString(endpoint.identifier);
|
||||
|
||||
for(const condition of this.conditionClassCollection) {
|
||||
if (condition.isTelemetryUsed(id)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
telemetryReceived(endpoint, datum) {
|
||||
if (!this.isTelemetryUsed(endpoint)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedDatum = this.createNormalizedDatum(datum, endpoint);
|
||||
const timeSystemKey = this.openmct.time.timeSystem().key;
|
||||
let timestamp = {};
|
||||
timestamp[timeSystemKey] = normalizedDatum[timeSystemKey];
|
||||
|
||||
this.conditionClassCollection.forEach(condition => {
|
||||
condition.getResult(normalizedDatum);
|
||||
});
|
||||
|
||||
const currentCondition = this.getCurrentCondition();
|
||||
|
||||
this.emit('conditionSetResultUpdated',
|
||||
Object.assign(
|
||||
{
|
||||
output: currentCondition.configuration.output,
|
||||
id: this.conditionSetDomainObject.identifier,
|
||||
conditionId: currentCondition.id
|
||||
},
|
||||
timestamp
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
getTestData(metadatum) {
|
||||
@@ -281,13 +330,20 @@ export default class ConditionManager extends EventEmitter {
|
||||
return data;
|
||||
}
|
||||
|
||||
createNormalizedDatum(telemetryDatum, id) {
|
||||
return Object.values(this.telemetryObjects[id].telemetryMetaData).reduce((normalizedDatum, metadatum) => {
|
||||
createNormalizedDatum(telemetryDatum, endpoint) {
|
||||
const id = this.openmct.objects.makeKeyString(endpoint.identifier);
|
||||
const metadata = this.openmct.telemetry.getMetadata(endpoint).valueMetadatas;
|
||||
|
||||
const normalizedDatum = Object.values(metadata).reduce((datum, metadatum) => {
|
||||
const testValue = this.getTestData(metadatum);
|
||||
const formatter = this.openmct.telemetry.getValueFormatter(metadatum);
|
||||
normalizedDatum[metadatum.key] = testValue !== undefined ? formatter.parse(testValue) : formatter.parse(telemetryDatum[metadatum.source]);
|
||||
return normalizedDatum;
|
||||
datum[metadatum.key] = testValue !== undefined ? formatter.parse(testValue) : formatter.parse(telemetryDatum[metadatum.source]);
|
||||
return datum;
|
||||
}, {});
|
||||
|
||||
normalizedDatum.id = id;
|
||||
|
||||
return normalizedDatum;
|
||||
}
|
||||
|
||||
updateTestData(testData) {
|
||||
@@ -310,7 +366,6 @@ export default class ConditionManager extends EventEmitter {
|
||||
}
|
||||
|
||||
this.conditionClassCollection.forEach((condition) => {
|
||||
condition.off('conditionResultUpdated', this.handleConditionResult);
|
||||
condition.destroy();
|
||||
})
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ describe('ConditionManager', () => {
|
||||
conditionMgr = new ConditionManager(conditionSetDomainObject, openmct);
|
||||
|
||||
conditionMgr.on('conditionSetResultUpdated', mockListener);
|
||||
conditionMgr.on('broadcastTelemetry', mockListener);
|
||||
conditionMgr.on('telemetryReceived', mockListener);
|
||||
});
|
||||
|
||||
it('creates a conditionCollection with a default condition', function () {
|
||||
|
||||
@@ -54,13 +54,22 @@ export default class ConditionSetMetadataProvider {
|
||||
return {
|
||||
values: this.getDomains().concat([
|
||||
{
|
||||
name: 'Output',
|
||||
key: 'output',
|
||||
format: 'enum',
|
||||
key: "state",
|
||||
source: "output",
|
||||
name: "State",
|
||||
format: "enum",
|
||||
enumerations: enumerations,
|
||||
hints: {
|
||||
range: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "output",
|
||||
name: "Value",
|
||||
format: "string",
|
||||
hints: {
|
||||
range: 2
|
||||
}
|
||||
}
|
||||
])
|
||||
};
|
||||
|
||||
@@ -45,7 +45,7 @@ export default class ConditionSetTelemetryProvider {
|
||||
|
||||
return conditionManager.requestLADConditionSetOutput()
|
||||
.then(latestOutput => {
|
||||
return latestOutput ? [latestOutput] : [];
|
||||
return latestOutput;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -25,12 +25,11 @@ import {TRIGGER} from "./utils/constants";
|
||||
import TelemetryCriterion from "./criterion/TelemetryCriterion";
|
||||
|
||||
let openmct = {},
|
||||
mockListener,
|
||||
testConditionDefinition,
|
||||
testTelemetryObject,
|
||||
conditionObj,
|
||||
conditionManager,
|
||||
mockBroadcastTelemetry,
|
||||
mockTelemetryReceived,
|
||||
mockTimeSystems;
|
||||
|
||||
describe("The condition", function () {
|
||||
@@ -39,27 +38,33 @@ describe("The condition", function () {
|
||||
conditionManager = jasmine.createSpyObj('conditionManager',
|
||||
['on']
|
||||
);
|
||||
mockBroadcastTelemetry = jasmine.createSpy('listener');
|
||||
conditionManager.on('broadcastTelemetry', mockBroadcastTelemetry);
|
||||
mockTelemetryReceived = jasmine.createSpy('listener');
|
||||
conditionManager.on('telemetryReceived', mockTelemetryReceived);
|
||||
|
||||
mockListener = jasmine.createSpy('listener');
|
||||
testTelemetryObject = {
|
||||
identifier:{ namespace: "", key: "test-object"},
|
||||
type: "test-object",
|
||||
name: "Test Object",
|
||||
telemetry: {
|
||||
values: [{
|
||||
key: "some-key",
|
||||
name: "Some attribute",
|
||||
key: "value",
|
||||
name: "Value",
|
||||
hints: {
|
||||
range: 2
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "utc",
|
||||
name: "Time",
|
||||
format: "utc",
|
||||
hints: {
|
||||
domain: 1
|
||||
}
|
||||
}, {
|
||||
key: "some-other-key",
|
||||
name: "Another attribute",
|
||||
hints: {
|
||||
range: 1
|
||||
}
|
||||
key: "testSource",
|
||||
source: "value",
|
||||
name: "Test",
|
||||
format: "string"
|
||||
}]
|
||||
}
|
||||
};
|
||||
@@ -104,8 +109,6 @@ describe("The condition", function () {
|
||||
openmct,
|
||||
conditionManager
|
||||
);
|
||||
|
||||
conditionObj.on('conditionUpdated', mockListener);
|
||||
});
|
||||
|
||||
it("generates criteria with the correct properties", function () {
|
||||
@@ -140,4 +143,38 @@ describe("The condition", function () {
|
||||
expect(result).toBeTrue();
|
||||
expect(conditionObj.criteria.length).toEqual(0);
|
||||
});
|
||||
|
||||
it("gets the result of a condition when new telemetry data is received", function () {
|
||||
conditionObj.getResult({
|
||||
value: '0',
|
||||
utc: 'Hi',
|
||||
id: testTelemetryObject.identifier.key
|
||||
});
|
||||
expect(conditionObj.result).toBeTrue();
|
||||
});
|
||||
|
||||
it("gets the result of a condition when new telemetry data is received", function () {
|
||||
conditionObj.getResult({
|
||||
value: '1',
|
||||
utc: 'Hi',
|
||||
id: testTelemetryObject.identifier.key
|
||||
});
|
||||
expect(conditionObj.result).toBeFalse();
|
||||
});
|
||||
|
||||
it("keeps the old result new telemetry data is not used by it", function () {
|
||||
conditionObj.getResult({
|
||||
value: '0',
|
||||
utc: 'Hi',
|
||||
id: testTelemetryObject.identifier.key
|
||||
});
|
||||
expect(conditionObj.result).toBeTrue();
|
||||
|
||||
conditionObj.getResult({
|
||||
value: '1',
|
||||
utc: 'Hi',
|
||||
id: '1234'
|
||||
});
|
||||
expect(conditionObj.result).toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,10 +23,13 @@
|
||||
import EventEmitter from 'EventEmitter';
|
||||
|
||||
export default class StyleRuleManager extends EventEmitter {
|
||||
constructor(styleConfiguration, openmct, callback) {
|
||||
constructor(styleConfiguration, openmct, callback, suppressSubscriptionOnEdit) {
|
||||
super();
|
||||
this.openmct = openmct;
|
||||
this.callback = callback;
|
||||
if (suppressSubscriptionOnEdit) {
|
||||
this.openmct.editor.on('isEditing', this.toggleSubscription.bind(this));
|
||||
}
|
||||
if (styleConfiguration) {
|
||||
this.initialize(styleConfiguration);
|
||||
if (styleConfiguration.conditionSetIdentifier) {
|
||||
@@ -37,9 +40,25 @@ export default class StyleRuleManager extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
toggleSubscription(isEditing) {
|
||||
this.isEditing = isEditing;
|
||||
if (this.isEditing) {
|
||||
if (this.stopProvidingTelemetry) {
|
||||
this.stopProvidingTelemetry();
|
||||
}
|
||||
if (this.conditionSetIdentifier) {
|
||||
this.applySelectedConditionStyle();
|
||||
}
|
||||
} else if (this.conditionSetIdentifier) {
|
||||
this.subscribeToConditionSet();
|
||||
}
|
||||
}
|
||||
|
||||
initialize(styleConfiguration) {
|
||||
this.conditionSetIdentifier = styleConfiguration.conditionSetIdentifier;
|
||||
this.staticStyle = styleConfiguration.staticStyle;
|
||||
this.selectedConditionId = styleConfiguration.selectedConditionId;
|
||||
this.defaultConditionId = styleConfiguration.defaultConditionId;
|
||||
this.updateConditionStylesMap(styleConfiguration.styles || []);
|
||||
}
|
||||
|
||||
@@ -54,7 +73,7 @@ export default class StyleRuleManager extends EventEmitter {
|
||||
this.handleConditionSetResultUpdated(output[0]);
|
||||
}
|
||||
});
|
||||
this.stopProvidingTelemetry = this.openmct.telemetry.subscribe(conditionSetDomainObject, output => this.handleConditionSetResultUpdated(output));
|
||||
this.stopProvidingTelemetry = this.openmct.telemetry.subscribe(conditionSetDomainObject, this.handleConditionSetResultUpdated.bind(this));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -66,9 +85,13 @@ export default class StyleRuleManager extends EventEmitter {
|
||||
let isNewConditionSet = !this.conditionSetIdentifier ||
|
||||
!this.openmct.objects.areIdsEqual(this.conditionSetIdentifier, styleConfiguration.conditionSetIdentifier);
|
||||
this.initialize(styleConfiguration);
|
||||
//Only resubscribe if the conditionSet has changed.
|
||||
if (isNewConditionSet) {
|
||||
this.subscribeToConditionSet();
|
||||
if (this.isEditing) {
|
||||
this.applySelectedConditionStyle();
|
||||
} else {
|
||||
//Only resubscribe if the conditionSet has changed.
|
||||
if (isNewConditionSet) {
|
||||
this.subscribeToConditionSet();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -103,13 +126,23 @@ export default class StyleRuleManager extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
applySelectedConditionStyle() {
|
||||
const conditionId = this.selectedConditionId || this.defaultConditionId;
|
||||
if (!conditionId) {
|
||||
this.applyStaticStyle();
|
||||
} else if (this.conditionalStyleMap[conditionId]) {
|
||||
this.currentStyle = this.conditionalStyleMap[conditionId];
|
||||
this.updateDomainObjectStyle();
|
||||
}
|
||||
}
|
||||
|
||||
applyStaticStyle() {
|
||||
if (this.staticStyle) {
|
||||
this.currentStyle = this.staticStyle.style;
|
||||
} else {
|
||||
if (this.currentStyle) {
|
||||
Object.keys(this.currentStyle).forEach(key => {
|
||||
this.currentStyle[key] = 'transparent';
|
||||
this.currentStyle[key] = '__no_value';
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -123,6 +156,7 @@ export default class StyleRuleManager extends EventEmitter {
|
||||
}
|
||||
delete this.stopProvidingTelemetry;
|
||||
this.conditionSetIdentifier = undefined;
|
||||
this.isEditing = undefined;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
>
|
||||
<div class="c-condition-h__drop-target"></div>
|
||||
<div v-if="isEditing"
|
||||
:class="{'is-current': condition.id === currentConditionId}"
|
||||
class="c-condition c-condition--edit"
|
||||
>
|
||||
<!-- Edit view -->
|
||||
@@ -167,6 +168,7 @@
|
||||
</div>
|
||||
<div v-else
|
||||
class="c-condition c-condition--browse"
|
||||
:class="{'is-current': condition.id === currentConditionId}"
|
||||
>
|
||||
<!-- Browse view -->
|
||||
<div class="c-condition__header">
|
||||
@@ -199,6 +201,10 @@ export default {
|
||||
ConditionDescription
|
||||
},
|
||||
props: {
|
||||
currentConditionId: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
condition: {
|
||||
type: Object,
|
||||
required: true
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
<Condition v-for="(condition, index) in conditionCollection"
|
||||
:key="condition.id"
|
||||
:condition="condition"
|
||||
:current-condition-id="currentConditionId"
|
||||
:condition-index="index"
|
||||
:telemetry="telemetryObjs"
|
||||
:is-editing="isEditing"
|
||||
@@ -107,7 +108,8 @@ export default {
|
||||
moveIndex: undefined,
|
||||
isDragging: false,
|
||||
defaultOutput: undefined,
|
||||
dragCounter: 0
|
||||
dragCounter: 0,
|
||||
currentConditionId: ''
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
@@ -145,6 +147,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
handleConditionSetResultUpdated(data) {
|
||||
this.currentConditionId = data.conditionId;
|
||||
this.$emit('conditionSetResultUpdated', data)
|
||||
},
|
||||
observeForChanges() {
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<div v-if="expanded"
|
||||
class="c-cs__content"
|
||||
>
|
||||
<div class="c-cdef__controls"
|
||||
<div class="c-cs__test-data__controls c-cdef__controls"
|
||||
:disabled="!telemetry.length"
|
||||
>
|
||||
<label class="c-toggle-switch">
|
||||
|
||||
@@ -46,102 +46,273 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
.c-condition,
|
||||
.c-test-datum {
|
||||
@include discreteItem();
|
||||
display: flex;
|
||||
padding: $interiorMargin;
|
||||
|
||||
&--edit {
|
||||
line-height: 160%; // For layout when inputs wrap, like in criteria
|
||||
}
|
||||
}
|
||||
.c-cs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
.c-condition {
|
||||
flex-direction: column;
|
||||
min-width: 400px;
|
||||
/************************** CONDITION SET LAYOUT */
|
||||
&__current-output {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
> * + * {
|
||||
margin-top: $interiorMarginSm;
|
||||
}
|
||||
&--browse {
|
||||
.c-condition__summary {
|
||||
border-top: 1px solid $colorInteriorBorder;
|
||||
padding-top: $interiorMargin;
|
||||
}
|
||||
}
|
||||
&__test-data-and-conditions-w {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/***************************** HEADER */
|
||||
&__header {
|
||||
$h: 22px;
|
||||
display: flex;
|
||||
align-items: start;
|
||||
align-content: stretch;
|
||||
overflow: hidden;
|
||||
min-height: $h;
|
||||
line-height: $h;
|
||||
&__test-data,
|
||||
&__conditions {
|
||||
flex: 0 0 auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
> * {
|
||||
flex: 0 0 auto;
|
||||
+ * {
|
||||
margin-left: $interiorMarginSm;
|
||||
}
|
||||
}
|
||||
}
|
||||
&__test-data {
|
||||
flex: 0 0 auto;
|
||||
max-height: 50%;
|
||||
|
||||
&__drag-grippy {
|
||||
transform: translateY(50%);
|
||||
}
|
||||
&.is-expanded {
|
||||
margin-bottom: $interiorMargin * 4;
|
||||
}
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-weight: bold;
|
||||
align-self: baseline; // Fixes bold line-height offset problem
|
||||
}
|
||||
&__conditions {
|
||||
flex: 1 1 auto;
|
||||
|
||||
&__output,
|
||||
&__summary {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
> * + * {
|
||||
margin-top: $interiorMarginSm;
|
||||
}
|
||||
}
|
||||
|
||||
/***************************** CONDITION DEFINITION, EDITING */
|
||||
.c-cdef {
|
||||
display: grid;
|
||||
grid-row-gap: $interiorMarginSm;
|
||||
grid-column-gap: $interiorMargin;
|
||||
grid-auto-columns: min-content 1fr max-content;
|
||||
align-items: start;
|
||||
min-width: 150px;
|
||||
margin-left: 29px;
|
||||
overflow: hidden;
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 0 1 auto;
|
||||
overflow: hidden;
|
||||
|
||||
&__criteria,
|
||||
&__match-and-criteria {
|
||||
display: contents;
|
||||
}
|
||||
> * {
|
||||
flex: 0 0 auto;
|
||||
overflow: hidden;
|
||||
+ * {
|
||||
margin-top: $interiorMarginSm;
|
||||
}
|
||||
}
|
||||
|
||||
&__label {
|
||||
grid-column: 1;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.c-button {
|
||||
align-self: start;
|
||||
}
|
||||
}
|
||||
|
||||
&__separator {
|
||||
grid-column: 1 / span 3;
|
||||
}
|
||||
.is-editing & {
|
||||
// Add some space to kick away from blue editing border indication
|
||||
padding: $interiorMargin;
|
||||
}
|
||||
|
||||
&__controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
grid-column: 2;
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
> * > * {
|
||||
margin-right: $interiorMarginSm;
|
||||
}
|
||||
}
|
||||
&__conditions-h {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
padding-right: $interiorMarginSm;
|
||||
|
||||
&__buttons {
|
||||
grid-column: 3;
|
||||
}
|
||||
}
|
||||
> * + * {
|
||||
margin-top: $interiorMarginSm;
|
||||
}
|
||||
}
|
||||
|
||||
.hint {
|
||||
padding: $interiorMarginSm;
|
||||
}
|
||||
|
||||
/************************** SPECIFIC ITEMS */
|
||||
&__current-output-value {
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
padding: 0 $interiorMargin $interiorMarginLg $interiorMargin;
|
||||
|
||||
> * {
|
||||
padding: $interiorMargin 0; // Must do this to align label and value
|
||||
}
|
||||
|
||||
&__label {
|
||||
color: $colorInspectorSectionHeaderFg;
|
||||
opacity: 0.9;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
&__value {
|
||||
$p: $interiorMargin * 3;
|
||||
font-size: 1.25em;
|
||||
margin-left: $interiorMargin;
|
||||
padding-left: $p;
|
||||
padding-right: $p;
|
||||
background: rgba(black, 0.2);
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/***************************** CONDITIONS AND TEST DATUM ELEMENTS */
|
||||
.c-condition,
|
||||
.c-test-datum {
|
||||
@include discreteItem();
|
||||
display: flex;
|
||||
padding: $interiorMargin;
|
||||
line-height: 170%; // Aligns text with controls like selects
|
||||
}
|
||||
|
||||
.c-cdef,
|
||||
.c-cs-test {
|
||||
&__controls {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-wrap: wrap;
|
||||
|
||||
> * > * {
|
||||
margin-right: $interiorMarginSm;
|
||||
}
|
||||
}
|
||||
|
||||
&__buttons {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.c-condition {
|
||||
border: 1px solid transparent;
|
||||
flex-direction: column;
|
||||
min-width: 400px;
|
||||
|
||||
> * + * {
|
||||
margin-top: $interiorMarginSm;
|
||||
}
|
||||
&--browse {
|
||||
.c-condition__summary {
|
||||
border-top: 1px solid $colorInteriorBorder;
|
||||
padding-top: $interiorMargin;
|
||||
}
|
||||
}
|
||||
|
||||
/***************************** HEADER */
|
||||
&__header {
|
||||
$h: 22px;
|
||||
display: flex;
|
||||
align-items: start;
|
||||
align-content: stretch;
|
||||
overflow: hidden;
|
||||
min-height: $h;
|
||||
line-height: $h;
|
||||
|
||||
> * {
|
||||
flex: 0 0 auto;
|
||||
+ * {
|
||||
margin-left: $interiorMarginSm;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__drag-grippy {
|
||||
transform: translateY(50%);
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-weight: bold;
|
||||
align-self: baseline; // Fixes bold line-height offset problem
|
||||
}
|
||||
|
||||
&__output,
|
||||
&__summary {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
&.is-current {
|
||||
$c: $colorBodyFg;
|
||||
border-color: rgba($c, 0.2);
|
||||
background: rgba($c, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
/***************************** CONDITION DEFINITION, EDITING */
|
||||
.c-cdef {
|
||||
display: grid;
|
||||
grid-row-gap: $interiorMarginSm;
|
||||
grid-column-gap: $interiorMargin;
|
||||
grid-auto-columns: min-content 1fr max-content;
|
||||
align-items: start;
|
||||
min-width: 150px;
|
||||
margin-left: 29px;
|
||||
overflow: hidden;
|
||||
|
||||
&__criteria,
|
||||
&__match-and-criteria {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
&__label {
|
||||
grid-column: 1;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__separator {
|
||||
grid-column: 1 / span 3;
|
||||
}
|
||||
|
||||
&__controls {
|
||||
align-items: flex-start;
|
||||
grid-column: 2;
|
||||
|
||||
> * > * {
|
||||
margin-right: $interiorMarginSm;
|
||||
}
|
||||
}
|
||||
|
||||
&__buttons {
|
||||
grid-column: 3;
|
||||
}
|
||||
}
|
||||
|
||||
.c-c__drag-ghost {
|
||||
width: 100%;
|
||||
min-height: $interiorMarginSm;
|
||||
|
||||
&.dragging {
|
||||
min-height: 5em;
|
||||
background-color: lightblue;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
/***************************** TEST DATA */
|
||||
.c-cs__test-data {
|
||||
&__controls {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.c-cs-tests {
|
||||
flex: 0 1 auto;
|
||||
overflow: auto;
|
||||
padding-right: $interiorMarginSm;
|
||||
|
||||
> * + * {
|
||||
margin-top: $interiorMarginSm;
|
||||
}
|
||||
}
|
||||
|
||||
.c-cs-test {
|
||||
> * + * {
|
||||
margin-left: $interiorMargin;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +79,8 @@
|
||||
<div v-for="(conditionStyle, index) in conditionalStyles"
|
||||
:key="index"
|
||||
class="c-inspect-styles__condition"
|
||||
:class="{'is-current': conditionStyle.conditionId === selectedConditionId}"
|
||||
@click="applySelectedConditionStyle(conditionStyle.conditionId)"
|
||||
>
|
||||
<condition-error :show-label="true"
|
||||
:condition="getCondition(conditionStyle.conditionId)"
|
||||
@@ -105,7 +107,7 @@ import ConditionDescription from "@/plugins/condition/components/ConditionDescri
|
||||
import ConditionError from "@/plugins/condition/components/ConditionError.vue";
|
||||
import Vue from 'vue';
|
||||
import PreviewAction from "@/ui/preview/PreviewAction.js";
|
||||
import {getInitialStyleForItem} from "@/plugins/condition/utils/styleUtils";
|
||||
import {getApplicableStylesForItem} from "@/plugins/condition/utils/styleUtils";
|
||||
|
||||
export default {
|
||||
name: 'ConditionalStylesView',
|
||||
@@ -126,13 +128,12 @@ export default {
|
||||
isEditing: this.openmct.editor.isEditing(),
|
||||
conditions: undefined,
|
||||
conditionsLoaded: false,
|
||||
navigateToPath: ''
|
||||
navigateToPath: '',
|
||||
selectedConditionId: ''
|
||||
}
|
||||
},
|
||||
destroyed() {
|
||||
if (this.stopObserving) {
|
||||
this.stopObserving();
|
||||
}
|
||||
this.removeListeners();
|
||||
},
|
||||
mounted() {
|
||||
this.itemId = '';
|
||||
@@ -164,7 +165,8 @@ export default {
|
||||
layoutItem = this.selection[0][0].context.layoutItem;
|
||||
const item = this.selection[0][0].context.item;
|
||||
this.canHide = true;
|
||||
if (item && this.isItemType('subobject-view', layoutItem)) {
|
||||
if (item &&
|
||||
(!layoutItem || (this.isItemType('subobject-view', layoutItem)))) {
|
||||
domainObject = item;
|
||||
} else {
|
||||
domainObject = this.selection[0][1].context.item;
|
||||
@@ -176,12 +178,24 @@ export default {
|
||||
domainObject = this.selection[0][0].context.item;
|
||||
}
|
||||
this.domainObject = domainObject;
|
||||
this.initialStyles = getInitialStyleForItem(domainObject, layoutItem);
|
||||
this.initialStyles = getApplicableStylesForItem(domainObject, layoutItem);
|
||||
this.$nextTick(() => {
|
||||
this.removeListeners();
|
||||
if (this.domainObject) {
|
||||
this.stopObserving = this.openmct.objects.observe(this.domainObject, '*', newDomainObject => this.domainObject = newDomainObject);
|
||||
this.stopObservingItems = this.openmct.objects.observe(this.domainObject, 'configuration.items', this.updateDomainObjectItemStyles);
|
||||
}
|
||||
});
|
||||
},
|
||||
removeListeners() {
|
||||
if (this.stopObserving) {
|
||||
this.stopObserving();
|
||||
}
|
||||
if (this.domainObject) {
|
||||
this.stopObserving = this.openmct.objects.observe(this.domainObject, '*', newDomainObject => this.domainObject = newDomainObject);
|
||||
if (this.stopObservingItems) {
|
||||
this.stopObservingItems();
|
||||
}
|
||||
if (this.stopProvidingTelemetry) {
|
||||
this.stopProvidingTelemetry();
|
||||
}
|
||||
},
|
||||
initialize(conditionSetDomainObject) {
|
||||
@@ -192,6 +206,13 @@ export default {
|
||||
},
|
||||
setEditState(isEditing) {
|
||||
this.isEditing = isEditing;
|
||||
if (this.isEditing) {
|
||||
if (this.stopProvidingTelemetry) {
|
||||
this.stopProvidingTelemetry();
|
||||
}
|
||||
} else {
|
||||
this.subscribeToConditionSet();
|
||||
}
|
||||
},
|
||||
addConditionSet() {
|
||||
let conditionSetDomainObject;
|
||||
@@ -262,6 +283,8 @@ export default {
|
||||
let domainObjectStyles = (this.domainObject.configuration && this.domainObject.configuration.objectStyles) || {};
|
||||
if (this.itemId) {
|
||||
domainObjectStyles[this.itemId].conditionSetIdentifier = undefined;
|
||||
domainObjectStyles[this.itemId].selectedConditionId = undefined;
|
||||
domainObjectStyles[this.itemId].defaultConditionId = undefined;
|
||||
delete domainObjectStyles[this.itemId].conditionSetIdentifier;
|
||||
domainObjectStyles[this.itemId].styles = undefined;
|
||||
delete domainObjectStyles[this.itemId].styles;
|
||||
@@ -270,6 +293,8 @@ export default {
|
||||
}
|
||||
} else {
|
||||
domainObjectStyles.conditionSetIdentifier = undefined;
|
||||
domainObjectStyles.selectedConditionId = undefined;
|
||||
domainObjectStyles.defaultConditionId = undefined;
|
||||
delete domainObjectStyles.conditionSetIdentifier;
|
||||
domainObjectStyles.styles = undefined;
|
||||
delete domainObjectStyles.styles;
|
||||
@@ -279,6 +304,43 @@ export default {
|
||||
}
|
||||
|
||||
this.persist(domainObjectStyles);
|
||||
if (this.stopProvidingTelemetry) {
|
||||
this.stopProvidingTelemetry();
|
||||
}
|
||||
},
|
||||
updateDomainObjectItemStyles(newItems) {
|
||||
//check that all items that have been styles still exist. Otherwise delete those styles
|
||||
let domainObjectStyles = (this.domainObject.configuration && this.domainObject.configuration.objectStyles) || {};
|
||||
let itemsToRemove = [];
|
||||
let keys = Object.keys(domainObjectStyles);
|
||||
//TODO: Need an easier way to find which properties are itemIds
|
||||
keys.forEach((key) => {
|
||||
const keyIsItemId = (key !== 'styles') &&
|
||||
(key !== 'staticStyle') &&
|
||||
(key !== 'defaultConditionId') &&
|
||||
(key !== 'selectedConditionId') &&
|
||||
(key !== 'conditionSetIdentifier');
|
||||
if (keyIsItemId) {
|
||||
if (!(newItems.find(item => item.id === key))) {
|
||||
itemsToRemove.push(key);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (itemsToRemove.length) {
|
||||
this.removeItemStyles(itemsToRemove, domainObjectStyles);
|
||||
}
|
||||
},
|
||||
removeItemStyles(itemIds, domainObjectStyles) {
|
||||
itemIds.forEach(itemId => {
|
||||
if (domainObjectStyles[itemId]) {
|
||||
domainObjectStyles[itemId] = undefined;
|
||||
delete domainObjectStyles[this.itemId];
|
||||
}
|
||||
});
|
||||
if (_.isEmpty(domainObjectStyles)) {
|
||||
domainObjectStyles = undefined;
|
||||
}
|
||||
this.persist(domainObjectStyles);
|
||||
},
|
||||
initializeConditionalStyles() {
|
||||
if (!this.conditions) {
|
||||
@@ -286,6 +348,9 @@ export default {
|
||||
}
|
||||
let conditionalStyles = [];
|
||||
this.conditionSetDomainObject.configuration.conditionCollection.forEach((conditionConfiguration, index) => {
|
||||
if (conditionConfiguration.isDefault) {
|
||||
this.selectedConditionId = conditionConfiguration.id;
|
||||
}
|
||||
this.conditions[conditionConfiguration.id] = conditionConfiguration;
|
||||
let foundStyle = this.findStyleByConditionId(conditionConfiguration.id);
|
||||
if (foundStyle) {
|
||||
@@ -301,13 +366,39 @@ export default {
|
||||
//we're doing this so that we remove styles for any conditions that have been removed from the condition set
|
||||
this.conditionalStyles = conditionalStyles;
|
||||
this.conditionsLoaded = true;
|
||||
this.persist(this.getDomainObjectConditionalStyle());
|
||||
this.persist(this.getDomainObjectConditionalStyle(this.selectedConditionId));
|
||||
if (!this.isEditing) {
|
||||
this.subscribeToConditionSet();
|
||||
}
|
||||
},
|
||||
subscribeToConditionSet() {
|
||||
if (this.stopProvidingTelemetry) {
|
||||
this.stopProvidingTelemetry();
|
||||
}
|
||||
if (this.conditionSetDomainObject) {
|
||||
this.openmct.telemetry.request(this.conditionSetDomainObject)
|
||||
.then(output => {
|
||||
if (output && output.length) {
|
||||
this.handleConditionSetResultUpdated(output[0]);
|
||||
}
|
||||
});
|
||||
this.stopProvidingTelemetry = this.openmct.telemetry.subscribe(this.conditionSetDomainObject, this.handleConditionSetResultUpdated.bind(this));
|
||||
}
|
||||
},
|
||||
handleConditionSetResultUpdated(resultData) {
|
||||
this.selectedConditionId = resultData ? resultData.conditionId : '';
|
||||
},
|
||||
initializeStaticStyle(objectStyles) {
|
||||
let staticStyle = objectStyles && objectStyles.staticStyle;
|
||||
this.staticStyle = staticStyle || {
|
||||
style: Object.assign({}, this.initialStyles)
|
||||
};
|
||||
if (staticStyle) {
|
||||
this.staticStyle = {
|
||||
style: Object.assign({}, this.initialStyles, staticStyle.style)
|
||||
};
|
||||
} else {
|
||||
this.staticStyle = {
|
||||
style: Object.assign({}, this.initialStyles)
|
||||
};
|
||||
}
|
||||
},
|
||||
findStyleByConditionId(id) {
|
||||
return this.conditionalStyles.find(conditionalStyle => conditionalStyle.conditionId === id);
|
||||
@@ -320,14 +411,19 @@ export default {
|
||||
let found = this.findStyleByConditionId(conditionStyle.conditionId);
|
||||
if (found) {
|
||||
found.style = conditionStyle.style;
|
||||
this.selectedConditionId = found.conditionId;
|
||||
this.persist(this.getDomainObjectConditionalStyle());
|
||||
}
|
||||
},
|
||||
getDomainObjectConditionalStyle() {
|
||||
getDomainObjectConditionalStyle(defaultConditionId) {
|
||||
let objectStyle = {
|
||||
styles: this.conditionalStyles,
|
||||
staticStyle: this.staticStyle
|
||||
staticStyle: this.staticStyle,
|
||||
selectedConditionId: this.selectedConditionId
|
||||
};
|
||||
if (defaultConditionId) {
|
||||
objectStyle.defaultConditionId = defaultConditionId;
|
||||
}
|
||||
if (this.conditionSetDomainObject) {
|
||||
objectStyle.conditionSetIdentifier = this.conditionSetDomainObject.identifier;
|
||||
}
|
||||
@@ -349,6 +445,10 @@ export default {
|
||||
getCondition(id) {
|
||||
return this.conditions ? this.conditions[id] : {};
|
||||
},
|
||||
applySelectedConditionStyle(conditionId) {
|
||||
this.selectedConditionId = conditionId;
|
||||
this.persist(this.getDomainObjectConditionalStyle());
|
||||
},
|
||||
persist(style) {
|
||||
this.openmct.objects.mutate(this.domainObject, 'configuration.objectStyles', style);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2020, 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.
|
||||
*****************************************************************************/
|
||||
|
||||
<template>
|
||||
<div class="c-inspector__styles c-inspect-styles">
|
||||
<div class="c-inspect-styles__header">
|
||||
Object Style
|
||||
</div>
|
||||
<div class="c-inspect-styles__content">
|
||||
<div v-if="isStaticAndConditionalStyles"
|
||||
class="c-inspect-styles__mixed-static-and-conditional u-alert u-alert--block u-alert--with-icon"
|
||||
>
|
||||
Your selection includes one or more items that use Conditional Styling. Applying a static style below will replace any Conditional Styling with the new choice.
|
||||
</div>
|
||||
<div v-if="staticStyle"
|
||||
class="c-inspect-styles__style"
|
||||
>
|
||||
<style-editor class="c-inspect-styles__editor"
|
||||
:style-item="staticStyle"
|
||||
:is-editing="isEditing"
|
||||
:mixed-styles="mixedStyles"
|
||||
@persist="updateStaticStyle"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import StyleEditor from "./StyleEditor.vue";
|
||||
import PreviewAction from "@/ui/preview/PreviewAction.js";
|
||||
import { getApplicableStylesForItem, getConsolidatedStyleValues, getConditionalStyleForItem } from "@/plugins/condition/utils/styleUtils";
|
||||
|
||||
export default {
|
||||
name: 'MultiSelectStylesView',
|
||||
components: {
|
||||
StyleEditor
|
||||
},
|
||||
inject: [
|
||||
'openmct',
|
||||
'selection'
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
staticStyle: undefined,
|
||||
isEditing: this.openmct.editor.isEditing(),
|
||||
mixedStyles: [],
|
||||
isStaticAndConditionalStyles: false
|
||||
}
|
||||
},
|
||||
destroyed() {
|
||||
this.removeListeners();
|
||||
},
|
||||
mounted() {
|
||||
this.items = [];
|
||||
this.previewAction = new PreviewAction(this.openmct);
|
||||
this.getObjectsAndItemsFromSelection();
|
||||
this.initializeStaticStyle();
|
||||
this.openmct.editor.on('isEditing', this.setEditState);
|
||||
},
|
||||
methods: {
|
||||
isItemType(type, item) {
|
||||
return item && (item.type === type);
|
||||
},
|
||||
hasConditionalStyles(domainObject, id) {
|
||||
return getConditionalStyleForItem(domainObject, id) !== undefined;
|
||||
},
|
||||
getObjectsAndItemsFromSelection() {
|
||||
let domainObject;
|
||||
let subObjects = [];
|
||||
|
||||
//multiple selection
|
||||
let itemInitialStyles = [];
|
||||
let itemStyle;
|
||||
this.selection.forEach((selectionItem) => {
|
||||
const item = selectionItem[0].context.item;
|
||||
const layoutItem = selectionItem[0].context.layoutItem;
|
||||
if (item && this.isItemType('subobject-view', layoutItem)) {
|
||||
subObjects.push(item);
|
||||
itemStyle = getApplicableStylesForItem(item);
|
||||
if (!this.isStaticAndConditionalStyles) {
|
||||
this.isStaticAndConditionalStyles = this.hasConditionalStyles(item);
|
||||
}
|
||||
} else {
|
||||
domainObject = selectionItem[1].context.item;
|
||||
itemStyle = getApplicableStylesForItem(domainObject, layoutItem || item);
|
||||
this.items.push({
|
||||
id: layoutItem.id,
|
||||
applicableStyles: itemStyle
|
||||
});
|
||||
if (!this.isStaticAndConditionalStyles) {
|
||||
this.isStaticAndConditionalStyles = this.hasConditionalStyles(domainObject, layoutItem.id);
|
||||
}
|
||||
}
|
||||
itemInitialStyles.push(itemStyle);
|
||||
});
|
||||
const {styles, mixedStyles} = getConsolidatedStyleValues(itemInitialStyles);
|
||||
this.initialStyles = styles;
|
||||
this.mixedStyles = mixedStyles;
|
||||
|
||||
this.domainObject = domainObject;
|
||||
this.removeListeners();
|
||||
if (this.domainObject) {
|
||||
this.stopObserving = this.openmct.objects.observe(this.domainObject, '*', newDomainObject => this.domainObject = newDomainObject);
|
||||
this.stopObservingItems = this.openmct.objects.observe(this.domainObject, 'configuration.items', this.updateDomainObjectItemStyles);
|
||||
}
|
||||
|
||||
subObjects.forEach(this.registerListener);
|
||||
},
|
||||
updateDomainObjectItemStyles(newItems) {
|
||||
//check that all items that have been styles still exist. Otherwise delete those styles
|
||||
let keys = Object.keys(this.domainObject.configuration.objectStyles || {});
|
||||
keys.forEach((key) => {
|
||||
if ((key !== 'styles') &&
|
||||
(key !== 'staticStyle') &&
|
||||
(key !== 'conditionSetIdentifier')) {
|
||||
if (!(newItems.find(item => item.id === key))) {
|
||||
this.removeItemStyles(key);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
registerListener(domainObject) {
|
||||
let id = this.openmct.objects.makeKeyString(domainObject.identifier);
|
||||
|
||||
if (!this.domainObjectsById) {
|
||||
this.domainObjectsById = {};
|
||||
}
|
||||
|
||||
if (!this.domainObjectsById[id]) {
|
||||
this.domainObjectsById[id] = domainObject;
|
||||
this.observeObject(domainObject, id);
|
||||
}
|
||||
},
|
||||
observeObject(domainObject, id) {
|
||||
let unobserveObject = this.openmct.objects.observe(domainObject, '*', function (newObject) {
|
||||
this.domainObjectsById[id] = JSON.parse(JSON.stringify(newObject));
|
||||
}.bind(this));
|
||||
this.unObserveObjects.push(unobserveObject);
|
||||
},
|
||||
removeListeners() {
|
||||
if (this.stopObserving) {
|
||||
this.stopObserving();
|
||||
}
|
||||
if (this.stopObservingItems) {
|
||||
this.stopObservingItems();
|
||||
}
|
||||
if (this.unObserveObjects) {
|
||||
this.unObserveObjects.forEach((unObserveObject) => {
|
||||
unObserveObject();
|
||||
});
|
||||
}
|
||||
this.unObserveObjects = [];
|
||||
},
|
||||
removeItemStyles(itemId) {
|
||||
let domainObjectStyles = (this.domainObject.configuration && this.domainObject.configuration.objectStyles) || {};
|
||||
if (itemId && domainObjectStyles[itemId]) {
|
||||
domainObjectStyles[itemId] = undefined;
|
||||
delete domainObjectStyles[this.itemId];
|
||||
|
||||
if (_.isEmpty(domainObjectStyles)) {
|
||||
domainObjectStyles = undefined;
|
||||
}
|
||||
this.persist(this.domainObject, domainObjectStyles);
|
||||
}
|
||||
},
|
||||
removeConditionalStyles(domainObjectStyles, itemId) {
|
||||
if (itemId) {
|
||||
domainObjectStyles[itemId].conditionSetIdentifier = undefined;
|
||||
delete domainObjectStyles[itemId].conditionSetIdentifier;
|
||||
domainObjectStyles[itemId].styles = undefined;
|
||||
delete domainObjectStyles[itemId].styles;
|
||||
} else {
|
||||
domainObjectStyles.conditionSetIdentifier = undefined;
|
||||
delete domainObjectStyles.conditionSetIdentifier;
|
||||
domainObjectStyles.styles = undefined;
|
||||
delete domainObjectStyles.styles;
|
||||
}
|
||||
},
|
||||
setEditState(isEditing) {
|
||||
this.isEditing = isEditing;
|
||||
},
|
||||
initializeStaticStyle() {
|
||||
this.staticStyle = {
|
||||
style: Object.assign({}, this.initialStyles)
|
||||
};
|
||||
},
|
||||
updateStaticStyle(staticStyle, property) {
|
||||
//update the static style for each of the layoutItems as well as each sub object item
|
||||
this.staticStyle = staticStyle;
|
||||
this.persist(this.domainObject, this.getDomainObjectStyle(this.domainObject, property, this.items));
|
||||
if (this.domainObjectsById) {
|
||||
const keys = Object.keys(this.domainObjectsById);
|
||||
keys.forEach(key => {
|
||||
let domainObject = this.domainObjectsById[key];
|
||||
this.persist(domainObject, this.getDomainObjectStyle(domainObject, property));
|
||||
});
|
||||
}
|
||||
this.isStaticAndConditionalStyles = false;
|
||||
let foundIndex = this.mixedStyles.indexOf(property);
|
||||
if (foundIndex > -1) {
|
||||
this.mixedStyles.splice(foundIndex, 1);
|
||||
}
|
||||
},
|
||||
getDomainObjectStyle(domainObject, property, items) {
|
||||
let domainObjectStyles = (domainObject.configuration && domainObject.configuration.objectStyles) || {};
|
||||
|
||||
if (items) {
|
||||
items.forEach(item => {
|
||||
let itemStaticStyle = {};
|
||||
if (domainObjectStyles[item.id] && domainObjectStyles[item.id].staticStyle) {
|
||||
itemStaticStyle = domainObjectStyles[item.id].staticStyle.style;
|
||||
}
|
||||
Object.keys(item.applicableStyles).forEach(key => {
|
||||
if (property === key) {
|
||||
itemStaticStyle[key] = this.staticStyle.style[key];
|
||||
}
|
||||
});
|
||||
if (this.isStaticAndConditionalStyles) {
|
||||
this.removeConditionalStyles(domainObjectStyles, item.id);
|
||||
}
|
||||
if (_.isEmpty(itemStaticStyle)) {
|
||||
itemStaticStyle = undefined;
|
||||
domainObjectStyles[item.id] = undefined;
|
||||
} else {
|
||||
domainObjectStyles[item.id] = Object.assign({}, { staticStyle: { style: itemStaticStyle } });
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (!domainObjectStyles.staticStyle) {
|
||||
domainObjectStyles.staticStyle = {
|
||||
style: {}
|
||||
}
|
||||
}
|
||||
if (this.isStaticAndConditionalStyles) {
|
||||
this.removeConditionalStyles(domainObjectStyles);
|
||||
}
|
||||
domainObjectStyles.staticStyle.style[property] = this.staticStyle.style[property];
|
||||
}
|
||||
|
||||
return domainObjectStyles;
|
||||
},
|
||||
|
||||
persist(domainObject, style) {
|
||||
this.openmct.objects.mutate(domainObject, 'configuration.objectStyles', style);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -22,12 +22,15 @@
|
||||
|
||||
<template>
|
||||
<div class="c-style">
|
||||
<span class="c-style-thumb"
|
||||
:class="{ 'is-style-invisible': styleItem.style.isStyleInvisible }"
|
||||
<span :class="[
|
||||
{ 'is-style-invisible': styleItem.style.isStyleInvisible },
|
||||
{ 'c-style-thumb--mixed': mixedStyles.indexOf('backgroundColor') > -1 }
|
||||
]"
|
||||
:style="[styleItem.style.imageUrl ? { backgroundImage:'url(' + styleItem.style.imageUrl + ')'} : itemStyle ]"
|
||||
class="c-style-thumb"
|
||||
>
|
||||
<span class="c-style-thumb__text"
|
||||
:class="{ 'hide-nice': !styleItem.style.color }"
|
||||
:class="{ 'hide-nice': !hasProperty(styleItem.style.color) }"
|
||||
>
|
||||
ABC
|
||||
</span>
|
||||
@@ -68,6 +71,7 @@ import ToolbarColorPicker from "@/ui/toolbar/components/toolbar-color-picker.vue
|
||||
import ToolbarButton from "@/ui/toolbar/components/toolbar-button.vue";
|
||||
import ToolbarToggleButton from "@/ui/toolbar/components/toolbar-toggle-button.vue";
|
||||
import {STYLE_CONSTANTS} from "@/plugins/condition/utils/constants";
|
||||
import {getStylesWithoutNoneValue} from "@/plugins/condition/utils/styleUtils";
|
||||
|
||||
export default {
|
||||
name: 'StyleEditor',
|
||||
@@ -83,6 +87,12 @@ export default {
|
||||
isEditing: {
|
||||
type: Boolean
|
||||
},
|
||||
mixedStyles: {
|
||||
type: Array,
|
||||
default() {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
styleItem: {
|
||||
type: Object,
|
||||
required: true
|
||||
@@ -90,21 +100,17 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
itemStyle() {
|
||||
let style = {};
|
||||
const keys = Object.keys(this.styleItem.style);
|
||||
keys.forEach(key => {
|
||||
style[key] = this.normalizeValue(this.styleItem.style[key]);
|
||||
});
|
||||
return style;
|
||||
return getStylesWithoutNoneValue(this.styleItem.style);
|
||||
},
|
||||
borderColorOption() {
|
||||
let value = this.styleItem.style.border.replace('1px solid ', '');
|
||||
return {
|
||||
icon: 'icon-line-horz',
|
||||
title: STYLE_CONSTANTS.borderColorTitle,
|
||||
value: this.normalizeValue(value),
|
||||
value: this.normalizeValueForSwatch(value),
|
||||
property: 'border',
|
||||
isEditing: this.isEditing
|
||||
isEditing: this.isEditing,
|
||||
nonSpecific: this.mixedStyles.indexOf('border') > -1
|
||||
}
|
||||
},
|
||||
backgroundColorOption() {
|
||||
@@ -112,9 +118,10 @@ export default {
|
||||
return {
|
||||
icon: 'icon-paint-bucket',
|
||||
title: STYLE_CONSTANTS.backgroundColorTitle,
|
||||
value: this.normalizeValue(value),
|
||||
value: this.normalizeValueForSwatch(value),
|
||||
property: 'backgroundColor',
|
||||
isEditing: this.isEditing
|
||||
isEditing: this.isEditing,
|
||||
nonSpecific: this.mixedStyles.indexOf('backgroundColor') > -1
|
||||
}
|
||||
},
|
||||
colorOption() {
|
||||
@@ -122,9 +129,10 @@ export default {
|
||||
return {
|
||||
icon: 'icon-font',
|
||||
title: STYLE_CONSTANTS.textColorTitle,
|
||||
value: this.normalizeValue(value),
|
||||
value: this.normalizeValueForSwatch(value),
|
||||
property: 'color',
|
||||
isEditing: this.isEditing
|
||||
isEditing: this.isEditing,
|
||||
nonSpecific: this.mixedStyles.indexOf('color') > -1
|
||||
}
|
||||
},
|
||||
imageUrlOption() {
|
||||
@@ -149,7 +157,8 @@ export default {
|
||||
property: 'imageUrl',
|
||||
formKeys: ['url'],
|
||||
value: {url: this.styleItem.style.imageUrl},
|
||||
isEditing: this.isEditing
|
||||
isEditing: this.isEditing,
|
||||
nonSpecific: this.mixedStyles.indexOf('imageUrl') > -1
|
||||
}
|
||||
},
|
||||
isStyleInvisibleOption() {
|
||||
@@ -177,13 +186,20 @@ export default {
|
||||
hasProperty(property) {
|
||||
return property !== undefined;
|
||||
},
|
||||
normalizeValue(value) {
|
||||
if (!value) {
|
||||
return 'transparent';
|
||||
normalizeValueForSwatch(value) {
|
||||
if (value && value.indexOf('__no_value') > -1) {
|
||||
return value.replace('__no_value', 'transparent');
|
||||
}
|
||||
return value;
|
||||
},
|
||||
normalizeValueForStyle(value) {
|
||||
if (value && value === 'transparent') {
|
||||
return '__no_value';
|
||||
}
|
||||
return value;
|
||||
},
|
||||
updateStyleValue(value, item) {
|
||||
value = this.normalizeValueForStyle(value);
|
||||
if (item.property === 'border') {
|
||||
value = '1px solid ' + value;
|
||||
}
|
||||
@@ -192,7 +208,7 @@ export default {
|
||||
} else {
|
||||
this.styleItem.style[item.property] = value;
|
||||
}
|
||||
this.$emit('persist', this.styleItem);
|
||||
this.$emit('persist', this.styleItem, item.property);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,31 @@
|
||||
|
||||
&__condition {
|
||||
@include discreteItem();
|
||||
border: 1px solid transparent;
|
||||
pointer-events: none; // Prevent selecting when the object isn't being edited
|
||||
|
||||
&.is-current {
|
||||
$c: $colorBodyFg;
|
||||
border-color: rgba($c, 0.2);
|
||||
background: rgba($c, 0.2);
|
||||
}
|
||||
|
||||
.is-editing & {
|
||||
cursor: pointer;
|
||||
pointer-events: initial;
|
||||
transition: $transOut;
|
||||
|
||||
&:hover {
|
||||
background: rgba($colorBodyFg, 0.1);
|
||||
transition: $transIn;
|
||||
}
|
||||
|
||||
&.is-current {
|
||||
$c: $editUIColorBg;
|
||||
border-color: $c;
|
||||
background: rgba($c, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.c-style {
|
||||
|
||||
@@ -20,11 +20,11 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import EventEmitter from 'EventEmitter';
|
||||
import {OPERATIONS} from '../utils/operations';
|
||||
import {computeCondition} from "@/plugins/condition/utils/evaluator";
|
||||
import TelemetryCriterion from './TelemetryCriterion';
|
||||
import { evaluateResults } from "../utils/evaluator";
|
||||
import { getLatestTimestamp } from '../utils/time';
|
||||
|
||||
export default class TelemetryCriterion extends EventEmitter {
|
||||
export default class AllTelemetryCriterion extends TelemetryCriterion {
|
||||
|
||||
/**
|
||||
* Subscribes/Unsubscribes to telemetry and emits the result
|
||||
@@ -34,23 +34,35 @@ export default class TelemetryCriterion extends EventEmitter {
|
||||
* @param openmct
|
||||
*/
|
||||
constructor(telemetryDomainObjectDefinition, openmct) {
|
||||
super();
|
||||
super(telemetryDomainObjectDefinition, openmct);
|
||||
}
|
||||
|
||||
this.openmct = openmct;
|
||||
this.objectAPI = this.openmct.objects;
|
||||
this.telemetryAPI = this.openmct.telemetry;
|
||||
this.timeAPI = this.openmct.time;
|
||||
this.id = telemetryDomainObjectDefinition.id;
|
||||
this.telemetry = telemetryDomainObjectDefinition.telemetry;
|
||||
this.operation = telemetryDomainObjectDefinition.operation;
|
||||
this.telemetryObjects = Object.assign({}, telemetryDomainObjectDefinition.telemetryObjects);
|
||||
this.input = telemetryDomainObjectDefinition.input;
|
||||
this.metadata = telemetryDomainObjectDefinition.metadata;
|
||||
initialize() {
|
||||
this.telemetryObjects = { ...this.telemetryDomainObjectDefinition.telemetryObjects };
|
||||
this.telemetryDataCache = {};
|
||||
}
|
||||
|
||||
isValid() {
|
||||
return (this.telemetry === 'any' || this.telemetry === 'all') && this.metadata && this.operation;
|
||||
}
|
||||
|
||||
updateTelemetry(telemetryObjects) {
|
||||
this.telemetryObjects = Object.assign({}, telemetryObjects);
|
||||
this.telemetryObjects = { ...telemetryObjects };
|
||||
this.removeTelemetryDataCache();
|
||||
}
|
||||
|
||||
removeTelemetryDataCache() {
|
||||
const telemetryCacheIds = Object.keys(this.telemetryDataCache);
|
||||
Object.values(this.telemetryObjects).forEach(telemetryObject => {
|
||||
const id = this.openmct.objects.makeKeyString(telemetryObject.identifier);
|
||||
const foundIndex = telemetryCacheIds.indexOf(id);
|
||||
if (foundIndex > -1) {
|
||||
telemetryCacheIds.splice(foundIndex, 1);
|
||||
}
|
||||
});
|
||||
telemetryCacheIds.forEach(id => {
|
||||
delete (this.telemetryDataCache[id]);
|
||||
});
|
||||
}
|
||||
|
||||
formatData(data, telemetryObjects) {
|
||||
@@ -68,105 +80,87 @@ export default class TelemetryCriterion extends EventEmitter {
|
||||
});
|
||||
|
||||
const datum = {
|
||||
result: computeCondition(this.telemetryDataCache, this.telemetry === 'all')
|
||||
result: evaluateResults(Object.values(this.telemetryDataCache), this.telemetry)
|
||||
};
|
||||
|
||||
if (data) {
|
||||
// TODO check back to see if we should format times here
|
||||
this.timeAPI.getAllTimeSystems().forEach(timeSystem => {
|
||||
this.openmct.time.getAllTimeSystems().forEach(timeSystem => {
|
||||
datum[timeSystem.key] = data[timeSystem.key]
|
||||
});
|
||||
}
|
||||
return datum;
|
||||
}
|
||||
|
||||
handleSubscription(data, telemetryObjects) {
|
||||
if(this.isValid()) {
|
||||
this.emitEvent('criterionResultUpdated', this.formatData(data, telemetryObjects));
|
||||
} else {
|
||||
this.emitEvent('criterionResultUpdated', this.formatData({}, telemetryObjects));
|
||||
}
|
||||
}
|
||||
getResult(data, telemetryObjects) {
|
||||
const validatedData = this.isValid() ? data : {};
|
||||
|
||||
findOperation(operation) {
|
||||
for (let i=0; i < OPERATIONS.length; i++) {
|
||||
if (operation === OPERATIONS[i].name) {
|
||||
return OPERATIONS[i].operation;
|
||||
}
|
||||
if (validatedData) {
|
||||
this.telemetryDataCache[validatedData.id] = this.computeResult(validatedData);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
computeResult(data) {
|
||||
let result = false;
|
||||
if (data) {
|
||||
let comparator = this.findOperation(this.operation);
|
||||
let params = [];
|
||||
params.push(data[this.metadata]);
|
||||
if (this.input instanceof Array && this.input.length) {
|
||||
this.input.forEach(input => params.push(input));
|
||||
Object.values(telemetryObjects).forEach(telemetryObject => {
|
||||
const id = this.openmct.objects.makeKeyString(telemetryObject.identifier);
|
||||
if (this.telemetryDataCache[id] === undefined) {
|
||||
this.telemetryDataCache[id] = false;
|
||||
}
|
||||
if (typeof comparator === 'function') {
|
||||
result = comparator(params);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
emitEvent(eventName, data) {
|
||||
this.emit(eventName, {
|
||||
id: this.id,
|
||||
data: data
|
||||
});
|
||||
|
||||
this.result = evaluateResults(Object.values(this.telemetryDataCache), this.telemetry);
|
||||
}
|
||||
|
||||
isValid() {
|
||||
return (this.telemetry === 'any' || this.telemetry === 'all') && this.metadata && this.operation;
|
||||
}
|
||||
|
||||
requestLAD(options) {
|
||||
options = Object.assign({},
|
||||
options,
|
||||
{
|
||||
strategy: 'latest',
|
||||
size: 1
|
||||
}
|
||||
);
|
||||
requestLAD(telemetryObjects) {
|
||||
const options = {
|
||||
strategy: 'latest',
|
||||
size: 1
|
||||
};
|
||||
|
||||
if (!this.isValid()) {
|
||||
return this.formatData({}, options.telemetryObjects);
|
||||
return this.formatData({}, telemetryObjects);
|
||||
}
|
||||
|
||||
let keys = Object.keys(Object.assign({}, options.telemetryObjects));
|
||||
let keys = Object.keys(Object.assign({}, telemetryObjects));
|
||||
const telemetryRequests = keys
|
||||
.map(key => this.telemetryAPI.request(
|
||||
options.telemetryObjects[key],
|
||||
.map(key => this.openmct.telemetry.request(
|
||||
telemetryObjects[key],
|
||||
options
|
||||
));
|
||||
|
||||
let telemetryDataCache = {};
|
||||
return Promise.all(telemetryRequests)
|
||||
.then(telemetryRequestsResults => {
|
||||
let latestDatum;
|
||||
let latestTimestamp;
|
||||
const timeSystems = this.openmct.time.getAllTimeSystems();
|
||||
const timeSystem = this.openmct.time.timeSystem();
|
||||
|
||||
telemetryRequestsResults.forEach((results, index) => {
|
||||
latestDatum = results.length ? results[results.length - 1] : {};
|
||||
if (index < telemetryRequestsResults.length-1) {
|
||||
if (latestDatum) {
|
||||
this.telemetryDataCache[latestDatum.id] = this.computeResult(latestDatum);
|
||||
}
|
||||
}
|
||||
const latestDatum = results.length ? results[results.length - 1] : {};
|
||||
const datumId = keys[index];
|
||||
const normalizedDatum = this.createNormalizedDatum(latestDatum, telemetryObjects[datumId]);
|
||||
|
||||
telemetryDataCache[datumId] = this.computeResult(normalizedDatum);
|
||||
|
||||
latestTimestamp = getLatestTimestamp(
|
||||
latestTimestamp,
|
||||
normalizedDatum,
|
||||
timeSystems,
|
||||
timeSystem
|
||||
);
|
||||
});
|
||||
|
||||
const datum = {
|
||||
result: evaluateResults(Object.values(telemetryDataCache), this.telemetry),
|
||||
...latestTimestamp
|
||||
};
|
||||
|
||||
return {
|
||||
id: this.id,
|
||||
data: this.formatData(latestDatum, options.telemetryObjects)
|
||||
data: datum
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.emitEvent('criterionRemoved');
|
||||
delete this.telemetryObjects;
|
||||
delete this.telemetryDataCache;
|
||||
delete this.telemetryObjectIdAsString;
|
||||
delete this.telemetryObject;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
*****************************************************************************/
|
||||
|
||||
import EventEmitter from 'EventEmitter';
|
||||
import {OPERATIONS} from '../utils/operations';
|
||||
import { OPERATIONS } from '../utils/operations';
|
||||
|
||||
export default class TelemetryCriterion extends EventEmitter {
|
||||
|
||||
@@ -36,44 +36,89 @@ export default class TelemetryCriterion extends EventEmitter {
|
||||
super();
|
||||
|
||||
this.openmct = openmct;
|
||||
this.objectAPI = this.openmct.objects;
|
||||
this.telemetryAPI = this.openmct.telemetry;
|
||||
this.timeAPI = this.openmct.time;
|
||||
this.telemetryDomainObjectDefinition = telemetryDomainObjectDefinition;
|
||||
this.id = telemetryDomainObjectDefinition.id;
|
||||
this.telemetry = telemetryDomainObjectDefinition.telemetry;
|
||||
this.operation = telemetryDomainObjectDefinition.operation;
|
||||
this.input = telemetryDomainObjectDefinition.input;
|
||||
this.metadata = telemetryDomainObjectDefinition.metadata;
|
||||
this.telemetryObject = telemetryDomainObjectDefinition.telemetryObject;
|
||||
this.telemetryObjectIdAsString = this.objectAPI.makeKeyString(telemetryDomainObjectDefinition.telemetry);
|
||||
this.on(`subscription:${this.telemetryObjectIdAsString}`, this.handleSubscription);
|
||||
this.result = undefined;
|
||||
|
||||
this.initialize();
|
||||
this.emitEvent('criterionUpdated', this);
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.telemetryObject = this.telemetryDomainObjectDefinition.telemetryObject;
|
||||
this.telemetryObjectIdAsString = this.openmct.objects.makeKeyString(this.telemetryDomainObjectDefinition.telemetry);
|
||||
}
|
||||
|
||||
isValid() {
|
||||
return this.telemetryObject && this.metadata && this.operation;
|
||||
}
|
||||
|
||||
updateTelemetry(telemetryObjects) {
|
||||
this.telemetryObject = telemetryObjects[this.telemetryObjectIdAsString];
|
||||
}
|
||||
|
||||
createNormalizedDatum(telemetryDatum, endpoint) {
|
||||
const id = this.openmct.objects.makeKeyString(endpoint.identifier);
|
||||
const metadata = this.openmct.telemetry.getMetadata(endpoint).valueMetadatas;
|
||||
|
||||
const normalizedDatum = Object.values(metadata).reduce((datum, metadatum) => {
|
||||
const formatter = this.openmct.telemetry.getValueFormatter(metadatum);
|
||||
datum[metadatum.key] = formatter.parse(telemetryDatum[metadatum.source]);
|
||||
return datum;
|
||||
}, {});
|
||||
|
||||
normalizedDatum.id = id;
|
||||
|
||||
return normalizedDatum;
|
||||
}
|
||||
|
||||
formatData(data) {
|
||||
const datum = {
|
||||
result: this.computeResult(data)
|
||||
};
|
||||
|
||||
if (data) {
|
||||
// TODO check back to see if we should format times here
|
||||
this.timeAPI.getAllTimeSystems().forEach(timeSystem => {
|
||||
this.openmct.time.getAllTimeSystems().forEach(timeSystem => {
|
||||
datum[timeSystem.key] = data[timeSystem.key]
|
||||
});
|
||||
}
|
||||
return datum;
|
||||
}
|
||||
|
||||
handleSubscription(data) {
|
||||
if(this.isValid()) {
|
||||
this.emitEvent('criterionResultUpdated', this.formatData(data));
|
||||
} else {
|
||||
this.emitEvent('criterionResultUpdated', this.formatData({}));
|
||||
getResult(data) {
|
||||
const validatedData = this.isValid() ? data : {};
|
||||
this.result = this.computeResult(validatedData);
|
||||
}
|
||||
|
||||
requestLAD() {
|
||||
const options = {
|
||||
strategy: 'latest',
|
||||
size: 1
|
||||
};
|
||||
|
||||
if (!this.isValid()) {
|
||||
return {
|
||||
id: this.id,
|
||||
data: this.formatData({})
|
||||
};
|
||||
}
|
||||
|
||||
return this.openmct.telemetry.request(
|
||||
this.telemetryObject,
|
||||
options
|
||||
).then(results => {
|
||||
const latestDatum = results.length ? results[results.length - 1] : {};
|
||||
const normalizedDatum = this.createNormalizedDatum(latestDatum, this.telemetryObject);
|
||||
|
||||
return {
|
||||
id: this.id,
|
||||
data: this.formatData(normalizedDatum)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
findOperation(operation) {
|
||||
@@ -95,7 +140,7 @@ export default class TelemetryCriterion extends EventEmitter {
|
||||
this.input.forEach(input => params.push(input));
|
||||
}
|
||||
if (typeof comparator === 'function') {
|
||||
result = comparator(params);
|
||||
result = !!comparator(params);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
@@ -108,42 +153,9 @@ export default class TelemetryCriterion extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
isValid() {
|
||||
return this.telemetryObject && this.metadata && this.operation;
|
||||
}
|
||||
|
||||
requestLAD(options) {
|
||||
options = Object.assign({},
|
||||
options,
|
||||
{
|
||||
strategy: 'latest',
|
||||
size: 1
|
||||
}
|
||||
);
|
||||
|
||||
if (!this.isValid()) {
|
||||
return {
|
||||
id: this.id,
|
||||
data: this.formatData({})
|
||||
};
|
||||
}
|
||||
|
||||
return this.telemetryAPI.request(
|
||||
this.telemetryObject,
|
||||
options
|
||||
).then(results => {
|
||||
const latestDatum = results.length ? results[results.length - 1] : {};
|
||||
return {
|
||||
id: this.id,
|
||||
data: this.formatData(latestDatum)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.off(`subscription:${this.telemetryObjectIdAsString}`, this.handleSubscription);
|
||||
this.emitEvent('criterionRemoved');
|
||||
delete this.telemetryObjectIdAsString;
|
||||
delete this.telemetryObject;
|
||||
delete this.telemetryObjectIdAsString;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ describe("The telemetry criterion", function () {
|
||||
key: "testSource",
|
||||
source: "value",
|
||||
name: "Test",
|
||||
format: "enum"
|
||||
format: "string"
|
||||
}]
|
||||
}
|
||||
};
|
||||
@@ -80,8 +80,9 @@ describe("The telemetry criterion", function () {
|
||||
testCriterionDefinition = {
|
||||
id: 'test-criterion-id',
|
||||
telemetry: openmct.objects.makeKeyString(testTelemetryObject.identifier),
|
||||
operation: 'lessThan',
|
||||
metadata: 'sin',
|
||||
operation: 'textContains',
|
||||
metadata: 'value',
|
||||
input: ['Hell'],
|
||||
telemetryObject: testTelemetryObject
|
||||
};
|
||||
|
||||
@@ -100,12 +101,21 @@ describe("The telemetry criterion", function () {
|
||||
expect(telemetryCriterion.telemetryObjectIdAsString).toEqual(testTelemetryObject.identifier.key);
|
||||
});
|
||||
|
||||
it("updates and emits event on new data from telemetry providers", function () {
|
||||
spyOn(telemetryCriterion, 'emitEvent').and.callThrough();
|
||||
telemetryCriterion.handleSubscription({
|
||||
it("returns a result on new data from relevant telemetry providers", function () {
|
||||
telemetryCriterion.getResult({
|
||||
value: 'Hello',
|
||||
utc: 'Hi'
|
||||
utc: 'Hi',
|
||||
id: testTelemetryObject.identifier.key
|
||||
});
|
||||
expect(telemetryCriterion.emitEvent).toHaveBeenCalled();
|
||||
expect(telemetryCriterion.result).toBeTrue();
|
||||
});
|
||||
|
||||
// it("does not return a result on new data from irrelavant telemetry providers", function () {
|
||||
// telemetryCriterion.getResult({
|
||||
// value: 'Hello',
|
||||
// utc: 'Hi',
|
||||
// id: '1234'
|
||||
// });
|
||||
// expect(telemetryCriterion.result).toBeFalse();
|
||||
// });
|
||||
});
|
||||
|
||||
@@ -19,36 +19,50 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import { TRIGGER } from "./constants";
|
||||
|
||||
export const computeCondition = (resultMap, allMustBeTrue) => {
|
||||
let result = false;
|
||||
for (let key in resultMap) {
|
||||
if (resultMap.hasOwnProperty(key)) {
|
||||
result = resultMap[key];
|
||||
if (allMustBeTrue && !result) {
|
||||
//If we want all conditions to be true, then even one negative result should break.
|
||||
break;
|
||||
} else if (!allMustBeTrue && result) {
|
||||
//If we want at least one condition to be true, then even one positive result should break.
|
||||
break;
|
||||
}
|
||||
export const evaluateResults = (results, trigger) => {
|
||||
if (trigger && trigger === TRIGGER.XOR) {
|
||||
return matchExact(results, 1);
|
||||
} else if (trigger && trigger === TRIGGER.NOT) {
|
||||
return matchExact(results, 0);
|
||||
} else if (trigger && trigger === TRIGGER.ALL) {
|
||||
return matchAll(results);
|
||||
} else {
|
||||
return matchAny(results);
|
||||
}
|
||||
}
|
||||
|
||||
function matchAll(results) {
|
||||
for (const result of results) {
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
//Returns true only if limit number of results are satisfied
|
||||
export const computeConditionByLimit = (resultMap, limit) => {
|
||||
let trueCount = 0;
|
||||
for (let key in resultMap) {
|
||||
if (resultMap.hasOwnProperty(key)) {
|
||||
if (resultMap[key]) {
|
||||
trueCount++;
|
||||
}
|
||||
if (trueCount > limit) {
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function matchAny(results) {
|
||||
for (const result of results) {
|
||||
if (result) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return trueCount === limit;
|
||||
};
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function matchExact(results, target) {
|
||||
let matches = 0;
|
||||
for (const result of results) {
|
||||
if (result) {
|
||||
matches++;
|
||||
}
|
||||
if (matches > target) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return matches === target;
|
||||
}
|
||||
|
||||
@@ -20,47 +20,185 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import { computeConditionByLimit } from "./evaluator";
|
||||
import { evaluateResults } from './evaluator';
|
||||
import { TRIGGER } from './constants';
|
||||
|
||||
describe('evaluate results based on trigger', function () {
|
||||
describe('evaluate results', () => {
|
||||
// const allTrue = [true, true, true, true, true];
|
||||
// const oneTrue = [false, false, false, false, true];
|
||||
// const multipleTrue = [false, true, false, true, false];
|
||||
// const noneTrue = [false, false, false, false, false];
|
||||
// const allTrueWithUndefined = [true, true, true, undefined, true];
|
||||
// const oneTrueWithUndefined = [undefined, undefined, undefined, undefined, true];
|
||||
// const multipleTrueWithUndefined = [true, undefined, true, undefined, true];
|
||||
// const allUndefined = [undefined, undefined, undefined, undefined, undefined];
|
||||
// const singleTrue = [true];
|
||||
// const singleFalse = [false];
|
||||
// const singleUndefined = [undefined];
|
||||
// const empty = [];
|
||||
|
||||
it('should evaluate to true if trigger is NOT', () => {
|
||||
const results = {
|
||||
result: false,
|
||||
result1: false,
|
||||
result2: false
|
||||
};
|
||||
const result = computeConditionByLimit(results, 0);
|
||||
expect(result).toBeTrue();
|
||||
const tests = [
|
||||
{
|
||||
name: 'allTrue',
|
||||
values: [true, true, true, true, true],
|
||||
any: true,
|
||||
all: true,
|
||||
not: false,
|
||||
xor: false
|
||||
}, {
|
||||
name: 'oneTrue',
|
||||
values: [false, false, false, false, true],
|
||||
any: true,
|
||||
all: false,
|
||||
not: false,
|
||||
xor: true
|
||||
}, {
|
||||
name: 'multipleTrue',
|
||||
values: [false, true, false, true, false],
|
||||
any: true,
|
||||
all: false,
|
||||
not: false,
|
||||
xor: false
|
||||
}, {
|
||||
name: 'noneTrue',
|
||||
values: [false, false, false, false, false],
|
||||
any: false,
|
||||
all: false,
|
||||
not: true,
|
||||
xor: false
|
||||
}, {
|
||||
name: 'allTrueWithUndefined',
|
||||
values: [true, true, true, undefined, true],
|
||||
any: true,
|
||||
all: false,
|
||||
not: false,
|
||||
xor: false
|
||||
}, {
|
||||
name: 'oneTrueWithUndefined',
|
||||
values: [undefined, undefined, undefined, undefined, true],
|
||||
any: true,
|
||||
all: false,
|
||||
not: false,
|
||||
xor: true
|
||||
}, {
|
||||
name: 'multipleTrueWithUndefined',
|
||||
values: [true, undefined, true, undefined, true],
|
||||
any: true,
|
||||
all: false,
|
||||
not: false,
|
||||
xor: false
|
||||
}, {
|
||||
name: 'allUndefined',
|
||||
values: [undefined, undefined, undefined, undefined, undefined],
|
||||
any: false,
|
||||
all: false,
|
||||
not: true,
|
||||
xor: false
|
||||
}, {
|
||||
name: 'singleTrue',
|
||||
values: [true],
|
||||
any: true,
|
||||
all: true,
|
||||
not: false,
|
||||
xor: true
|
||||
}, {
|
||||
name: 'singleFalse',
|
||||
values: [false],
|
||||
any: false,
|
||||
all: false,
|
||||
not: true,
|
||||
xor: false
|
||||
}, {
|
||||
name: 'singleUndefined',
|
||||
values: [undefined],
|
||||
any: false,
|
||||
all: false,
|
||||
not: true,
|
||||
xor: false
|
||||
}
|
||||
// , {
|
||||
// name: 'empty',
|
||||
// values: [],
|
||||
// any: false,
|
||||
// all: false,
|
||||
// not: true,
|
||||
// xor: false
|
||||
// }
|
||||
];
|
||||
|
||||
describe(`based on trigger ${TRIGGER.ANY}`, () => {
|
||||
it('should evaluate to expected result', () => {
|
||||
tests.forEach(test => {
|
||||
const result = evaluateResults(test.values, TRIGGER.ANY);
|
||||
expect(result).toEqual(test[TRIGGER.ANY])
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should evaluate to false if trigger is NOT', () => {
|
||||
const results = {
|
||||
result: true,
|
||||
result1: false,
|
||||
result2: false
|
||||
};
|
||||
const result = computeConditionByLimit(results, 0);
|
||||
expect(result).toBeFalse();
|
||||
describe(`based on trigger ${TRIGGER.ALL}`, () => {
|
||||
it('should evaluate to expected result', () => {
|
||||
tests.forEach(test => {
|
||||
const result = evaluateResults(test.values, TRIGGER.ALL);
|
||||
expect(result).toEqual(test[TRIGGER.ALL])
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should evaluate to true if trigger is XOR', () => {
|
||||
const results = {
|
||||
result: false,
|
||||
result1: true,
|
||||
result2: false
|
||||
};
|
||||
const result = computeConditionByLimit(results, 1);
|
||||
expect(result).toBeTrue();
|
||||
describe(`based on trigger ${TRIGGER.NOT}`, () => {
|
||||
it('should evaluate to expected result', () => {
|
||||
tests.forEach(test => {
|
||||
const result = evaluateResults(test.values, TRIGGER.NOT);
|
||||
expect(result).toEqual(test[TRIGGER.NOT])
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should evaluate to false if trigger is XOR', () => {
|
||||
const results = {
|
||||
result: false,
|
||||
result1: true,
|
||||
result2: true
|
||||
};
|
||||
const result = computeConditionByLimit(results, 1);
|
||||
expect(result).toBeFalse();
|
||||
describe(`based on trigger ${TRIGGER.XOR}`, () => {
|
||||
it('should evaluate to expected result', () => {
|
||||
tests.forEach(test => {
|
||||
const result = evaluateResults(test.values, TRIGGER.XOR);
|
||||
expect(result).toEqual(test[TRIGGER.XOR])
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// it('should evaluate to true if trigger is NOT', () => {
|
||||
// const results = {
|
||||
// result: false,
|
||||
// result1: false,
|
||||
// result2: false
|
||||
// };
|
||||
// const result = computeConditionByLimit(results, 0);
|
||||
// expect(result).toBeTrue();
|
||||
// });
|
||||
|
||||
// it('should evaluate to false if trigger is NOT', () => {
|
||||
// const results = {
|
||||
// result: true,
|
||||
// result1: false,
|
||||
// result2: false
|
||||
// };
|
||||
// const result = computeConditionByLimit(results, 0);
|
||||
// expect(result).toBeFalse();
|
||||
// });
|
||||
|
||||
// it('should evaluate to true if trigger is XOR', () => {
|
||||
// const results = {
|
||||
// result: false,
|
||||
// result1: true,
|
||||
// result2: false
|
||||
// };
|
||||
// const result = computeConditionByLimit(results, 1);
|
||||
// expect(result).toBeTrue();
|
||||
// });
|
||||
|
||||
// it('should evaluate to false if trigger is XOR', () => {
|
||||
// const results = {
|
||||
// result: false,
|
||||
// result1: true,
|
||||
// result2: true
|
||||
// };
|
||||
// const result = computeConditionByLimit(results, 1);
|
||||
// expect(result).toBeFalse();
|
||||
// });
|
||||
});
|
||||
|
||||
@@ -22,6 +22,22 @@
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
const convertToNumbers = (input) => {
|
||||
let numberInputs = [];
|
||||
input.forEach(inputValue => numberInputs.push(Number(inputValue)));
|
||||
return numberInputs;
|
||||
};
|
||||
|
||||
const convertToStrings = (input) => {
|
||||
let stringInputs = [];
|
||||
input.forEach(inputValue => stringInputs.push(inputValue !== undefined ? inputValue.toString() : ''));
|
||||
return stringInputs;
|
||||
};
|
||||
|
||||
const joinValues = (values, length) => {
|
||||
return values.slice(0, length).join(', ');
|
||||
};
|
||||
|
||||
export const OPERATIONS = [
|
||||
{
|
||||
name: 'equalTo',
|
||||
@@ -32,7 +48,7 @@ export const OPERATIONS = [
|
||||
appliesTo: ['number'],
|
||||
inputCount: 1,
|
||||
getDescription: function (values) {
|
||||
return ' is ' + values.join(', ');
|
||||
return ' is ' + joinValues(values, 1);
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -44,7 +60,7 @@ export const OPERATIONS = [
|
||||
appliesTo: ['number'],
|
||||
inputCount: 1,
|
||||
getDescription: function (values) {
|
||||
return ' is not ' + values.join(', ');
|
||||
return ' is not ' + joinValues(values, 1);
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -56,7 +72,7 @@ export const OPERATIONS = [
|
||||
appliesTo: ['number'],
|
||||
inputCount: 1,
|
||||
getDescription: function (values) {
|
||||
return ' > ' + values.join(', ');
|
||||
return ' > ' + joinValues(values, 1);
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -68,7 +84,7 @@ export const OPERATIONS = [
|
||||
appliesTo: ['number'],
|
||||
inputCount: 1,
|
||||
getDescription: function (values) {
|
||||
return ' < ' + values.join(', ');
|
||||
return ' < ' + joinValues(values, 1);
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -80,7 +96,7 @@ export const OPERATIONS = [
|
||||
appliesTo: ['number'],
|
||||
inputCount: 1,
|
||||
getDescription: function (values) {
|
||||
return ' >= ' + values.join(', ');
|
||||
return ' >= ' + joinValues(values, 1);
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -92,14 +108,13 @@ export const OPERATIONS = [
|
||||
appliesTo: ['number'],
|
||||
inputCount: 1,
|
||||
getDescription: function (values) {
|
||||
return ' <= ' + values.join(', ');
|
||||
return ' <= ' + joinValues(values, 1);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'between',
|
||||
operation: function (input) {
|
||||
let numberInputs = [];
|
||||
input.forEach(inputValue => numberInputs.push(Number(inputValue)));
|
||||
let numberInputs = convertToNumbers(input);
|
||||
let larger = Math.max(...numberInputs.slice(1,3));
|
||||
let smaller = Math.min(...numberInputs.slice(1,3));
|
||||
return (numberInputs[0] > smaller) && (numberInputs[0] < larger);
|
||||
@@ -114,8 +129,7 @@ export const OPERATIONS = [
|
||||
{
|
||||
name: 'notBetween',
|
||||
operation: function (input) {
|
||||
let numberInputs = [];
|
||||
input.forEach(inputValue => numberInputs.push(Number(inputValue)));
|
||||
let numberInputs = convertToNumbers(input);
|
||||
let larger = Math.max(...numberInputs.slice(1,3));
|
||||
let smaller = Math.min(...numberInputs.slice(1,3));
|
||||
return (numberInputs[0] < smaller) || (numberInputs[0] > larger);
|
||||
@@ -136,7 +150,7 @@ export const OPERATIONS = [
|
||||
appliesTo: ['string'],
|
||||
inputCount: 1,
|
||||
getDescription: function (values) {
|
||||
return ' contains ' + values.join(', ');
|
||||
return ' contains ' + joinValues(values, 1);
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -148,7 +162,7 @@ export const OPERATIONS = [
|
||||
appliesTo: ['string'],
|
||||
inputCount: 1,
|
||||
getDescription: function (values) {
|
||||
return ' does not contain ' + values.join(', ');
|
||||
return ' does not contain ' + joinValues(values, 1);
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -160,7 +174,7 @@ export const OPERATIONS = [
|
||||
appliesTo: ['string'],
|
||||
inputCount: 1,
|
||||
getDescription: function (values) {
|
||||
return ' starts with ' + values.join(', ');
|
||||
return ' starts with ' + joinValues(values, 1);
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -172,7 +186,7 @@ export const OPERATIONS = [
|
||||
appliesTo: ['string'],
|
||||
inputCount: 1,
|
||||
getDescription: function (values) {
|
||||
return ' ends with ' + values.join(', ');
|
||||
return ' ends with ' + joinValues(values, 1);
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -184,7 +198,7 @@ export const OPERATIONS = [
|
||||
appliesTo: ['string'],
|
||||
inputCount: 1,
|
||||
getDescription: function (values) {
|
||||
return ' is exactly ' + values.join(', ');
|
||||
return ' is exactly ' + joinValues(values, 1);
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -214,33 +228,36 @@ export const OPERATIONS = [
|
||||
{
|
||||
name: 'enumValueIs',
|
||||
operation: function (input) {
|
||||
return input[0] === input[1];
|
||||
let stringInputs = convertToStrings(input);
|
||||
return stringInputs[0] === stringInputs[1];
|
||||
},
|
||||
text: 'is',
|
||||
appliesTo: ['enum'],
|
||||
inputCount: 1,
|
||||
getDescription: function (values) {
|
||||
return ' is ' + values.join(', ');
|
||||
return ' is ' + joinValues(values, 1);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'enumValueIsNot',
|
||||
operation: function (input) {
|
||||
return input[0] !== input[1];
|
||||
let stringInputs = convertToStrings(input);
|
||||
return stringInputs[0] !== stringInputs[1];
|
||||
},
|
||||
text: 'is not',
|
||||
appliesTo: ['enum'],
|
||||
inputCount: 1,
|
||||
getDescription: function (values) {
|
||||
return ' is not ' + values.join(', ');
|
||||
return ' is not ' + joinValues(values, 1);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'valueIs',
|
||||
operation: function (input) {
|
||||
const lhsValue = input[0] !== undefined ? input[0].toString() : '';
|
||||
if (input[1]) {
|
||||
const values = input[1].split(',');
|
||||
return values.find((value) => input[0].toString() === _.trim(value.toString()));
|
||||
return values.find((value) => lhsValue === _.trim(value.toString()));
|
||||
}
|
||||
return false;
|
||||
},
|
||||
@@ -254,9 +271,10 @@ export const OPERATIONS = [
|
||||
{
|
||||
name: 'valueIsNot',
|
||||
operation: function (input) {
|
||||
const lhsValue = input[0] !== undefined ? input[0].toString() : '';
|
||||
if (input[1]) {
|
||||
const values = input[1].split(',');
|
||||
const found = values.find((value) => input[0].toString() === _.trim(value.toString()));
|
||||
const found = values.find((value) => lhsValue === _.trim(value.toString()));
|
||||
return !found;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -25,8 +25,10 @@ let isOneOfOperation = OPERATIONS.find((operation) => operation.name === 'valueI
|
||||
let isNotOneOfOperation = OPERATIONS.find((operation) => operation.name === 'valueIsNot');
|
||||
let isBetween = OPERATIONS.find((operation) => operation.name === 'between');
|
||||
let isNotBetween = OPERATIONS.find((operation) => operation.name === 'notBetween');
|
||||
let enumIsOperation = OPERATIONS.find((operation) => operation.name === 'enumValueIs');
|
||||
let enumIsNotOperation = OPERATIONS.find((operation) => operation.name === 'enumValueIsNot');
|
||||
|
||||
describe('Is one of and is not one of operations', function () {
|
||||
describe('operations', function () {
|
||||
|
||||
it('should evaluate isOneOf to true for number inputs', () => {
|
||||
const inputs = [45, "5,6,45,8"];
|
||||
@@ -87,4 +89,54 @@ describe('Is one of and is not one of operations', function () {
|
||||
const inputs = ["45", "30", "50"];
|
||||
expect(!!isNotBetween.operation(inputs)).toBeFalse();
|
||||
});
|
||||
|
||||
it('should evaluate enumValueIs to true for number inputs', () => {
|
||||
const inputs = [1, "1"];
|
||||
expect(!!enumIsOperation.operation(inputs)).toBeTrue();
|
||||
});
|
||||
|
||||
it('should evaluate enumValueIs to true for string inputs', () => {
|
||||
const inputs = ["45", "45"];
|
||||
expect(!!enumIsOperation.operation(inputs)).toBeTrue();
|
||||
});
|
||||
|
||||
it('should evaluate enumValueIsNot to true for number inputs', () => {
|
||||
const inputs = [45, "46"];
|
||||
expect(!!enumIsNotOperation.operation(inputs)).toBeTrue();
|
||||
});
|
||||
|
||||
it('should evaluate enumValueIsNot to true for string inputs', () => {
|
||||
const inputs = ["45", "46"];
|
||||
expect(!!enumIsNotOperation.operation(inputs)).toBeTrue();
|
||||
});
|
||||
|
||||
it('should evaluate enumValueIs to false for number inputs', () => {
|
||||
const inputs = [1, "2"];
|
||||
expect(!!enumIsOperation.operation(inputs)).toBeFalse();
|
||||
});
|
||||
|
||||
it('should evaluate enumValueIs to false for string inputs', () => {
|
||||
const inputs = ["45", "46"];
|
||||
expect(!!enumIsOperation.operation(inputs)).toBeFalse();
|
||||
});
|
||||
|
||||
it('should evaluate enumValueIsNot to false for number inputs', () => {
|
||||
const inputs = [45, "45"];
|
||||
expect(!!enumIsNotOperation.operation(inputs)).toBeFalse();
|
||||
});
|
||||
|
||||
it('should evaluate enumValueIsNot to false for string inputs', () => {
|
||||
const inputs = ["45", "45"];
|
||||
expect(!!enumIsNotOperation.operation(inputs)).toBeFalse();
|
||||
});
|
||||
|
||||
it('should evaluate enumValueIs to false for undefined input', () => {
|
||||
const inputs = [undefined, "45"];
|
||||
expect(!!enumIsOperation.operation(inputs)).toBeFalse();
|
||||
});
|
||||
|
||||
it('should evaluate enumValueIsNot to true for undefined input', () => {
|
||||
const inputs = [undefined, "45"];
|
||||
expect(!!enumIsNotOperation.operation(inputs)).toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
const NONE_VALUE = 'transparent';
|
||||
const NONE_VALUE = '__no_value';
|
||||
|
||||
const styleProps = {
|
||||
backgroundColor: {
|
||||
@@ -46,7 +46,7 @@ const styleProps = {
|
||||
},
|
||||
color: {
|
||||
svgProperty: 'color',
|
||||
noneValue: 'NONE_VALUE',
|
||||
noneValue: NONE_VALUE,
|
||||
applicableForType: type => {
|
||||
return !type ? true : (type === 'text-view' ||
|
||||
type === 'telemetry-view'||
|
||||
@@ -62,19 +62,74 @@ const styleProps = {
|
||||
}
|
||||
};
|
||||
|
||||
const aggregateStyleValues = (accumulator, currentStyle) => {
|
||||
const styleKeys = Object.keys(currentStyle);
|
||||
const properties = Object.keys(styleProps);
|
||||
properties.forEach((property) => {
|
||||
if (!accumulator[property]) {
|
||||
accumulator[property] = [];
|
||||
}
|
||||
const found = styleKeys.find(key => key === property);
|
||||
if (found) {
|
||||
accumulator[property].push(currentStyle[found]);
|
||||
}
|
||||
});
|
||||
return accumulator;
|
||||
};
|
||||
|
||||
// Returns a union of styles used by multiple items.
|
||||
// Styles that are common to all items but don't have the same value are added to the mixedStyles list
|
||||
export const getConsolidatedStyleValues = (multipleItemStyles) => {
|
||||
let aggregatedStyleValues = multipleItemStyles.reduce(aggregateStyleValues, {});
|
||||
|
||||
let styleValues = {};
|
||||
let mixedStyles = [];
|
||||
const properties = Object.keys(styleProps);
|
||||
properties.forEach((property) => {
|
||||
const values = aggregatedStyleValues[property];
|
||||
if (values.length) {
|
||||
if (values.every(value => value === values[0])) {
|
||||
styleValues[property] = values[0];
|
||||
} else {
|
||||
styleValues[property] = '';
|
||||
mixedStyles.push(property);
|
||||
}
|
||||
}
|
||||
});
|
||||
return {
|
||||
styles: styleValues,
|
||||
mixedStyles
|
||||
};
|
||||
};
|
||||
|
||||
const getStaticStyleForItem = (domainObject, id) => {
|
||||
let domainObjectStyles = domainObject && domainObject.configuration && domainObject.configuration.objectStyles;
|
||||
if (domainObjectStyles) {
|
||||
if (id && domainObjectStyles[id] && domainObjectStyles[id].staticStyle) {
|
||||
return domainObjectStyles[id].staticStyle.style;
|
||||
if (id) {
|
||||
if(domainObjectStyles[id] && domainObjectStyles[id].staticStyle) {
|
||||
return domainObjectStyles[id].staticStyle.style;
|
||||
}
|
||||
} else if (domainObjectStyles.staticStyle) {
|
||||
return domainObjectStyles.staticStyle.style;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const getConditionalStyleForItem = (domainObject, id) => {
|
||||
let domainObjectStyles = domainObject && domainObject.configuration && domainObject.configuration.objectStyles;
|
||||
if (domainObjectStyles) {
|
||||
if (id) {
|
||||
if (domainObjectStyles[id] && domainObjectStyles[id].conditionSetIdentifier) {
|
||||
return domainObjectStyles[id].styles;
|
||||
}
|
||||
} else if (domainObjectStyles.staticStyle) {
|
||||
return domainObjectStyles.styles;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//Returns either existing static styles or uses SVG defaults if available
|
||||
export const getInitialStyleForItem = (domainObject, item) => {
|
||||
export const getApplicableStylesForItem = (domainObject, item) => {
|
||||
const type = item && item.type;
|
||||
const id = item && item.id;
|
||||
let style = {};
|
||||
@@ -97,3 +152,21 @@ export const getInitialStyleForItem = (domainObject, item) => {
|
||||
|
||||
return style;
|
||||
};
|
||||
|
||||
export const getStylesWithoutNoneValue = (style) => {
|
||||
if (_.isEmpty(style) || !style) {
|
||||
return;
|
||||
}
|
||||
let styleObj = {};
|
||||
const keys = Object.keys(style);
|
||||
keys.forEach(key => {
|
||||
if ((typeof style[key] === 'string')) {
|
||||
if (style[key].indexOf('__no_value') > -1) {
|
||||
style[key] = '';
|
||||
} else {
|
||||
styleObj[key] = style[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
return styleObj;
|
||||
};
|
||||
|
||||
@@ -20,15 +20,33 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
export const getLatestTimestamp = (current, compare, timeSystems) => {
|
||||
const timestamp = Object.assign({}, current);
|
||||
export const getLatestTimestamp = (
|
||||
currentTimestamp,
|
||||
compareTimestamp,
|
||||
timeSystems,
|
||||
currentTimeSystem
|
||||
) => {
|
||||
let latest = { ...currentTimestamp };
|
||||
const compare = { ...compareTimestamp };
|
||||
const key = currentTimeSystem.key;
|
||||
|
||||
if (!latest || !latest[key]) {
|
||||
latest = updateLatestTimeStamp(compare, timeSystems)
|
||||
}
|
||||
|
||||
if (compare[key] > latest[key]) {
|
||||
latest = updateLatestTimeStamp(compare, timeSystems)
|
||||
}
|
||||
|
||||
return latest;
|
||||
}
|
||||
|
||||
function updateLatestTimeStamp(timestamp, timeSystems) {
|
||||
let latest = {};
|
||||
|
||||
timeSystems.forEach(timeSystem => {
|
||||
if (!timestamp[timeSystem.key]
|
||||
|| compare[timeSystem.key] > timestamp[timeSystem.key]
|
||||
) {
|
||||
timestamp[timeSystem.key] = compare[timeSystem.key];
|
||||
}
|
||||
latest[timeSystem.key] = timestamp[timeSystem.key];
|
||||
});
|
||||
return timestamp;
|
||||
|
||||
return latest;
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ export default {
|
||||
makeDefinition() {
|
||||
return {
|
||||
fill: '#717171',
|
||||
stroke: 'transparent',
|
||||
stroke: '',
|
||||
x: 1,
|
||||
y: 1,
|
||||
width: 10,
|
||||
@@ -74,13 +74,14 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
style() {
|
||||
return Object.assign({
|
||||
backgroundColor: this.item.fill,
|
||||
border: '1px solid ' + this.item.stroke
|
||||
}, this.itemStyle);
|
||||
},
|
||||
styleClass() {
|
||||
return this.itemStyle && this.itemStyle.isStyleInvisible;
|
||||
if (this.itemStyle) {
|
||||
return this.itemStyle;
|
||||
} else {
|
||||
return {
|
||||
backgroundColor: this.item.fill,
|
||||
border: this.item.stroke ? '1px solid ' + this.item.stroke : ''
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
||||
@@ -74,13 +74,18 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
style() {
|
||||
let backgroundImage = 'url(' + this.item.url + ')';
|
||||
let border = '1px solid ' + this.item.stroke;
|
||||
if (this.itemStyle) {
|
||||
if (this.itemStyle.imageUrl !== undefined) {
|
||||
backgroundImage = 'url(' + this.itemStyle.imageUrl + ')';
|
||||
}
|
||||
border = this.itemStyle.border;
|
||||
}
|
||||
return {
|
||||
backgroundImage: this.itemStyle ? ('url(' + this.itemStyle.imageUrl + ')') : 'url(' + this.item.url + ')',
|
||||
border: (this.itemStyle && this.itemStyle.border) ? this.itemStyle.border : ('1px solid ' + this.item.stroke)
|
||||
backgroundImage,
|
||||
border
|
||||
};
|
||||
},
|
||||
styleClass() {
|
||||
return this.itemStyle && this.itemStyle.isStyleInvisible;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
||||
@@ -127,8 +127,11 @@ export default {
|
||||
return {x, y, x2, y2};
|
||||
},
|
||||
stroke() {
|
||||
if (this.itemStyle && this.itemStyle.border) {
|
||||
return this.itemStyle.border.replace('1px solid ', '');
|
||||
if (this.itemStyle) {
|
||||
if (this.itemStyle.border) {
|
||||
return this.itemStyle.border.replace('1px solid ', '');
|
||||
}
|
||||
return '';
|
||||
} else {
|
||||
return this.item.stroke;
|
||||
}
|
||||
@@ -146,9 +149,6 @@ export default {
|
||||
height: `${height}px`
|
||||
};
|
||||
},
|
||||
styleClass() {
|
||||
return this.itemStyle && this.itemStyle.isStyleInvisible;
|
||||
},
|
||||
startHandleClass() {
|
||||
return START_HANDLE_QUADRANTS[this.vectorQuadrant];
|
||||
},
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
v-if="domainObject"
|
||||
class="c-telemetry-view"
|
||||
:class="styleClass"
|
||||
:style="telemetryObjectStyle || styleObject"
|
||||
:style="styleObject"
|
||||
@contextmenu.prevent="showContextMenu"
|
||||
>
|
||||
<div
|
||||
@@ -79,8 +79,8 @@ export default {
|
||||
height: DEFAULT_TELEMETRY_DIMENSIONS[1],
|
||||
displayMode: 'all',
|
||||
value: metadata.getDefaultDisplayValue(),
|
||||
stroke: "transparent",
|
||||
fill: "transparent",
|
||||
stroke: "",
|
||||
fill: "",
|
||||
color: "",
|
||||
size: "13px"
|
||||
};
|
||||
@@ -125,27 +125,10 @@ export default {
|
||||
return displayMode === 'all' || displayMode === 'value';
|
||||
},
|
||||
styleObject() {
|
||||
return {
|
||||
backgroundColor: this.item.fill,
|
||||
borderColor: this.item.stroke,
|
||||
color: this.item.color,
|
||||
return Object.assign({}, {
|
||||
fontSize: this.item.size
|
||||
}
|
||||
},
|
||||
styleClass() {
|
||||
return this.telemetryObjectStyle && this.telemetryObjectStyle.isStyleInvisible;
|
||||
},
|
||||
telemetryObjectStyle() {
|
||||
let styleObj = Object.assign({}, this.itemStyle);
|
||||
let keys = Object.keys(styleObj);
|
||||
keys.forEach(key => {
|
||||
if ((typeof styleObj[key] === 'string') && (styleObj[key].indexOf('transparent') > -1)) {
|
||||
if (styleObj[key]) {
|
||||
styleObj[key] = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
return styleObj;
|
||||
}, this.itemStyle);
|
||||
|
||||
},
|
||||
fieldName() {
|
||||
return this.valueMetadata && this.valueMetadata.name;
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
:class="[styleClass]"
|
||||
:style="style"
|
||||
>
|
||||
{{ item.text }}
|
||||
<div class="c-text-view__text">{{ item.text }}</div>
|
||||
</div>
|
||||
</layout-frame>
|
||||
</template>
|
||||
@@ -44,8 +44,8 @@ import conditionalStylesMixin from "../mixins/objectStyles-mixin";
|
||||
export default {
|
||||
makeDefinition(openmct, gridSize, element) {
|
||||
return {
|
||||
fill: 'transparent',
|
||||
stroke: 'transparent',
|
||||
fill: '',
|
||||
stroke: '',
|
||||
size: '13px',
|
||||
color: '',
|
||||
x: 1,
|
||||
@@ -80,14 +80,8 @@ export default {
|
||||
computed: {
|
||||
style() {
|
||||
return Object.assign({
|
||||
backgroundColor: this.item.fill,
|
||||
border: '1px solid ' + this.item.stroke,
|
||||
color: this.item.color,
|
||||
fontSize: this.item.size
|
||||
}, this.itemStyle);
|
||||
},
|
||||
styleClass() {
|
||||
return this.itemStyle && this.itemStyle.isStyleInvisible;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
.is-editing {
|
||||
/******************* STYLES FOR C-FRAME WHILE EDITING */
|
||||
.c-frame {
|
||||
border: 1px solid rgba($editFrameColorHov, 0.3);
|
||||
|
||||
&:not([s-selected]) {
|
||||
&:hover {
|
||||
border: $editFrameBorderHov;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
.c-text-view {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
align-items: center; // Vertically center text
|
||||
overflow: hidden;
|
||||
padding: $interiorMargin;
|
||||
|
||||
.c-frame & {
|
||||
@include abs();
|
||||
|
||||
@@ -21,12 +21,14 @@
|
||||
*****************************************************************************/
|
||||
|
||||
import StyleRuleManager from "@/plugins/condition/StyleRuleManager";
|
||||
import {getStylesWithoutNoneValue} from "@/plugins/condition/utils/styleUtils";
|
||||
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
data() {
|
||||
return {
|
||||
itemStyle: this.itemStyle
|
||||
itemStyle: undefined,
|
||||
styleClass: ''
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@@ -50,7 +52,7 @@ export default {
|
||||
},
|
||||
initObjectStyles() {
|
||||
if (!this.styleRuleManager) {
|
||||
this.styleRuleManager = new StyleRuleManager(this.objectStyle, this.openmct, this.updateStyle.bind(this));
|
||||
this.styleRuleManager = new StyleRuleManager(this.objectStyle, this.openmct, this.updateStyle.bind(this), true);
|
||||
} else {
|
||||
this.styleRuleManager.updateObjectStyleConfig(this.objectStyle);
|
||||
}
|
||||
@@ -69,13 +71,8 @@ export default {
|
||||
});
|
||||
},
|
||||
updateStyle(style) {
|
||||
this.itemStyle = style;
|
||||
let keys = Object.keys(this.itemStyle);
|
||||
keys.forEach((key) => {
|
||||
if ((typeof this.itemStyle[key] === 'string') && (this.itemStyle[key].indexOf('transparent') > -1)) {
|
||||
delete this.itemStyle[key];
|
||||
}
|
||||
});
|
||||
this.itemStyle = getStylesWithoutNoneValue(style);
|
||||
this.styleClass = this.itemStyle && this.itemStyle.isStyleInvisible;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
21
src/plugins/notebook/components/menu-items.vue
Normal file
21
src/plugins/notebook/components/menu-items.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<div class="c-menu">
|
||||
<ul>
|
||||
<li
|
||||
v-for="(item, index) in popupMenuItems"
|
||||
:key="index"
|
||||
:class="item.cssClass"
|
||||
:title="item.name"
|
||||
@click="item.callback"
|
||||
>
|
||||
{{ item.name }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
inject: ['popupMenuItems']
|
||||
}
|
||||
</script>
|
||||
@@ -12,24 +12,7 @@
|
||||
:class="embed.cssClass"
|
||||
@click="changeLocation"
|
||||
>{{ embed.name }}</a>
|
||||
<a class="c-ne__embed__context-available icon-arrow-down"
|
||||
@click="toggleActionMenu"
|
||||
></a>
|
||||
</div>
|
||||
<div class="hide-menu hidden">
|
||||
<div class="menu-element context-menu-wrapper mobile-disable-select">
|
||||
<div class="c-menu">
|
||||
<ul>
|
||||
<li v-for="action in actions"
|
||||
:key="action.name"
|
||||
:class="action.cssClass"
|
||||
@click="action.perform(embed)"
|
||||
>
|
||||
{{ action.name }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<PopupMenu :popup-menu-items="popupMenuItems" />
|
||||
</div>
|
||||
<div v-if="embed.snapshot"
|
||||
class="c-ne__embed__time"
|
||||
@@ -42,15 +25,17 @@
|
||||
|
||||
<script>
|
||||
import Moment from 'moment';
|
||||
import PopupMenu from './popup-menu.vue';
|
||||
import PreviewAction from '../../../ui/preview/PreviewAction';
|
||||
import Painterro from 'painterro';
|
||||
import RemoveDialog from '../utils/removeDialog';
|
||||
import SnapshotTemplate from './snapshot-template.html';
|
||||
import { togglePopupMenu } from '../utils/popup-menu';
|
||||
import Vue from 'vue';
|
||||
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
components: {
|
||||
PopupMenu
|
||||
},
|
||||
props: {
|
||||
embed: {
|
||||
@@ -62,23 +47,35 @@ export default {
|
||||
removeActionString: {
|
||||
type: String,
|
||||
default() {
|
||||
return 'Remove Embed';
|
||||
return 'Remove This Embed';
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
actions: [this.removeEmbedAction()],
|
||||
agentService: this.openmct.$injector.get('agentService'),
|
||||
popupService: this.openmct.$injector.get('popupService')
|
||||
popupMenuItems: []
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
},
|
||||
beforeMount() {
|
||||
this.populateActionMenu();
|
||||
mounted() {
|
||||
this.addPopupMenuItems();
|
||||
},
|
||||
methods: {
|
||||
addPopupMenuItems() {
|
||||
const removeEmbed = {
|
||||
cssClass: 'icon-trash',
|
||||
name: this.removeActionString,
|
||||
callback: this.getRemoveDialog.bind(this)
|
||||
}
|
||||
const preview = {
|
||||
cssClass: 'icon-eye-open',
|
||||
name: 'Preview',
|
||||
callback: this.previewEmbed.bind(this)
|
||||
}
|
||||
|
||||
this.popupMenuItems = [removeEmbed, preview];
|
||||
},
|
||||
annotateSnapshot() {
|
||||
const self = this;
|
||||
|
||||
@@ -165,24 +162,45 @@ export default {
|
||||
}).show(this.embed.snapshot.src);
|
||||
},
|
||||
changeLocation() {
|
||||
this.openmct.time.stopClock();
|
||||
this.openmct.time.bounds({
|
||||
start: this.embed.bounds.start,
|
||||
end: this.embed.bounds.end
|
||||
});
|
||||
|
||||
const link = this.embed.historicLink;
|
||||
if (!link) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bounds = this.openmct.time.bounds();
|
||||
const isTimeBoundChanged = this.embed.bounds.start !== bounds.start
|
||||
&& this.embed.bounds.end !== bounds.end;
|
||||
const isFixedTimespanMode = !this.openmct.time.clock();
|
||||
|
||||
this.openmct.time.stopClock();
|
||||
window.location.href = link;
|
||||
const message = 'Time bounds changed to fixed timespan mode';
|
||||
|
||||
let message = '';
|
||||
if (isTimeBoundChanged) {
|
||||
this.openmct.time.bounds({
|
||||
start: this.embed.bounds.start,
|
||||
end: this.embed.bounds.end
|
||||
});
|
||||
message = 'Time bound values changed';
|
||||
}
|
||||
|
||||
if (!isFixedTimespanMode) {
|
||||
message = 'Time bound values changed to fixed timespan mode';
|
||||
}
|
||||
|
||||
this.openmct.notifications.alert(message);
|
||||
},
|
||||
formatTime(unixTime, timeFormat) {
|
||||
return Moment.utc(unixTime).format(timeFormat);
|
||||
},
|
||||
getRemoveDialog() {
|
||||
const options = {
|
||||
name: this.removeActionString,
|
||||
callback: this.removeEmbed.bind(this)
|
||||
}
|
||||
const removeDialog = new RemoveDialog(this.openmct, options);
|
||||
removeDialog.show();
|
||||
},
|
||||
openSnapshot() {
|
||||
const self = this;
|
||||
const snapshot = new Vue({
|
||||
@@ -214,53 +232,17 @@ export default {
|
||||
]
|
||||
});
|
||||
},
|
||||
populateActionMenu() {
|
||||
previewEmbed() {
|
||||
const self = this;
|
||||
const actions = [new PreviewAction(self.openmct)];
|
||||
const previewAction = new PreviewAction(self.openmct);
|
||||
previewAction.invoke(JSON.parse(self.embed.objectPath));
|
||||
},
|
||||
removeEmbed(success) {
|
||||
if (!success) {
|
||||
return;
|
||||
}
|
||||
|
||||
actions.forEach((action) => {
|
||||
self.actions.push({
|
||||
cssClass: action.cssClass,
|
||||
name: action.name,
|
||||
perform: () => {
|
||||
action.invoke(JSON.parse(self.embed.objectPath));
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
removeEmbed(id) {
|
||||
this.$emit('removeEmbed', id);
|
||||
},
|
||||
removeEmbedAction() {
|
||||
const self = this;
|
||||
|
||||
return {
|
||||
name: self.removeActionString,
|
||||
cssClass: 'icon-trash',
|
||||
perform: function (embed) {
|
||||
const dialog = self.openmct.overlays.dialog({
|
||||
iconClass: "error",
|
||||
message: `This action will permanently ${self.removeActionString.toLowerCase()}. Do you wish to continue?`,
|
||||
buttons: [{
|
||||
label: "No",
|
||||
callback: function () {
|
||||
dialog.dismiss();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Yes",
|
||||
emphasis: true,
|
||||
callback: function () {
|
||||
dialog.dismiss();
|
||||
self.removeEmbed(embed.id);
|
||||
}
|
||||
}]
|
||||
});
|
||||
}
|
||||
};
|
||||
},
|
||||
toggleActionMenu(event) {
|
||||
togglePopupMenu(event, this.openmct);
|
||||
this.$emit('removeEmbed', this.embed.id);
|
||||
},
|
||||
updateEmbed(embed) {
|
||||
this.$emit('updateEmbed', embed);
|
||||
|
||||
@@ -221,7 +221,7 @@ export default {
|
||||
return position;
|
||||
},
|
||||
formatTime(unixTime, timeFormat) {
|
||||
return Moment(unixTime).format(timeFormat);
|
||||
return Moment.utc(unixTime).format(timeFormat);
|
||||
},
|
||||
moveSnapshot(snapshotId) {
|
||||
const snapshot = this.snapshotContainer.getSnapshot(snapshotId);
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
<script>
|
||||
import Snapshot from '../snapshot';
|
||||
import { clearDefaultNotebook, getDefaultNotebook } from '../utils/notebook-storage';
|
||||
import { getDefaultNotebook } from '../utils/notebook-storage';
|
||||
import { NOTEBOOK_DEFAULT, NOTEBOOK_SNAPSHOT } from '../notebook-constants';
|
||||
|
||||
export default {
|
||||
@@ -40,6 +40,18 @@ export default {
|
||||
default() {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
ignoreLink: {
|
||||
type: Boolean,
|
||||
default() {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
objectPath: {
|
||||
type: Array,
|
||||
default() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -60,28 +72,22 @@ export default {
|
||||
methods: {
|
||||
async setNotebookTypes() {
|
||||
const notebookTypes = [];
|
||||
let defaultPath = '';
|
||||
const defaultNotebook = getDefaultNotebook();
|
||||
|
||||
if (defaultNotebook) {
|
||||
const domainObject = await this.openmct.objects.get(defaultNotebook.notebookMeta.identifier)
|
||||
.then(d => d);
|
||||
const domainObject = defaultNotebook.domainObject;
|
||||
|
||||
if (!domainObject.location) {
|
||||
clearDefaultNotebook();
|
||||
} else {
|
||||
defaultPath = `${domainObject.name} - ${defaultNotebook.section.name} - ${defaultNotebook.page.name}`;
|
||||
if (domainObject.location) {
|
||||
const defaultPath = `${domainObject.name} - ${defaultNotebook.section.name} - ${defaultNotebook.page.name}`;
|
||||
|
||||
notebookTypes.push({
|
||||
cssClass: 'icon-notebook',
|
||||
name: `Save to Notebook ${defaultPath}`,
|
||||
type: NOTEBOOK_DEFAULT
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (defaultPath.length !== 0) {
|
||||
notebookTypes.push({
|
||||
cssClass: 'icon-notebook',
|
||||
name: `Save to Notebook ${defaultPath}`,
|
||||
type: NOTEBOOK_DEFAULT
|
||||
});
|
||||
}
|
||||
|
||||
notebookTypes.push({
|
||||
cssClass: 'icon-notebook',
|
||||
name: 'Save to Notebook Snapshots',
|
||||
@@ -97,17 +103,27 @@ export default {
|
||||
this.showMenu = false;
|
||||
},
|
||||
snapshot(notebook) {
|
||||
let element = document.getElementsByClassName("l-shell__main-container")[0];
|
||||
const bounds = this.openmct.time.bounds();
|
||||
const objectPath = this.openmct.router.path;
|
||||
const snapshotMeta = {
|
||||
bounds,
|
||||
link: window.location.href,
|
||||
objectPath,
|
||||
openmct: this.openmct
|
||||
};
|
||||
this.hideMenu();
|
||||
|
||||
this.notebookSnapshot.capture(snapshotMeta, notebook.type, element);
|
||||
this.$nextTick(() => {
|
||||
const element = document.querySelector('.c-overlay__contents')
|
||||
|| document.getElementsByClassName('l-shell__main-container')[0];
|
||||
|
||||
const bounds = this.openmct.time.bounds();
|
||||
const link = !this.ignoreLink
|
||||
? window.location.href
|
||||
: null;
|
||||
|
||||
const objectPath = this.objectPath || this.openmct.router.path;
|
||||
const snapshotMeta = {
|
||||
bounds,
|
||||
link,
|
||||
objectPath,
|
||||
openmct: this.openmct
|
||||
};
|
||||
|
||||
this.notebookSnapshot.capture(snapshotMeta, notebook.type, element);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,24 +10,9 @@
|
||||
> {{ snapshots.length }} of {{ getNotebookSnapshotMaxCount() }}
|
||||
</span>
|
||||
</div>
|
||||
<a class="l-browse-bar__context-actions c-disclosure-button"
|
||||
@click="toggleActionMenu"
|
||||
></a>
|
||||
<div class="hide-menu hidden">
|
||||
<div class="menu-element context-menu-wrapper mobile-disable-select">
|
||||
<div class="c-menu">
|
||||
<ul>
|
||||
<li v-for="action in actions"
|
||||
:key="action.name"
|
||||
:class="action.cssClass"
|
||||
@click="action.perform()"
|
||||
>
|
||||
{{ action.name }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PopupMenu v-if="snapshots.length > 0"
|
||||
:popup-menu-items="popupMenuItems"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -62,14 +47,16 @@
|
||||
|
||||
<script>
|
||||
import NotebookEmbed from './notebook-embed.vue';
|
||||
import PopupMenu from './popup-menu.vue';
|
||||
import RemoveDialog from '../utils/removeDialog';
|
||||
import { NOTEBOOK_SNAPSHOT_MAX_COUNT } from '../snapshot-container';
|
||||
import { EVENT_SNAPSHOTS_UPDATED } from '../notebook-constants';
|
||||
import { togglePopupMenu } from '../utils/popup-menu';
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'snapshotContainer'],
|
||||
components: {
|
||||
NotebookEmbed
|
||||
NotebookEmbed,
|
||||
PopupMenu
|
||||
},
|
||||
props: {
|
||||
toggleSnapshot: {
|
||||
@@ -81,54 +68,47 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
actions: [this.removeAllSnapshotAction()],
|
||||
popupMenuItems: [],
|
||||
removeActionString: 'Delete all snapshots',
|
||||
snapshots: []
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.addPopupMenuItems();
|
||||
this.snapshotContainer.on(EVENT_SNAPSHOTS_UPDATED, this.snapshotsUpdated);
|
||||
this.snapshots = this.snapshotContainer.getSnapshots();
|
||||
},
|
||||
beforeDestory() {
|
||||
},
|
||||
methods: {
|
||||
addPopupMenuItems() {
|
||||
const removeSnapshot = {
|
||||
cssClass: 'icon-trash',
|
||||
name: this.removeActionString,
|
||||
callback: this.getRemoveDialog.bind(this)
|
||||
}
|
||||
|
||||
this.popupMenuItems = [removeSnapshot];
|
||||
},
|
||||
close() {
|
||||
this.toggleSnapshot();
|
||||
},
|
||||
getNotebookSnapshotMaxCount() {
|
||||
return NOTEBOOK_SNAPSHOT_MAX_COUNT;
|
||||
},
|
||||
removeAllSnapshotAction() {
|
||||
const self = this;
|
||||
|
||||
return {
|
||||
name: 'Delete All Snapshots',
|
||||
cssClass: 'icon-trash',
|
||||
perform: function (embed) {
|
||||
const dialog = self.openmct.overlays.dialog({
|
||||
iconClass: "error",
|
||||
message: 'This action will delete all notebook snapshots. Do you want to continue?',
|
||||
buttons: [
|
||||
{
|
||||
label: "No",
|
||||
callback: () => {
|
||||
dialog.dismiss();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Yes",
|
||||
emphasis: true,
|
||||
callback: () => {
|
||||
self.removeAllSnapshots();
|
||||
dialog.dismiss();
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
};
|
||||
getRemoveDialog() {
|
||||
const options = {
|
||||
name: this.removeActionString,
|
||||
callback: this.removeAllSnapshots.bind(this)
|
||||
}
|
||||
const removeDialog = new RemoveDialog(this.openmct, options);
|
||||
removeDialog.show();
|
||||
},
|
||||
removeAllSnapshots() {
|
||||
removeAllSnapshots(success) {
|
||||
if (!success) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.snapshotContainer.removeAllSnapshots();
|
||||
},
|
||||
removeSnapshot(id) {
|
||||
@@ -141,9 +121,6 @@ export default {
|
||||
event.dataTransfer.setData('text/plain', snapshot.id);
|
||||
event.dataTransfer.setData('snapshot/id', snapshot.id);
|
||||
},
|
||||
toggleActionMenu(event) {
|
||||
togglePopupMenu(event, this.openmct);
|
||||
},
|
||||
updateSnapshot(snapshot) {
|
||||
this.snapshotContainer.updateSnapshot(snapshot);
|
||||
}
|
||||
|
||||
@@ -49,13 +49,19 @@
|
||||
class="c-notebook__controls__time"
|
||||
>
|
||||
<option value="0"
|
||||
selected="selected"
|
||||
:selected="showTime === 0"
|
||||
>
|
||||
Show all
|
||||
</option>
|
||||
<option value="1">Last hour</option>
|
||||
<option value="8">Last 8 hours</option>
|
||||
<option value="24">Last 24 hours</option>
|
||||
<option value="1"
|
||||
:selected="showTime === 1"
|
||||
>Last hour</option>
|
||||
<option value="8"
|
||||
:selected="showTime === 8"
|
||||
>Last 8 hours</option>
|
||||
<option value="24"
|
||||
:selected="showTime === 24"
|
||||
>Last 24 hours</option>
|
||||
</select>
|
||||
<select v-model="defaultSort"
|
||||
class="c-notebook__controls__time"
|
||||
@@ -132,9 +138,17 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
filteredAndSortedEntries() {
|
||||
const filterTime = Date.now();
|
||||
const pageEntries = getNotebookEntries(this.internalDomainObject, this.selectedSection, this.selectedPage) || [];
|
||||
|
||||
return pageEntries.sort(this.sortEntries);
|
||||
const hours = parseInt(this.showTime, 10);
|
||||
const filteredPageEntriesByTime = hours
|
||||
? pageEntries.filter(entry => (filterTime - entry.createdOn) <= hours * 60 * 60 * 1000)
|
||||
: pageEntries;
|
||||
|
||||
return this.defaultSort === 'oldest'
|
||||
? filteredPageEntriesByTime
|
||||
: [...filteredPageEntriesByTime].reverse();
|
||||
},
|
||||
pages() {
|
||||
return this.getPages() || [];
|
||||
@@ -225,6 +239,7 @@ export default {
|
||||
const section = this.getSelectedSection();
|
||||
|
||||
return {
|
||||
domainObject: this.internalDomainObject,
|
||||
notebookMeta,
|
||||
section,
|
||||
page
|
||||
@@ -420,18 +435,13 @@ export default {
|
||||
searchItem(input) {
|
||||
this.search = input;
|
||||
},
|
||||
sortEntries(right, left) {
|
||||
return this.defaultSort === 'newest'
|
||||
? left.createdOn - right.createdOn
|
||||
: right.createdOn - left.createdOn;
|
||||
},
|
||||
toggleNav() {
|
||||
this.showNav = !this.showNav;
|
||||
},
|
||||
async updateDefaultNotebook(notebookStorage) {
|
||||
const defaultNotebookObject = await this.getDefaultNotebookObject();
|
||||
this.removeDefaultClass(defaultNotebookObject);
|
||||
setDefaultNotebook(notebookStorage);
|
||||
setDefaultNotebook(this.openmct, notebookStorage);
|
||||
this.addDefaultClass();
|
||||
this.defaultSectionId = notebookStorage.section.id;
|
||||
this.defaultPageId = notebookStorage.page.id;
|
||||
@@ -486,7 +496,7 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
if (section.id !== defaultNotebookSection.id) {
|
||||
if (id !== defaultNotebookSection.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,32 +9,19 @@
|
||||
@keydown.enter="updateName"
|
||||
@blur="updateName"
|
||||
>{{ page.name.length ? page.name : `Unnamed ${pageTitle}` }}</span>
|
||||
<a class="c-list__item__menu-indicator icon-arrow-down"
|
||||
@click="toggleActionMenu"
|
||||
></a>
|
||||
<div class="hide-menu hidden">
|
||||
<div class="menu-element context-menu-wrapper mobile-disable-select">
|
||||
<div class="c-menu">
|
||||
<ul>
|
||||
<li v-for="action in actions"
|
||||
:key="action.name"
|
||||
:class="action.cssClass"
|
||||
@click="action.perform(page.id)"
|
||||
>
|
||||
{{ action.name }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PopupMenu :popup-menu-items="popupMenuItems" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { togglePopupMenu } from '../utils/popup-menu';
|
||||
import PopupMenu from './popup-menu.vue';
|
||||
import RemoveDialog from '../utils/removeDialog';
|
||||
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
components: {
|
||||
PopupMenu
|
||||
},
|
||||
props: {
|
||||
defaultPageId: {
|
||||
type: String,
|
||||
@@ -55,7 +42,8 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
actions: [this.deletePage()]
|
||||
popupMenuItems: [],
|
||||
removeActionString: `Delete ${this.pageTitle}`
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -64,40 +52,37 @@ export default {
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.addPopupMenuItems();
|
||||
this.toggleContentEditable();
|
||||
},
|
||||
destroyed() {
|
||||
},
|
||||
methods: {
|
||||
deletePage() {
|
||||
const self = this;
|
||||
|
||||
return {
|
||||
name: `Delete ${this.pageTitle}`,
|
||||
addPopupMenuItems() {
|
||||
const removePage = {
|
||||
cssClass: 'icon-trash',
|
||||
perform: function (id) {
|
||||
const dialog = self.openmct.overlays.dialog({
|
||||
iconClass: "error",
|
||||
message: 'This action will delete this page and all of its entries. Do you want to continue?',
|
||||
buttons: [
|
||||
{
|
||||
label: "No",
|
||||
callback: () => {
|
||||
dialog.dismiss();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Yes",
|
||||
emphasis: true,
|
||||
callback: () => {
|
||||
self.$emit('deletePage', id);
|
||||
dialog.dismiss();
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
};
|
||||
name: this.removeActionString,
|
||||
callback: this.getRemoveDialog.bind(this)
|
||||
}
|
||||
|
||||
this.popupMenuItems = [removePage];
|
||||
},
|
||||
deletePage(success) {
|
||||
if (!success) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit('deletePage', this.page.id);
|
||||
},
|
||||
getRemoveDialog() {
|
||||
const message = 'This action will delete this page and all of its entries. Do you want to continue?';
|
||||
const options = {
|
||||
name: this.removeActionString,
|
||||
callback: this.deletePage.bind(this),
|
||||
message
|
||||
}
|
||||
const removeDialog = new RemoveDialog(this.openmct, options);
|
||||
removeDialog.show();
|
||||
},
|
||||
selectPage(event) {
|
||||
const target = event.target;
|
||||
@@ -117,10 +102,6 @@ export default {
|
||||
|
||||
this.$emit('selectPage', id);
|
||||
},
|
||||
toggleActionMenu(event) {
|
||||
event.preventDefault();
|
||||
togglePopupMenu(event, this.openmct);
|
||||
},
|
||||
toggleContentEditable(page = this.page) {
|
||||
const pageTitle = this.$el.querySelector('span');
|
||||
pageTitle.contentEditable = page.isSelected;
|
||||
|
||||
93
src/plugins/notebook/components/popup-menu.vue
Normal file
93
src/plugins/notebook/components/popup-menu.vue
Normal file
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<button
|
||||
class="c-popup-menu-button c-disclosure-button"
|
||||
title="popup menu"
|
||||
@click="showMenuItems"
|
||||
>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MenuItems from './menu-items.vue';
|
||||
import Vue from 'vue';
|
||||
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
props: {
|
||||
domainObject: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
popupMenuItems: {
|
||||
type: Array,
|
||||
default() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
menuItems: null
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
},
|
||||
methods: {
|
||||
calculateMenuPosition(event, element) {
|
||||
let eventPosX = event.clientX;
|
||||
let eventPosY = event.clientY;
|
||||
|
||||
let menuDimensions = element.getBoundingClientRect();
|
||||
let overflowX = (eventPosX + menuDimensions.width) - document.body.clientWidth;
|
||||
let overflowY = (eventPosY + menuDimensions.height) - document.body.clientHeight;
|
||||
|
||||
if (overflowX > 0) {
|
||||
eventPosX = eventPosX - overflowX;
|
||||
}
|
||||
|
||||
if (overflowY > 0) {
|
||||
eventPosY = eventPosY - overflowY;
|
||||
}
|
||||
|
||||
return {
|
||||
x: eventPosX,
|
||||
y: eventPosY
|
||||
}
|
||||
},
|
||||
hideMenuItems() {
|
||||
document.body.removeChild(this.menuItems.$el);
|
||||
this.menuItems.$destroy();
|
||||
this.menuItems = null;
|
||||
document.removeEventListener('click', this.hideMenuItems);
|
||||
|
||||
return;
|
||||
},
|
||||
showMenuItems($event) {
|
||||
const menuItems = new Vue({
|
||||
components: {
|
||||
MenuItems
|
||||
},
|
||||
provide: {
|
||||
popupMenuItems: this.popupMenuItems
|
||||
},
|
||||
template: '<MenuItems />'
|
||||
});
|
||||
this.menuItems = menuItems;
|
||||
|
||||
menuItems.$mount();
|
||||
const element = this.menuItems.$el;
|
||||
document.body.appendChild(element);
|
||||
|
||||
const position = this.calculateMenuPosition($event, element);
|
||||
element.style.left = `${position.x}px`;
|
||||
element.style.top = `${position.y}px`;
|
||||
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', this.hideMenuItems);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -9,24 +9,7 @@
|
||||
@keydown.enter="updateName"
|
||||
@blur="updateName"
|
||||
>{{ section.name.length ? section.name : `Unnamed ${sectionTitle}` }}</span>
|
||||
<a class="c-list__item__menu-indicator icon-arrow-down"
|
||||
@click="toggleActionMenu"
|
||||
></a>
|
||||
<div class="hide-menu hidden">
|
||||
<div class="menu-element context-menu-wrapper mobile-disable-select">
|
||||
<div class="c-menu">
|
||||
<ul>
|
||||
<li v-for="action in actions"
|
||||
:key="action.name"
|
||||
:class="action.cssClass"
|
||||
@click="action.perform(section.id)"
|
||||
>
|
||||
{{ action.name }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PopupMenu :popup-menu-items="popupMenuItems" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -34,10 +17,14 @@
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { togglePopupMenu } from '../utils/popup-menu';
|
||||
import PopupMenu from './popup-menu.vue';
|
||||
import RemoveDialog from '../utils/removeDialog';
|
||||
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
components: {
|
||||
PopupMenu
|
||||
},
|
||||
props: {
|
||||
defaultSectionId: {
|
||||
type: String,
|
||||
@@ -58,7 +45,8 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
actions: [this.deleteSectionAction()]
|
||||
popupMenuItems: [],
|
||||
removeActionString: `Delete ${this.sectionTitle}`
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -67,40 +55,38 @@ export default {
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.addPopupMenuItems();
|
||||
this.toggleContentEditable();
|
||||
},
|
||||
destroyed() {
|
||||
},
|
||||
methods: {
|
||||
deleteSectionAction() {
|
||||
const self = this;
|
||||
|
||||
return {
|
||||
name: `Delete ${this.sectionTitle}`,
|
||||
addPopupMenuItems() {
|
||||
const removeSection = {
|
||||
cssClass: 'icon-trash',
|
||||
perform: function (id) {
|
||||
const dialog = self.openmct.overlays.dialog({
|
||||
iconClass: "error",
|
||||
message: 'This action will delete this section and all of its pages and entries. Do you want to continue?',
|
||||
buttons: [
|
||||
{
|
||||
label: "No",
|
||||
callback: () => {
|
||||
dialog.dismiss();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Yes",
|
||||
emphasis: true,
|
||||
callback: () => {
|
||||
self.$emit('deleteSection', id);
|
||||
dialog.dismiss();
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
};
|
||||
name: this.removeActionString,
|
||||
callback: this.getRemoveDialog.bind(this)
|
||||
}
|
||||
|
||||
this.popupMenuItems = [removeSection];
|
||||
},
|
||||
deleteSection(success) {
|
||||
if (!success) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit('deleteSection', this.section.id);
|
||||
},
|
||||
getRemoveDialog() {
|
||||
const message = 'This action will delete this section and all of its pages and entries. Do you want to continue?';
|
||||
const options = {
|
||||
name: this.removeActionString,
|
||||
callback: this.deleteSection.bind(this),
|
||||
message
|
||||
}
|
||||
|
||||
const removeDialog = new RemoveDialog(this.openmct, options);
|
||||
removeDialog.show();
|
||||
},
|
||||
selectSection(event) {
|
||||
const target = event.target;
|
||||
@@ -121,9 +107,6 @@ export default {
|
||||
|
||||
this.$emit('selectSection', id);
|
||||
},
|
||||
toggleActionMenu(event) {
|
||||
togglePopupMenu(event, this.openmct);
|
||||
},
|
||||
toggleContentEditable(section = this.section) {
|
||||
const sectionTitle = this.$el.querySelector('span');
|
||||
sectionTitle.contentEditable = section.isSelected;
|
||||
|
||||
@@ -1,6 +1,46 @@
|
||||
const NOTEBOOK_LOCAL_STORAGE = 'notebook-storage';
|
||||
let currentNotebookObject = null;
|
||||
let unlisten = null;
|
||||
|
||||
function defaultNotebookObjectChanged(newDomainObject) {
|
||||
if (newDomainObject.location !== null) {
|
||||
currentNotebookObject = newDomainObject;
|
||||
const notebookStorage = getDefaultNotebook();
|
||||
notebookStorage.domainObject = newDomainObject;
|
||||
saveDefaultNotebook(notebookStorage);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (unlisten) {
|
||||
unlisten();
|
||||
unlisten = null;
|
||||
}
|
||||
|
||||
clearDefaultNotebook();
|
||||
}
|
||||
|
||||
function observeDefaultNotebookObject(openmct, notebookStorage) {
|
||||
const domainObject = notebookStorage.domainObject;
|
||||
if (currentNotebookObject
|
||||
&& currentNotebookObject.identifier.key === domainObject.identifier.key) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (unlisten) {
|
||||
unlisten();
|
||||
unlisten = null;
|
||||
}
|
||||
|
||||
unlisten = openmct.objects.observe(notebookStorage.domainObject, '*', defaultNotebookObjectChanged);
|
||||
}
|
||||
|
||||
function saveDefaultNotebook(notebookStorage) {
|
||||
window.localStorage.setItem(NOTEBOOK_LOCAL_STORAGE, JSON.stringify(notebookStorage));
|
||||
}
|
||||
|
||||
export function clearDefaultNotebook() {
|
||||
currentNotebookObject = null;
|
||||
window.localStorage.setItem(NOTEBOOK_LOCAL_STORAGE, null);
|
||||
}
|
||||
|
||||
@@ -10,20 +50,21 @@ export function getDefaultNotebook() {
|
||||
return JSON.parse(notebookStorage);
|
||||
}
|
||||
|
||||
export function setDefaultNotebook(notebookStorage) {
|
||||
window.localStorage.setItem(NOTEBOOK_LOCAL_STORAGE, JSON.stringify(notebookStorage));
|
||||
export function setDefaultNotebook(openmct, notebookStorage) {
|
||||
observeDefaultNotebookObject(openmct, notebookStorage);
|
||||
saveDefaultNotebook(notebookStorage);
|
||||
}
|
||||
|
||||
export function setDefaultNotebookSection(section) {
|
||||
const notebookStorage = getDefaultNotebook();
|
||||
|
||||
notebookStorage.section = section;
|
||||
window.localStorage.setItem(NOTEBOOK_LOCAL_STORAGE, JSON.stringify(notebookStorage));
|
||||
saveDefaultNotebook(notebookStorage);
|
||||
|
||||
}
|
||||
|
||||
export function setDefaultNotebookPage(page) {
|
||||
const notebookStorage = getDefaultNotebook();
|
||||
notebookStorage.page = page;
|
||||
window.localStorage.setItem(NOTEBOOK_LOCAL_STORAGE, JSON.stringify(notebookStorage));
|
||||
saveDefaultNotebook(notebookStorage);
|
||||
}
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import $ from 'zepto';
|
||||
|
||||
export const togglePopupMenu = (event, openmct) => {
|
||||
event.preventDefault();
|
||||
|
||||
const body = $(document.body);
|
||||
const container = $(event.target.parentElement.parentElement);
|
||||
const classList = document.querySelector('body').classList;
|
||||
const isPhone = Array.from(classList).includes('phone');
|
||||
const isTablet = Array.from(classList).includes('tablet');
|
||||
|
||||
const initiatingEvent = isPhone || isTablet
|
||||
? 'touchstart'
|
||||
: 'mousedown';
|
||||
const menu = container.find('.menu-element');
|
||||
let dismissExistingMenu;
|
||||
|
||||
function dismiss() {
|
||||
container.find('.hide-menu').append(menu);
|
||||
body.off(initiatingEvent, menuClickHandler);
|
||||
dismissExistingMenu = undefined;
|
||||
}
|
||||
|
||||
function menuClickHandler(e) {
|
||||
window.setTimeout(() => {
|
||||
dismiss();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// Dismiss any menu which was already showing
|
||||
if (dismissExistingMenu) {
|
||||
dismissExistingMenu();
|
||||
}
|
||||
|
||||
// ...and record the presence of this menu.
|
||||
dismissExistingMenu = dismiss;
|
||||
|
||||
const popupService = openmct.$injector.get('popupService');
|
||||
popupService.display(menu, [event.pageX,event.pageY], {
|
||||
marginX: 0,
|
||||
marginY: -50
|
||||
});
|
||||
|
||||
body.on(initiatingEvent, menuClickHandler);
|
||||
}
|
||||
36
src/plugins/notebook/utils/removeDialog.js
Normal file
36
src/plugins/notebook/utils/removeDialog.js
Normal file
@@ -0,0 +1,36 @@
|
||||
export default class RemoveDialog {
|
||||
constructor(openmct, options) {
|
||||
this.name = options.name;
|
||||
this.openmct = openmct;
|
||||
|
||||
this.callback = options.callback;
|
||||
this.cssClass = options.cssClass || 'icon-trash';
|
||||
this.description = options.description || 'Remove action dialog';
|
||||
this.iconClass = "error";
|
||||
this.key = 'remove';
|
||||
this.message = options.message || `This action will permanently ${this.name.toLowerCase()}. Do you wish to continue?`;
|
||||
}
|
||||
|
||||
show() {
|
||||
const dialog = this.openmct.overlays.dialog({
|
||||
iconClass: this.iconClass,
|
||||
message: this.message,
|
||||
buttons: [
|
||||
{
|
||||
label: "Ok",
|
||||
callback: () => {
|
||||
this.callback(true);
|
||||
dialog.dismiss();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Cancel",
|
||||
callback: () => {
|
||||
this.callback(false);
|
||||
dialog.dismiss();
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@
|
||||
-->
|
||||
<div ng-controller="StackedPlotController as stackedPlot"
|
||||
class="c-plot c-plot--stacked holder holder-plot has-control-bar">
|
||||
<div class="l-control-bar" ng-show="!stackedPlot.hideExportButtons">
|
||||
<div class="c-control-bar" ng-show="!stackedPlot.hideExportButtons">
|
||||
<span class="c-button-set c-button-set--strip-h">
|
||||
<button class="c-button icon-download"
|
||||
ng-click="stackedPlot.exportPNG()"
|
||||
|
||||
@@ -54,27 +54,29 @@ function (
|
||||
* @constructor
|
||||
*/
|
||||
function MCTChartController($scope) {
|
||||
this.$scope = $scope;
|
||||
this.isDestroyed = false;
|
||||
this.lines = [];
|
||||
this.pointSets = [];
|
||||
this.alarmSets = [];
|
||||
this.offset = {};
|
||||
this.config = $scope.config;
|
||||
this.listenTo(this.$scope, '$destroy', this.destroy, this);
|
||||
this.draw = this.draw.bind(this);
|
||||
this.scheduleDraw = this.scheduleDraw.bind(this);
|
||||
this.seriesElements = new WeakMap();
|
||||
this.$onInit = () => {
|
||||
this.$scope = $scope;
|
||||
this.isDestroyed = false;
|
||||
this.lines = [];
|
||||
this.pointSets = [];
|
||||
this.alarmSets = [];
|
||||
this.offset = {};
|
||||
this.config = $scope.config;
|
||||
this.listenTo(this.$scope, '$destroy', this.destroy, this);
|
||||
this.draw = this.draw.bind(this);
|
||||
this.scheduleDraw = this.scheduleDraw.bind(this);
|
||||
this.seriesElements = new WeakMap();
|
||||
|
||||
this.listenTo(this.config.series, 'add', this.onSeriesAdd, this);
|
||||
this.listenTo(this.config.series, 'remove', this.onSeriesRemove, this);
|
||||
this.listenTo(this.config.yAxis, 'change:key', this.clearOffset, this);
|
||||
this.listenTo(this.config.xAxis, 'change:key', this.clearOffset, this);
|
||||
this.listenTo(this.config.yAxis, 'change', this.scheduleDraw);
|
||||
this.listenTo(this.config.xAxis, 'change', this.scheduleDraw);
|
||||
this.$scope.$watch('highlights', this.scheduleDraw);
|
||||
this.$scope.$watch('rectangles', this.scheduleDraw);
|
||||
this.config.series.forEach(this.onSeriesAdd, this);
|
||||
this.listenTo(this.config.series, 'add', this.onSeriesAdd, this);
|
||||
this.listenTo(this.config.series, 'remove', this.onSeriesRemove, this);
|
||||
this.listenTo(this.config.yAxis, 'change:key', this.clearOffset, this);
|
||||
this.listenTo(this.config.xAxis, 'change:key', this.clearOffset, this);
|
||||
this.listenTo(this.config.yAxis, 'change', this.scheduleDraw);
|
||||
this.listenTo(this.config.xAxis, 'change', this.scheduleDraw);
|
||||
this.$scope.$watch('highlights', this.scheduleDraw);
|
||||
this.$scope.$watch('rectangles', this.scheduleDraw);
|
||||
this.config.series.forEach(this.onSeriesAdd, this);
|
||||
}
|
||||
}
|
||||
|
||||
eventHelpers.extend(MCTChartController.prototype);
|
||||
|
||||
@@ -34,25 +34,27 @@ define([
|
||||
* values near the cursor.
|
||||
*/
|
||||
function MCTPlotController($scope, $element, $window) {
|
||||
this.$scope = $scope;
|
||||
this.$scope.config = this.config;
|
||||
this.$scope.plot = this;
|
||||
this.$element = $element;
|
||||
this.$window = $window;
|
||||
this.$onInit = () => {
|
||||
this.$scope = $scope;
|
||||
this.$scope.config = this.config;
|
||||
this.$scope.plot = this;
|
||||
this.$element = $element;
|
||||
this.$window = $window;
|
||||
|
||||
this.xScale = new LinearScale(this.config.xAxis.get('displayRange'));
|
||||
this.yScale = new LinearScale(this.config.yAxis.get('displayRange'));
|
||||
this.xScale = new LinearScale(this.config.xAxis.get('displayRange'));
|
||||
this.yScale = new LinearScale(this.config.yAxis.get('displayRange'));
|
||||
|
||||
this.pan = undefined;
|
||||
this.marquee = undefined;
|
||||
this.pan = undefined;
|
||||
this.marquee = undefined;
|
||||
|
||||
this.chartElementBounds = undefined;
|
||||
this.tickUpdate = false;
|
||||
this.chartElementBounds = undefined;
|
||||
this.tickUpdate = false;
|
||||
|
||||
this.$scope.plotHistory = this.plotHistory = [];
|
||||
this.listenTo(this.$scope, 'plot:clearHistory', this.clear, this);
|
||||
this.$scope.plotHistory = this.plotHistory = [];
|
||||
this.listenTo(this.$scope, 'plot:clearHistory', this.clear, this);
|
||||
|
||||
this.initialize();
|
||||
this.initialize();
|
||||
}
|
||||
}
|
||||
|
||||
MCTPlotController.$inject = ['$scope', '$element', '$window'];
|
||||
@@ -65,10 +67,10 @@ define([
|
||||
}
|
||||
this.$canvas = this.$element.find('canvas');
|
||||
|
||||
this.listenTo(this.$canvas, 'click', this.onMouseClick, this);
|
||||
this.listenTo(this.$canvas, 'mousemove', this.trackMousePosition, this);
|
||||
this.listenTo(this.$canvas, 'mouseleave', this.untrackMousePosition, this);
|
||||
this.listenTo(this.$canvas, 'mousedown', this.onMouseDown, this);
|
||||
this.listenTo(this.$canvas, 'wheel', this.wheelZoom, this);
|
||||
|
||||
this.watchForMarquee();
|
||||
};
|
||||
@@ -76,7 +78,6 @@ define([
|
||||
MCTPlotController.prototype.initialize = function () {
|
||||
this.$canvas = this.$element.find('canvas');
|
||||
|
||||
this.listenTo(this.$canvas, 'click', this.onMouseClick, this);
|
||||
this.listenTo(this.$canvas, 'mousemove', this.trackMousePosition, this);
|
||||
this.listenTo(this.$canvas, 'mouseleave', this.untrackMousePosition, this);
|
||||
this.listenTo(this.$canvas, 'mousedown', this.onMouseDown, this);
|
||||
@@ -208,23 +209,6 @@ define([
|
||||
this.highlightValues(point);
|
||||
};
|
||||
|
||||
MCTPlotController.prototype.onMouseClick = function ($event) {
|
||||
const isClick = this.isMouseClick();
|
||||
if (this.pan) {
|
||||
this.endPan($event);
|
||||
}
|
||||
if (this.marquee) {
|
||||
this.endMarquee($event);
|
||||
}
|
||||
this.$scope.$apply();
|
||||
|
||||
if (!this.$scope.highlights.length || !isClick) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$scope.lockHighlightPoint = !this.$scope.lockHighlightPoint;
|
||||
};
|
||||
|
||||
MCTPlotController.prototype.highlightValues = function (point) {
|
||||
this.highlightPoint = point;
|
||||
this.$scope.$emit('plot:highlight:update', point);
|
||||
@@ -272,11 +256,23 @@ define([
|
||||
MCTPlotController.prototype.onMouseUp = function ($event) {
|
||||
this.stopListening(this.$window, 'mouseup', this.onMouseUp, this);
|
||||
this.stopListening(this.$window, 'mousemove', this.trackMousePosition, this);
|
||||
|
||||
if (this.isMouseClick()) {
|
||||
this.$scope.lockHighlightPoint = !this.$scope.lockHighlightPoint;
|
||||
}
|
||||
|
||||
if (this.allowPan) {
|
||||
return this.endPan($event);
|
||||
}
|
||||
|
||||
if (this.allowMarquee) {
|
||||
return this.endMarquee($event);
|
||||
}
|
||||
};
|
||||
|
||||
MCTPlotController.prototype.isMouseClick = function () {
|
||||
if (!this.marquee) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
const { start, end } = this.marquee;
|
||||
@@ -329,7 +325,7 @@ define([
|
||||
} else {
|
||||
// A history entry is created by startMarquee, need to remove
|
||||
// if marquee zoom doesn't occur.
|
||||
this.back();
|
||||
this.plotHistory.pop();
|
||||
}
|
||||
this.$scope.rectangles = [];
|
||||
this.marquee = undefined;
|
||||
|
||||
@@ -114,15 +114,17 @@ define([
|
||||
}
|
||||
|
||||
function MCTTicksController($scope, $element) {
|
||||
this.$scope = $scope;
|
||||
this.$element = $element;
|
||||
this.$onInit = () => {
|
||||
this.$scope = $scope;
|
||||
this.$element = $element;
|
||||
|
||||
this.tickCount = 4;
|
||||
this.tickUpdate = false;
|
||||
this.listenTo(this.axis, 'change:displayRange', this.updateTicks, this);
|
||||
this.listenTo(this.axis, 'change:format', this.updateTicks, this);
|
||||
this.listenTo(this.$scope, '$destroy', this.stopListening, this);
|
||||
this.updateTicks();
|
||||
this.tickCount = 4;
|
||||
this.tickUpdate = false;
|
||||
this.listenTo(this.axis, 'change:displayRange', this.updateTicks, this);
|
||||
this.listenTo(this.axis, 'change:format', this.updateTicks, this);
|
||||
this.listenTo(this.$scope, '$destroy', this.stopListening, this);
|
||||
this.updateTicks();
|
||||
}
|
||||
}
|
||||
|
||||
MCTTicksController.$inject = ['$scope', '$element'];
|
||||
|
||||
@@ -81,7 +81,8 @@ define(
|
||||
clonedElement.classList.add(className);
|
||||
}
|
||||
element.id = oldId;
|
||||
}
|
||||
},
|
||||
removeContainer: true // Set to false to debug what html2canvas renders
|
||||
}).then(function (canvas) {
|
||||
dialog.dismiss();
|
||||
return new Promise(function (resolve, reject) {
|
||||
|
||||
@@ -227,8 +227,9 @@ define([
|
||||
};
|
||||
|
||||
PlotController.prototype.stopLoading = function () {
|
||||
this.$scope.pending -= 1;
|
||||
this.$scope.$digest();
|
||||
this.$scope.$evalAsync(() => {
|
||||
this.$scope.pending -= 1;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
72
src/plugins/plotlyPlot/PlotlyViewProvider.js
Normal file
72
src/plugins/plotlyPlot/PlotlyViewProvider.js
Normal file
@@ -0,0 +1,72 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2019, 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 PlotlyViewLayout from './components/PlotlyViewLayout.vue';
|
||||
import Vue from 'vue';
|
||||
|
||||
export default function PlotlyViewProvider(openmct) {
|
||||
return {
|
||||
key: 'plotlyPlot',
|
||||
name: 'Plotly Plot',
|
||||
cssClass: 'icon-plot-overlay',
|
||||
canView: function (domainObject) {
|
||||
return domainObject.type === 'plotlyPlot';
|
||||
},
|
||||
canEdit: function (domainObject) {
|
||||
return domainObject.type === 'plotlyPlot';
|
||||
},
|
||||
view: function (domainObject) {
|
||||
let component;
|
||||
|
||||
return {
|
||||
show: function (element, isEditing) {
|
||||
component = new Vue({
|
||||
provide: {
|
||||
openmct,
|
||||
domainObject
|
||||
},
|
||||
el: element,
|
||||
components: {
|
||||
PlotlyViewLayout
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isEditing
|
||||
}
|
||||
},
|
||||
template: '<plotly-view-layout :isEditing="isEditing"></plotly-view-layout>'
|
||||
});
|
||||
},
|
||||
onEditModeChange: function (isEditing) {
|
||||
component.isEditing = isEditing;
|
||||
},
|
||||
destroy: function (element) {
|
||||
component.$destroy();
|
||||
component = undefined;
|
||||
}
|
||||
};
|
||||
},
|
||||
priority: function () {
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
}
|
||||
221
src/plugins/plotlyPlot/components/PlotlyViewLayout.vue
Normal file
221
src/plugins/plotlyPlot/components/PlotlyViewLayout.vue
Normal file
@@ -0,0 +1,221 @@
|
||||
<template>
|
||||
<div class="l-view-section"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Plotly from 'plotly.js-dist';
|
||||
import moment from 'moment'
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject'],
|
||||
data: function () {
|
||||
|
||||
return {
|
||||
telemetryObjects: [],
|
||||
bounds: this.openmct.time.bounds(),
|
||||
timeRange: 0,
|
||||
plotData: {},
|
||||
subscriptions: {}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.plotElement = document.querySelector('.l-view-section');
|
||||
|
||||
this.composition = this.openmct.composition.get(this.domainObject);
|
||||
this.composition.on('add', this.addTelemetry);
|
||||
this.composition.on('remove', this.removeTelemetry);
|
||||
this.composition.load();
|
||||
|
||||
this.openmct.time.on('bounds', this.refreshData);
|
||||
this.openmct.time.on('clock', this.changeClock);
|
||||
},
|
||||
destroyed() {
|
||||
this.unsubscribe();
|
||||
},
|
||||
methods: {
|
||||
changeClock() {
|
||||
if (this.openmct.time.clock()) {
|
||||
Plotly.purge(this.plotElement);
|
||||
this.telemetryObjects.forEach((telemetryObject, index) => {
|
||||
this.subscribeTo(telemetryObject, index);
|
||||
});
|
||||
}
|
||||
},
|
||||
addTelemetry(telemetryObject) {
|
||||
this.telemetryObjects.push(telemetryObject);
|
||||
const index = this.telemetryObjects.findIndex(obj => obj === telemetryObject);
|
||||
this.requestHistory(telemetryObject, index, true);
|
||||
this.subscribeTo(telemetryObject, index);
|
||||
},
|
||||
subscribeTo(telemetryObject, index) {
|
||||
let keyString = this.openmct.objects.makeKeyString(telemetryObject.identifier);
|
||||
this.subscriptions[keyString] = this.openmct.telemetry.subscribe(telemetryObject, (datum) => {
|
||||
//Check that telemetry object has not been removed since telemetry was requested.
|
||||
if (!this.telemetryObjects.includes(telemetryObject)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const length = this.plotData[telemetryObject.identifier.key].x.length;
|
||||
this.updateData(datum, index, length);
|
||||
});
|
||||
},
|
||||
unsubscribe(keyString) {
|
||||
this.subscriptions[keyString]();
|
||||
delete this.subscriptions[keyString];
|
||||
},
|
||||
refreshData(bounds, isTick) {
|
||||
this.bounds = bounds;
|
||||
|
||||
this.telemetryObjects.forEach((telemetryObject, index) => {
|
||||
if(!isTick) {
|
||||
this.requestHistory(telemetryObject, index, false);
|
||||
} else {
|
||||
if (this.timeRange === 0 || this.timeRange !== this.bounds.end - this.bounds.start) {
|
||||
this.timeRange = this.bounds.end - this.bounds.start;
|
||||
|
||||
this.requestHistory(telemetryObject, index, false);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
requestHistory(telemetryObject, index, isAdd) {
|
||||
this.openmct
|
||||
.telemetry
|
||||
.request(telemetryObject, {
|
||||
start: this.bounds.start,
|
||||
end: this.bounds.end
|
||||
})
|
||||
.then((telemetryData) => {
|
||||
this.addTrace(telemetryData, telemetryObject, index, isAdd);
|
||||
});
|
||||
},
|
||||
getLayout(telemetryObject, isFixed) {
|
||||
return {
|
||||
hovermode: 'compare',
|
||||
hoverdistance: -1,
|
||||
autosize: "true",
|
||||
showlegend: false,
|
||||
font: {
|
||||
family: "'Helvetica Neue', Helvetica, Arial, sans-serif",
|
||||
size: "12px",
|
||||
color: "#666"
|
||||
},
|
||||
xaxis: { // hardcoded as UTC for now
|
||||
title: 'UTC',
|
||||
zeroline: false,
|
||||
range: isFixed ? 'undefined' : [
|
||||
this.formatDatumX({utc: this.bounds.start}),
|
||||
this.formatDatumX({utc: this.bounds.start})
|
||||
]
|
||||
},
|
||||
yaxis: {
|
||||
title: this.getYAxisLabel(telemetryObject),
|
||||
zeroline: false
|
||||
},
|
||||
margin: {
|
||||
l: 40,
|
||||
r: 10,
|
||||
b: 40,
|
||||
t: 10
|
||||
},
|
||||
paper_bgcolor: 'transparent',
|
||||
plot_bgcolor: 'transparent'
|
||||
}
|
||||
},
|
||||
removeTelemetry(identifier) {
|
||||
let keyString = this.openmct.objects.makeKeyString(identifier);
|
||||
this.unsubscribe(keyString);
|
||||
this.telemetryObjects = this.telemetryObjects.filter((object) => !_.eq(identifier, object.identifier));
|
||||
if (!this.domainObject.composition.length) {
|
||||
Plotly.purge(this.plotElement);
|
||||
} else {
|
||||
Plotly.deleteTraces(this.plotElement, this.domainObject.composition.length);
|
||||
}
|
||||
},
|
||||
getYAxisLabel(telemetryObject) {
|
||||
this.setYAxisProp(telemetryObject);
|
||||
const valueMetadatas = this.openmct.telemetry.getMetadata(telemetryObject).values();
|
||||
const index = valueMetadatas.findIndex(value => value.key === this.yAxisProp);
|
||||
const yLabel = valueMetadatas[index].name;
|
||||
|
||||
return yLabel;
|
||||
},
|
||||
setYAxisProp(telemetryObject) {
|
||||
if (telemetryObject.type === 'generator') {
|
||||
this.yAxisProp = 'sin';
|
||||
} else if (telemetryObject.type === 'example.state-generator') {
|
||||
this.yAxisProp = 'state';
|
||||
} else if (telemetryObject.type === 'conditionSet') {
|
||||
this.yAxisProp = 'output';
|
||||
}
|
||||
},
|
||||
formatDatumX(datum) {
|
||||
let timestamp = moment.utc(datum.utc).format('YYYY-MM-DDTHH:mm:ss[Z]');
|
||||
return timestamp;
|
||||
},
|
||||
formatDatumY(datum) {
|
||||
return datum.sin;
|
||||
},
|
||||
addTrace(telemetryData, telemetryObject, index, isAdd) {
|
||||
let x = [];
|
||||
let y = [];
|
||||
|
||||
const colors = ['red', 'green', 'blue'];
|
||||
|
||||
telemetryData.forEach((datum) => {
|
||||
x.push(this.formatDatumX(datum));
|
||||
y.push(this.formatDatumY(datum));
|
||||
})
|
||||
|
||||
let traceData = [{ // trace configuration
|
||||
x,
|
||||
y,
|
||||
type: 'scattergl',
|
||||
mode: 'lines+markers',
|
||||
line: {
|
||||
color: colors[index], // to set new color for each trace
|
||||
shape: 'linear'
|
||||
}
|
||||
}];
|
||||
|
||||
this.plotData[telemetryObject.identifier.key] = traceData[0];
|
||||
|
||||
if (!this.plotElement.childNodes.length) { // not traces yet, so create new plot
|
||||
Plotly.newPlot(
|
||||
this.plotElement,
|
||||
traceData,
|
||||
this.getLayout(telemetryObject, true),
|
||||
{
|
||||
displayModeBar: false, // turns off hover-activated toolbar
|
||||
staticPlot: true // turns off hover effects on datapoints
|
||||
}
|
||||
);
|
||||
} else {
|
||||
if (isAdd) { // add a new trace to existing plot
|
||||
Plotly.addTraces(this.plotElement, traceData);
|
||||
} else { // update existing trace with new data (bounds change)
|
||||
Plotly.react(this.plotElement, Object.values(this.plotData), this.getLayout(telemetryObject, false));
|
||||
}
|
||||
}
|
||||
},
|
||||
updateData(datum, index, length) {
|
||||
// plot all datapoints within bounds
|
||||
if (datum.utc <= this.bounds.end && this.openmct.time.clock()) {
|
||||
Plotly.extendTraces(
|
||||
this.plotElement,
|
||||
{
|
||||
x: [[this.formatDatumX(datum)]],
|
||||
y: [[this.formatDatumY(datum)]]
|
||||
},
|
||||
[index], // apply changes to particular trace
|
||||
length // set the fixed number of points (will drop points from beginning as new points are added)
|
||||
);
|
||||
let newRange = {
|
||||
'xaxis.range': [this.formatDatumX({utc: this.bounds.start}),this.formatDatumX({utc: this.bounds.end})]
|
||||
};
|
||||
Plotly.relayout(this.plotElement, newRange);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
2
src/plugins/plotlyPlot/components/plotly.scss
Normal file
2
src/plugins/plotlyPlot/components/plotly.scss
Normal file
@@ -0,0 +1,2 @@
|
||||
.plot svg {
|
||||
}
|
||||
18
src/plugins/plotlyPlot/plugin.js
Normal file
18
src/plugins/plotlyPlot/plugin.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import PlotlyViewProvider from './PlotlyViewProvider.js';
|
||||
|
||||
export default function () {
|
||||
return function install(openmct) {
|
||||
openmct.objectViews.addProvider(new PlotlyViewProvider(openmct));
|
||||
|
||||
openmct.types.addType('plotlyPlot', {
|
||||
name: "Plotly Plot",
|
||||
description: "Simple plot rendered by plotly.js",
|
||||
creatable: true,
|
||||
cssClass: 'icon-plot-overlay',
|
||||
initialize: function (domainObject) {
|
||||
domainObject.composition = [];
|
||||
domainObject.telemetry = {};
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -34,6 +34,7 @@ define([
|
||||
'./URLIndicatorPlugin/URLIndicatorPlugin',
|
||||
'./telemetryMean/plugin',
|
||||
'./plot/plugin',
|
||||
'./plotlyPlot/plugin',
|
||||
'./telemetryTable/plugin',
|
||||
'./staticRootPlugin/plugin',
|
||||
'./notebook/plugin',
|
||||
@@ -66,6 +67,7 @@ define([
|
||||
URLIndicatorPlugin,
|
||||
TelemetryMean,
|
||||
PlotPlugin,
|
||||
PlotlyPlotPlugin,
|
||||
TelemetryTablePlugin,
|
||||
StaticRootPlugin,
|
||||
Notebook,
|
||||
@@ -88,7 +90,8 @@ define([
|
||||
var bundleMap = {
|
||||
LocalStorage: 'platform/persistence/local',
|
||||
MyItems: 'platform/features/my-items',
|
||||
CouchDB: 'platform/persistence/couch'
|
||||
CouchDB: 'platform/persistence/couch',
|
||||
Elasticsearch: 'platform/persistence/elastic'
|
||||
};
|
||||
|
||||
var plugins = _.mapValues(bundleMap, function (bundleName, pluginName) {
|
||||
@@ -170,8 +173,8 @@ define([
|
||||
plugins.ExampleImagery = ExampleImagery;
|
||||
plugins.ImageryPlugin = ImageryPlugin;
|
||||
plugins.Plot = PlotPlugin;
|
||||
plugins.PlotlyPlot = PlotlyPlotPlugin.default;
|
||||
plugins.TelemetryTable = TelemetryTablePlugin;
|
||||
|
||||
plugins.SummaryWidget = SummaryWidget;
|
||||
plugins.TelemetryMean = TelemetryMean;
|
||||
plugins.URLIndicator = URLIndicatorPlugin;
|
||||
@@ -180,7 +183,7 @@ define([
|
||||
plugins.FolderView = FolderView;
|
||||
plugins.Tabs = Tabs;
|
||||
plugins.FlexibleLayout = FlexibleLayout;
|
||||
plugins.LADTable = LADTable;
|
||||
plugins.LADTable = LADTable.default;
|
||||
plugins.Filters = Filters;
|
||||
plugins.ObjectMigration = ObjectMigration.default;
|
||||
plugins.GoToOriginalAction = GoToOriginalAction.default;
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
{'is-current': isCurrent(tab)},
|
||||
tab.type.definition.cssClass
|
||||
]"
|
||||
@click="showTab(tab)"
|
||||
@click="showTab(tab, index)"
|
||||
>
|
||||
<span class="c-button__label">{{ tab.domainObject.name }}</span>
|
||||
</button>
|
||||
@@ -48,6 +48,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<object-view
|
||||
v-if="internalDomainObject.keep_alive ? currentTab : isCurrent(tab)"
|
||||
class="c-tabs-view__object"
|
||||
:object="tab.domainObject"
|
||||
/>
|
||||
@@ -57,7 +58,6 @@
|
||||
|
||||
<script>
|
||||
import ObjectView from '../../../ui/components/ObjectView.vue';
|
||||
import _ from 'lodash';
|
||||
|
||||
var unknownObjectType = {
|
||||
definition: {
|
||||
@@ -73,6 +73,7 @@ export default {
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
internalDomainObject: this.domainObject,
|
||||
currentTab: {},
|
||||
tabsList: [],
|
||||
setCurrentTab: true,
|
||||
@@ -85,9 +86,17 @@ export default {
|
||||
this.composition.on('add', this.addItem);
|
||||
this.composition.on('remove', this.removeItem);
|
||||
this.composition.on('reorder', this.onReorder);
|
||||
this.composition.load();
|
||||
this.composition.load().then(() => {
|
||||
let currentTabIndex = this.domainObject.currentTabIndex;
|
||||
|
||||
if (currentTabIndex !== undefined && this.tabsList.length > currentTabIndex) {
|
||||
this.currentTab = this.tabsList[currentTabIndex];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.unsubscribe = this.openmct.objects.observe(this.internalDomainObject, '*', this.updateInternalDomainObject);
|
||||
|
||||
document.addEventListener('dragstart', this.dragstart);
|
||||
document.addEventListener('dragend', this.dragend);
|
||||
},
|
||||
@@ -96,18 +105,25 @@ export default {
|
||||
this.composition.off('remove', this.removeItem);
|
||||
this.composition.off('reorder', this.onReorder);
|
||||
|
||||
this.unsubscribe();
|
||||
|
||||
document.removeEventListener('dragstart', this.dragstart);
|
||||
document.removeEventListener('dragend', this.dragend);
|
||||
},
|
||||
methods:{
|
||||
showTab(tab) {
|
||||
showTab(tab, index) {
|
||||
if (index !== undefined) {
|
||||
this.storeCurrentTabIndex(index);
|
||||
}
|
||||
|
||||
this.currentTab = tab;
|
||||
},
|
||||
addItem(domainObject) {
|
||||
let type = this.openmct.types.get(domainObject.type) || unknownObjectType,
|
||||
tabItem = {
|
||||
domainObject,
|
||||
type: type
|
||||
type: type,
|
||||
key: this.openmct.objects.makeKeyString(domainObject.identifier)
|
||||
};
|
||||
|
||||
this.tabsList.push(tabItem);
|
||||
@@ -126,7 +142,7 @@ export default {
|
||||
this.tabsList.splice(pos, 1);
|
||||
|
||||
if (this.isCurrent(tabToBeRemoved)) {
|
||||
this.showTab(this.tabsList[this.tabsList.length - 1]);
|
||||
this.showTab(this.tabsList[this.tabsList.length - 1], this.tabsList.length - 1);
|
||||
}
|
||||
},
|
||||
onReorder(reorderPlan) {
|
||||
@@ -138,6 +154,7 @@ export default {
|
||||
},
|
||||
onDrop(e) {
|
||||
this.setCurrentTab = true;
|
||||
this.storeCurrentTabIndex(this.tabsList.length);
|
||||
},
|
||||
dragstart(e) {
|
||||
if (e.dataTransfer.types.includes('openmct/domain-object-path')) {
|
||||
@@ -155,7 +172,13 @@ export default {
|
||||
this.allowDrop = false;
|
||||
},
|
||||
isCurrent(tab) {
|
||||
return _.isEqual(this.currentTab, tab)
|
||||
return this.currentTab.key === tab.key;
|
||||
},
|
||||
updateInternalDomainObject(domainObject) {
|
||||
this.internalDomainObject = domainObject;
|
||||
},
|
||||
storeCurrentTabIndex(index) {
|
||||
this.openmct.objects.mutate(this.internalDomainObject, 'currentTabIndex', index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,27 @@ define([
|
||||
cssClass: 'icon-tabs-view',
|
||||
initialize(domainObject) {
|
||||
domainObject.composition = [];
|
||||
}
|
||||
domainObject.keep_alive = true;
|
||||
},
|
||||
form: [
|
||||
{
|
||||
"key": "keep_alive",
|
||||
"name": "Keep Tabs Alive",
|
||||
"control": "select",
|
||||
"options": [
|
||||
{
|
||||
'name': 'True',
|
||||
'value': true
|
||||
},
|
||||
{
|
||||
'name': 'False',
|
||||
'value': false
|
||||
}
|
||||
],
|
||||
"required": true,
|
||||
"cssClass": "l-input"
|
||||
}
|
||||
]
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
@@ -165,7 +165,16 @@
|
||||
/******************************* LEGACY */
|
||||
.s-status-taking-snapshot,
|
||||
.overlay.snapshot {
|
||||
// Handle overflow-y issues with tables and html2canvas
|
||||
// Replaces .l-sticky-headers .l-tabular-body { overflow: auto; }
|
||||
.c-table__body-w { overflow: auto; }
|
||||
.c-table {
|
||||
&__body-w {
|
||||
overflow: auto; // Handle overflow-y issues with tables and html2canvas
|
||||
}
|
||||
|
||||
&-control-bar {
|
||||
display: none;
|
||||
+ * {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,13 +122,8 @@ button {
|
||||
margin-left: $interiorMargin;
|
||||
}
|
||||
|
||||
$c1: nth($mixedSettingBg, 1);
|
||||
$c2: nth($mixedSettingBg, 2);
|
||||
$mixedBgD: $mixedSettingBgSize $mixedSettingBgSize;
|
||||
|
||||
&--mixed {
|
||||
// E.g. click-icons in toolbar that apply to multiple selected items with different settings
|
||||
@include bgStripes2Color($c1, $c2, $bgSize: $mixedBgD);
|
||||
@include mixedBg();
|
||||
}
|
||||
|
||||
&--swatched {
|
||||
@@ -151,13 +146,6 @@ button {
|
||||
flex: 1 1 auto;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
&--mixed {
|
||||
// Styling for swatched buttons when settings are mixed
|
||||
> [class*='swatch'] {
|
||||
@include bgStripes2Color($c1, $c2, $bgSize: $mixedBgD);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -577,6 +565,20 @@ select {
|
||||
}
|
||||
}
|
||||
}
|
||||
/******************************************************** CONTROL BARS */
|
||||
.c-control-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
> * + * {
|
||||
margin-left: $interiorMarginSm;
|
||||
}
|
||||
|
||||
&__label {
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************** PALETTES */
|
||||
.c-palette {
|
||||
@@ -821,6 +823,10 @@ select {
|
||||
box-shadow: rgba($colorBodyFg, 0.4) 0 0 3px;
|
||||
flex: 0 0 auto;
|
||||
padding: $interiorMargin $interiorMarginLg;
|
||||
|
||||
&--mixed {
|
||||
@include mixedBg();
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************** SLIDERS */
|
||||
|
||||
@@ -40,28 +40,58 @@ mct-plot {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.c-plot,
|
||||
.gl-plot {
|
||||
.s-status-taking-snapshot & {
|
||||
.c-control-bar {
|
||||
display: none;
|
||||
}
|
||||
.gl-plot-y-label__select {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.c-plot {
|
||||
$p: $mainViewPad;
|
||||
position: absolute;
|
||||
top: $p; right: $p; bottom: $p; left: $p;
|
||||
//$p: $mainViewPad;
|
||||
@include abs($mainViewPad);
|
||||
//position: absolute;
|
||||
//top: $p; right: $p; bottom: $p; left: $p;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> * + * {
|
||||
margin-top: $interiorMargin;
|
||||
}
|
||||
|
||||
.l-control-bar {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.l-view-section {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&--stacked {
|
||||
.l-view-section {
|
||||
// Make this a flex container
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
.gl-plot.child-frame {
|
||||
mct-plot {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
flex: 1 1 auto;
|
||||
&:not(:first-child) {
|
||||
margin-top: $interiorMargin;
|
||||
.child-frame {
|
||||
.has-control-bar {
|
||||
.c-control-bar {
|
||||
// Hides buttons per plot element in a stacked plot
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
mct-plot {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.s-status-timeconductor-unsynced .holder-plot {
|
||||
@@ -70,7 +100,6 @@ mct-plot {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -186,7 +215,7 @@ mct-plot {
|
||||
left: 0; top: 0; right: auto; bottom: 0;
|
||||
padding-left: 5px;
|
||||
text-orientation: mixed;
|
||||
overflow: hidden;
|
||||
//overflow: hidden;
|
||||
writing-mode: vertical-lr;
|
||||
&:before {
|
||||
// Icon denoting configurability
|
||||
@@ -339,11 +368,11 @@ mct-plot {
|
||||
z-index: -10;
|
||||
|
||||
.l-view-section {
|
||||
$m: $interiorMargin;
|
||||
top: $m !important;
|
||||
right: $m;
|
||||
bottom: $m;
|
||||
left: $m;
|
||||
//$m: $interiorMargin;
|
||||
//top: $m !important;
|
||||
//right: $m;
|
||||
//bottom: $m;
|
||||
//left: $m;
|
||||
|
||||
.s-status-timeconductor-unsynced .holder-plot {
|
||||
.t-object-alert.t-alert-unsynced {
|
||||
|
||||
@@ -19,59 +19,13 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
/******************************************************************* VIEWS */
|
||||
// From _views.scss
|
||||
// Legacy overlay and stacked plots depend on this for now
|
||||
// Styles for sub-dividing views generically
|
||||
.l-control-bar {
|
||||
// Element that can be placed above l-view-section, holds controls, buttons, etc.
|
||||
height: $controlBarH;
|
||||
}
|
||||
|
||||
.c-control-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
> * + * {
|
||||
margin-left: $interiorMarginSm;
|
||||
}
|
||||
|
||||
&__label {
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.l-view-section {
|
||||
@include abs();
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.has-control-bar {
|
||||
.l-view-section {
|
||||
top: $controlBarH + $interiorMargin;
|
||||
}
|
||||
}
|
||||
|
||||
.child-frame {
|
||||
.has-control-bar {
|
||||
.l-control-bar,
|
||||
.c-control-bar {
|
||||
// Hides buttons per plot element in a stacked plot
|
||||
display: none;
|
||||
}
|
||||
.l-view-section {
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*********************************************************************** CLOCKS AND TIMERS */
|
||||
.c-clock,
|
||||
.c-timer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 1.25em;
|
||||
overflow: hidden;
|
||||
|
||||
> * {
|
||||
flex: 0 0 auto;
|
||||
@@ -82,6 +36,12 @@
|
||||
&__value {
|
||||
color: $colorBodyFgEm;
|
||||
}
|
||||
|
||||
.c-frame & {
|
||||
// When in a Display or Flexible Layout
|
||||
@include abs();
|
||||
padding: $interiorMargin;
|
||||
}
|
||||
}
|
||||
|
||||
.c-clock {
|
||||
|
||||
@@ -50,6 +50,14 @@
|
||||
}
|
||||
|
||||
/************************** EFFECTS */
|
||||
@mixin mixedBg() {
|
||||
$c1: nth($mixedSettingBg, 1);
|
||||
$c2: nth($mixedSettingBg, 2);
|
||||
$mixedBgD: $mixedSettingBgSize $mixedSettingBgSize;
|
||||
@include bgStripes2Color($c1, $c2, $bgSize: $mixedBgD);
|
||||
}
|
||||
|
||||
|
||||
@mixin pulse($animName: pulse, $dur: 500ms, $iteration: infinite, $opacity0: 0.5, $opacity100: 1) {
|
||||
@keyframes #{$animName} {
|
||||
0% { opacity: $opacity0; }
|
||||
|
||||
@@ -388,7 +388,21 @@
|
||||
.s-status-taking-snapshot,
|
||||
.overlay.snapshot {
|
||||
// Handle overflow-y issues with tables and html2canvas
|
||||
background: $colorBodyBg; // Prevent html2canvas from using white background
|
||||
color: $colorBodyFg;
|
||||
padding: $interiorMarginSm !important; // Prevents items from going right to the edge of the image
|
||||
|
||||
.l-sticky-headers .l-tabular-body { overflow: auto; }
|
||||
.l-browse-bar {
|
||||
display: none; // Suppress browse-bar when snapshotting from view-large overlay
|
||||
+ * {
|
||||
margin-top: 0 !important; // Remove margin from any following elements
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
box-shadow: none !important; // Prevent html2canvas problems with box-shadow
|
||||
}
|
||||
}
|
||||
|
||||
.c-notebook-snapshot {
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
@import "../plugins/folderView/components/list-item.scss";
|
||||
@import "../plugins/folderView/components/list-view.scss";
|
||||
@import "../plugins/imagery/components/imagery-view-layout.scss";
|
||||
@import "../plugins/plotlyPlot/components/plotly.scss";
|
||||
@import "../plugins/telemetryTable/components/table-row.scss";
|
||||
@import "../plugins/telemetryTable/components/telemetry-filter-indicator.scss";
|
||||
@import "../plugins/tabs/components/tabs.scss";
|
||||
|
||||
224
src/testTools.js
224
src/testTools.js
@@ -1,4 +1,6 @@
|
||||
import MCT from 'MCT';
|
||||
let nativeFunctions = [],
|
||||
mockObjects = setMockObjects();
|
||||
|
||||
export function createOpenMct() {
|
||||
const openmct = new MCT();
|
||||
@@ -16,3 +18,225 @@ export function createMouseEvent(eventName) {
|
||||
view: window
|
||||
});
|
||||
}
|
||||
|
||||
export const spyOnBuiltins = (functionNames, object = window) => {
|
||||
functionNames.forEach(functionName => {
|
||||
if (nativeFunctions[functionName]) {
|
||||
throw `Builtin spy function already defined for ${functionName}`;
|
||||
}
|
||||
|
||||
nativeFunctions.push({functionName, object, nativeFunction: object[functionName]});
|
||||
spyOn(object, functionName);
|
||||
});
|
||||
};
|
||||
|
||||
export const clearBuiltinSpies = () => {
|
||||
nativeFunctions.forEach(clearBuiltinSpy);
|
||||
nativeFunctions = [];
|
||||
};
|
||||
|
||||
function clearBuiltinSpy(funcDefinition) {
|
||||
funcDefinition.object[funcDefinition.functionName] = funcDefinition.nativeFunction;
|
||||
}
|
||||
|
||||
export const getLatestTelemetry = (telemetry = [], opts = {}) => {
|
||||
let latest = [],
|
||||
timeFormat = opts.timeFormat || 'utc';
|
||||
|
||||
if(telemetry.length) {
|
||||
latest = telemetry.reduce((prev, cur) => {
|
||||
return prev[timeFormat] > cur[timeFormat] ? prev : cur;
|
||||
});
|
||||
}
|
||||
|
||||
return latest;
|
||||
};
|
||||
|
||||
// EXAMPLE:
|
||||
// getMockObjects({
|
||||
// name: 'Jamie Telemetry',
|
||||
// keys: ['test','other','yeah','sup'],
|
||||
// format: 'local',
|
||||
// telemetryConfig: {
|
||||
// hints: {
|
||||
// test: {
|
||||
// domain: 1
|
||||
// },
|
||||
// other: {
|
||||
// range: 2
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
export const getMockObjects = (opts = {}) => {
|
||||
opts.type = opts.type || 'default';
|
||||
if(opts.objectKeyStrings && !Array.isArray(opts.objectKeyStrings)) {
|
||||
throw `"getMockObjects" optional parameter "objectKeyStrings" must be an array of string object keys`;
|
||||
}
|
||||
|
||||
let requestedMocks = {};
|
||||
|
||||
if (!opts.objectKeyStrings) {
|
||||
requestedMocks = copyObj(mockObjects[opts.type]);
|
||||
} else {
|
||||
opts.objectKeyStrings.forEach(objKey => {
|
||||
if(mockObjects[opts.type] && mockObjects[opts.type][objKey]) {
|
||||
requestedMocks[objKey] = copyObj(mockObjects[opts.type][objKey]);
|
||||
} else {
|
||||
throw `No mock object for object key "${objKey}" of type "${opts.type}"`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// build out custom telemetry mappings if necessary
|
||||
if(requestedMocks.telemetry && opts.telemetryConfig) {
|
||||
let keys = opts.telemetryConfig.keys,
|
||||
format = opts.telemetryConfig.format || 'utc',
|
||||
hints = opts.telemetryConfig.hints,
|
||||
values;
|
||||
|
||||
// if utc, keep default
|
||||
if(format === 'utc') {
|
||||
// save for later if new keys
|
||||
if(keys) {
|
||||
format = requestedMocks.telemetry
|
||||
.telemetry.values.find((vals) => vals.key === 'utc');
|
||||
}
|
||||
} else {
|
||||
format = {
|
||||
key: format,
|
||||
name: "Time",
|
||||
format: format === 'local' ? 'local-format' : format,
|
||||
hints: {
|
||||
domain: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(keys) {
|
||||
values = keys.map((key) => ({ key, name: key + ' attribute' }));
|
||||
values.push(format); // add time format back in
|
||||
} else {
|
||||
values = requestedMocks.telemetry.telemetry.values;
|
||||
}
|
||||
|
||||
if(hints) {
|
||||
for(let val of values) {
|
||||
if(hints[val.key]) {
|
||||
val.hints = hints[val.key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
requestedMocks.telemetry.telemetry.values = values;
|
||||
}
|
||||
|
||||
// overwrite any field keys
|
||||
if(opts.overwrite) {
|
||||
for(let mock in requestedMocks) {
|
||||
if(opts.overwrite[mock]) {
|
||||
for(let key in opts.overwrite[mock]) {
|
||||
if (Object.prototype.hasOwnProperty.call(opts.overwrite[mock], key)) {
|
||||
requestedMocks[mock][key] = opts.overwrite[mock][key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return requestedMocks;
|
||||
}
|
||||
|
||||
// EXAMPLE:
|
||||
// getMockTelemetry({
|
||||
// name: 'My Telemetry',
|
||||
// keys: ['test','other','yeah','sup'],
|
||||
// count: 8,
|
||||
// format: 'local'
|
||||
// })
|
||||
export const getMockTelemetry = (opts = {}) => {
|
||||
let count = opts.count || 2,
|
||||
format = opts.format || 'utc',
|
||||
name = opts.name || 'Mock Telemetry Datum',
|
||||
keyCount = 2,
|
||||
keys = false,
|
||||
telemetry = [];
|
||||
|
||||
if(opts.keys && Array.isArray(opts.keys)) {
|
||||
keyCount = opts.keys.length;
|
||||
keys = opts.keys;
|
||||
} else if(opts.keyCount) {
|
||||
keyCount = opts.keyCount;
|
||||
}
|
||||
|
||||
for(let i = 1; i < count + 1; i++) {
|
||||
let datum = {
|
||||
[format]: i,
|
||||
name
|
||||
}
|
||||
|
||||
for(let k = 1; k < keyCount + 1; k++) {
|
||||
let key = keys ? keys[k - 1] : 'some-key-' + k,
|
||||
value = keys ? keys[k - 1] + ' value ' + i : 'some value ' + i + '-' + k;
|
||||
datum[key] = value;
|
||||
}
|
||||
|
||||
telemetry.push(datum);
|
||||
}
|
||||
|
||||
return telemetry;
|
||||
}
|
||||
|
||||
// copy objects a bit more easily
|
||||
function copyObj(obj) {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
}
|
||||
|
||||
// add any other necessary types to this mockObjects object
|
||||
function setMockObjects() {
|
||||
return {
|
||||
default: {
|
||||
ladTable: {
|
||||
identifier: { namespace: "", key: "lad-object"},
|
||||
type: 'LadTable',
|
||||
composition: []
|
||||
},
|
||||
ladTableSet: {
|
||||
identifier: { namespace: "", key: "lad-set-object"},
|
||||
type: 'LadTableSet',
|
||||
composition: []
|
||||
},
|
||||
telemetry: {
|
||||
identifier: { namespace: "", key: "telemetry-object"},
|
||||
type: "test-telemetry-object",
|
||||
name: "Test Telemetry Object",
|
||||
telemetry: {
|
||||
values: [{
|
||||
key: "name",
|
||||
name: "Name",
|
||||
format: "string"
|
||||
},{
|
||||
key: "utc",
|
||||
name: "Time",
|
||||
format: "utc",
|
||||
hints: {
|
||||
domain: 1
|
||||
}
|
||||
},{
|
||||
name: "Some attribute 1",
|
||||
key: "some-key-1",
|
||||
hints: {
|
||||
range: 1
|
||||
}
|
||||
}, {
|
||||
name: "Some attribute 2",
|
||||
key: "some-key-2"
|
||||
}]
|
||||
}
|
||||
}
|
||||
},
|
||||
otherType: {
|
||||
example: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,8 @@
|
||||
<script>
|
||||
import ObjectView from './ObjectView.vue'
|
||||
import ContextMenuDropDown from './contextMenuDropDown.vue';
|
||||
import PreviewHeader from '@/ui/preview/preview-header.vue';
|
||||
import Vue from 'vue';
|
||||
|
||||
const SIMPLE_CONTENT_TYPES = [
|
||||
'clock',
|
||||
@@ -116,13 +118,41 @@ export default {
|
||||
childElement = parentElement.children[0];
|
||||
|
||||
this.openmct.overlays.overlay({
|
||||
element: childElement,
|
||||
element: this.getOverlayElement(childElement),
|
||||
size: 'large',
|
||||
onDestroy() {
|
||||
parentElement.append(childElement);
|
||||
}
|
||||
});
|
||||
},
|
||||
getOverlayElement(childElement) {
|
||||
const fragment = new DocumentFragment();
|
||||
const header = this.getPreviewHeader();
|
||||
fragment.append(header);
|
||||
fragment.append(childElement);
|
||||
|
||||
return fragment;
|
||||
},
|
||||
getPreviewHeader() {
|
||||
const domainObject = this.objectPath[0];
|
||||
const preview = new Vue({
|
||||
components: {
|
||||
PreviewHeader
|
||||
},
|
||||
provide: {
|
||||
openmct: this.openmct,
|
||||
objectPath: this.objectPath
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
domainObject
|
||||
}
|
||||
},
|
||||
template: '<PreviewHeader :domainObject="domainObject" :hideViewSwitcher="true" :showNotebookMenuSwitcher="true"></PreviewHeader>'
|
||||
});
|
||||
|
||||
return preview.$mount().$el;
|
||||
},
|
||||
getSelectionContext() {
|
||||
return this.$refs.objectView.getSelectionContext();
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ export default {
|
||||
keys.forEach(key => {
|
||||
let firstChild = this.$el.querySelector(':first-child');
|
||||
if (firstChild) {
|
||||
if ((typeof styleObj[key] === 'string') && (styleObj[key].indexOf('transparent') > -1)) {
|
||||
if ((typeof styleObj[key] === 'string') && (styleObj[key].indexOf('__no_value') > -1)) {
|
||||
if (firstChild.style[key]) {
|
||||
firstChild.style[key] = '';
|
||||
}
|
||||
@@ -201,7 +201,7 @@ export default {
|
||||
},
|
||||
initObjectStyles() {
|
||||
if (!this.styleRuleManager) {
|
||||
this.styleRuleManager = new StyleRuleManager((this.currentObject.configuration && this.currentObject.configuration.objectStyles), this.openmct, this.updateStyle.bind(this));
|
||||
this.styleRuleManager = new StyleRuleManager((this.currentObject.configuration && this.currentObject.configuration.objectStyles), this.openmct, this.updateStyle.bind(this), true);
|
||||
} else {
|
||||
this.styleRuleManager.updateObjectStyleConfig(this.currentObject.configuration && this.currentObject.configuration.objectStyles);
|
||||
}
|
||||
|
||||
@@ -23,9 +23,12 @@
|
||||
}
|
||||
|
||||
&:not(.c-so-view--no-frame) {
|
||||
background: $colorBodyBg;
|
||||
border: $browseFrameBorder;
|
||||
padding: $interiorMargin;
|
||||
|
||||
.is-editing & {
|
||||
background: rgba($colorBodyBg, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
&--no-frame {
|
||||
|
||||
@@ -108,7 +108,7 @@ export default {
|
||||
let object = selection[0][0].context.item;
|
||||
if (object) {
|
||||
let type = this.openmct.types.get(object.type);
|
||||
this.showStyles = (this.excludeObjectTypes.indexOf(object.type) < 0) && type.definition.creatable;
|
||||
this.showStyles = this.isLayoutObject(selection[0], object.type) || this.isCreatableObject(object, type);
|
||||
}
|
||||
if (!this.currentTabbedView.key || (!this.showStyles && this.currentTabbedView.key === this.tabbedViews[1].key))
|
||||
{
|
||||
@@ -116,6 +116,14 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
isLayoutObject(selection, objectType) {
|
||||
//we allow conditionSets to be styled if they're part of a layout
|
||||
return selection.length > 1 &&
|
||||
((objectType === 'conditionSet') || (this.excludeObjectTypes.indexOf(objectType) < 0));
|
||||
},
|
||||
isCreatableObject(object, type) {
|
||||
return (this.excludeObjectTypes.indexOf(object.type) < 0) && type.definition.creatable;
|
||||
},
|
||||
updateCurrentTab(view) {
|
||||
this.currentTabbedView = view;
|
||||
},
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
|
||||
<script>
|
||||
import ConditionalStylesView from '../../plugins/condition/components/inspector/ConditionalStylesView.vue';
|
||||
import MultiSelectStylesView from '../../plugins/condition/components/inspector/MultiSelectStylesView.vue';
|
||||
import Vue from 'vue';
|
||||
|
||||
export default {
|
||||
@@ -45,6 +46,7 @@ export default {
|
||||
methods: {
|
||||
updateSelection(selection) {
|
||||
if (selection.length > 0 && selection[0].length > 0) {
|
||||
let template = selection.length > 1 ? '<multi-select-styles-view></multi-select-styles-view>' : '<conditional-styles-view></conditional-styles-view>';
|
||||
if (this.component) {
|
||||
this.component.$destroy();
|
||||
this.component = undefined;
|
||||
@@ -59,9 +61,10 @@ export default {
|
||||
},
|
||||
el: viewContainer,
|
||||
components: {
|
||||
ConditionalStylesView
|
||||
ConditionalStylesView,
|
||||
MultiSelectStylesView
|
||||
},
|
||||
template: '<conditional-styles-view></conditional-styles-view>'
|
||||
template: template
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
<!-- Action buttons -->
|
||||
<NotebookMenuSwitcher v-if="notebookEnabled"
|
||||
:domain-object="domainObject"
|
||||
:object-path="openmct.router.path"
|
||||
class="c-notebook-snapshot-menubutton"
|
||||
/>
|
||||
<div class="l-browse-bar__actions">
|
||||
@@ -198,8 +199,6 @@ export default {
|
||||
updateName(event) {
|
||||
if (event.target.innerText !== this.domainObject.name && event.target.innerText.match(/\S/)) {
|
||||
this.openmct.objects.mutate(this.domainObject, 'name', event.target.innerText);
|
||||
} else {
|
||||
event.target.innerText = this.domainObject.name;
|
||||
}
|
||||
},
|
||||
updateNameOnEnterKeyPress(event) {
|
||||
|
||||
@@ -21,28 +21,12 @@
|
||||
*****************************************************************************/
|
||||
<template>
|
||||
<div class="l-preview-window">
|
||||
<div class="l-browse-bar">
|
||||
<div class="l-browse-bar__start">
|
||||
<div
|
||||
class="l-browse-bar__object-name--w"
|
||||
:class="type.cssClass"
|
||||
>
|
||||
<span class="l-browse-bar__object-name">
|
||||
{{ domainObject.name }}
|
||||
</span>
|
||||
<context-menu-drop-down :object-path="objectPath" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="l-browse-bar__end">
|
||||
<div class="l-browse-bar__actions">
|
||||
<view-switcher
|
||||
:views="views"
|
||||
:current-view="currentView"
|
||||
@setView="setView"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PreviewHeader
|
||||
:current-view="currentView"
|
||||
:domain-object="domainObject"
|
||||
:views="views"
|
||||
@setView="setView"
|
||||
/>
|
||||
<div class="l-preview-window__object-view">
|
||||
<div ref="objectView"></div>
|
||||
</div>
|
||||
@@ -50,13 +34,13 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ContextMenuDropDown from '../../ui/components/contextMenuDropDown.vue';
|
||||
import ViewSwitcher from '../../ui/layout/ViewSwitcher.vue';
|
||||
import PreviewHeader from './preview-header.vue';
|
||||
import {STYLE_CONSTANTS} from "@/plugins/condition/utils/constants";
|
||||
import StyleRuleManager from "@/plugins/condition/StyleRuleManager";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ContextMenuDropDown,
|
||||
ViewSwitcher
|
||||
PreviewHeader
|
||||
},
|
||||
inject: [
|
||||
'openmct',
|
||||
@@ -64,12 +48,9 @@ export default {
|
||||
],
|
||||
data() {
|
||||
let domainObject = this.objectPath[0];
|
||||
let type = this.openmct.types.get(domainObject.type);
|
||||
|
||||
return {
|
||||
domainObject: domainObject,
|
||||
type: type,
|
||||
notebookEnabled: false,
|
||||
viewKey: undefined
|
||||
};
|
||||
},
|
||||
@@ -90,6 +71,14 @@ export default {
|
||||
},
|
||||
destroyed() {
|
||||
this.view.destroy();
|
||||
if (this.stopListeningStyles) {
|
||||
this.stopListeningStyles();
|
||||
}
|
||||
|
||||
if (this.styleRuleManager) {
|
||||
this.styleRuleManager.destroy();
|
||||
delete this.styleRuleManager;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
clear() {
|
||||
@@ -97,6 +86,7 @@ export default {
|
||||
this.view.destroy();
|
||||
this.$refs.objectView.innerHTML = '';
|
||||
}
|
||||
|
||||
delete this.view;
|
||||
delete this.viewContainer;
|
||||
},
|
||||
@@ -110,6 +100,46 @@ export default {
|
||||
|
||||
this.view = this.currentView.view(this.domainObject, this.objectPath);
|
||||
this.view.show(this.viewContainer, false);
|
||||
this.initObjectStyles();
|
||||
},
|
||||
initObjectStyles() {
|
||||
if (!this.styleRuleManager) {
|
||||
this.styleRuleManager = new StyleRuleManager((this.domainObject.configuration && this.domainObject.configuration.objectStyles), this.openmct, this.updateStyle.bind(this));
|
||||
} else {
|
||||
this.styleRuleManager.updateObjectStyleConfig(this.domainObject.configuration && this.domainObject.configuration.objectStyles);
|
||||
}
|
||||
|
||||
if (this.stopListeningStyles) {
|
||||
this.stopListeningStyles();
|
||||
}
|
||||
|
||||
this.stopListeningStyles = this.openmct.objects.observe(this.domainObject, 'configuration.objectStyles', (newObjectStyle) => {
|
||||
//Updating styles in the inspector view will trigger this so that the changes are reflected immediately
|
||||
this.styleRuleManager.updateObjectStyleConfig(newObjectStyle);
|
||||
});
|
||||
},
|
||||
updateStyle(styleObj) {
|
||||
if (!styleObj) {
|
||||
return;
|
||||
}
|
||||
let keys = Object.keys(styleObj);
|
||||
keys.forEach(key => {
|
||||
let firstChild = this.$refs.objectView.querySelector(':first-child');
|
||||
if (firstChild) {
|
||||
if ((typeof styleObj[key] === 'string') && (styleObj[key].indexOf('__no_value') > -1)) {
|
||||
if (firstChild.style[key]) {
|
||||
firstChild.style[key] = '';
|
||||
}
|
||||
} else {
|
||||
if (!styleObj.isStyleInvisible && firstChild.classList.contains(STYLE_CONSTANTS.isStyleInvisible)) {
|
||||
firstChild.classList.remove(STYLE_CONSTANTS.isStyleInvisible);
|
||||
} else if (styleObj.isStyleInvisible && !firstChild.classList.contains(styleObj.isStyleInvisible)) {
|
||||
firstChild.classList.add(styleObj.isStyleInvisible);
|
||||
}
|
||||
firstChild.style[key] = styleObj[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,12 @@ export default class PreviewAction {
|
||||
* Dependencies
|
||||
*/
|
||||
this._openmct = openmct;
|
||||
|
||||
if (PreviewAction.isVisible === undefined) {
|
||||
PreviewAction.isVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
invoke(objectPath) {
|
||||
let preview = new Vue({
|
||||
components: {
|
||||
@@ -59,16 +64,27 @@ export default class PreviewAction {
|
||||
callback: () => overlay.dismiss()
|
||||
}
|
||||
],
|
||||
onDestroy: () => preview.$destroy()
|
||||
onDestroy: () => {
|
||||
PreviewAction.isVisible = false;
|
||||
preview.$destroy()
|
||||
}
|
||||
});
|
||||
|
||||
PreviewAction.isVisible = true;
|
||||
}
|
||||
|
||||
appliesTo(objectPath) {
|
||||
return !this._isNavigatedObject(objectPath)
|
||||
return !PreviewAction.isVisible && !this._isNavigatedObject(objectPath);
|
||||
}
|
||||
|
||||
_isNavigatedObject(objectPath) {
|
||||
let targetObject = objectPath[0];
|
||||
let navigatedObject = this._openmct.router.path[0];
|
||||
return targetObject.identifier.namespace === navigatedObject.identifier.namespace &&
|
||||
targetObject.identifier.key === navigatedObject.identifier.key;
|
||||
}
|
||||
_preventPreview(objectPath) {
|
||||
const noPreviewTypes = ['folder'];
|
||||
return noPreviewTypes.includes(objectPath[0].type);
|
||||
}
|
||||
}
|
||||
|
||||
92
src/ui/preview/preview-header.vue
Normal file
92
src/ui/preview/preview-header.vue
Normal file
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<div class="l-browse-bar">
|
||||
<div class="l-browse-bar__start">
|
||||
<div
|
||||
class="l-browse-bar__object-name--w"
|
||||
:class="type.cssClass"
|
||||
>
|
||||
<span class="l-browse-bar__object-name">
|
||||
{{ domainObject.name }}
|
||||
</span>
|
||||
<context-menu-drop-down :object-path="objectPath" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="l-browse-bar__end">
|
||||
<div class="l-browse-bar__actions">
|
||||
<view-switcher
|
||||
:v-if="!hideViewSwitcher"
|
||||
:views="views"
|
||||
:current-view="currentView"
|
||||
@setView="setView"
|
||||
/>
|
||||
<NotebookMenuSwitcher v-if="showNotebookMenuSwitcher"
|
||||
:domain-object="domainObject"
|
||||
:ignore-link="true"
|
||||
:object-path="objectPath"
|
||||
class="c-notebook-snapshot-menubutton"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import ContextMenuDropDown from '../../ui/components/contextMenuDropDown.vue';
|
||||
import NotebookMenuSwitcher from '@/plugins/notebook/components/notebook-menu-switcher.vue';
|
||||
import ViewSwitcher from '../../ui/layout/ViewSwitcher.vue';
|
||||
|
||||
export default {
|
||||
inject: [
|
||||
'openmct',
|
||||
'objectPath'
|
||||
],
|
||||
components: {
|
||||
ContextMenuDropDown,
|
||||
NotebookMenuSwitcher,
|
||||
ViewSwitcher
|
||||
},
|
||||
props: {
|
||||
currentView: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
domainObject: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
hideViewSwitcher: {
|
||||
type: Boolean,
|
||||
default: () => {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
showNotebookMenuSwitcher: {
|
||||
type: Boolean,
|
||||
default: () => {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
views: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
type: this.openmct.types.get(this.domainObject.type)
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
setView(view) {
|
||||
this.$emit('setView', view);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user