mirror of
https://github.com/nasa/openmct.git
synced 2025-06-25 10:44:21 +00:00
Compare commits
572 Commits
condition-
...
deep-plotl
Author | SHA1 | Date | |
---|---|---|---|
043d6aa9c3 | |||
ecfab8f7f3 | |||
05c38c37aa | |||
ce78925119 | |||
26aca0f433 | |||
41259bbd40 | |||
580640ff47 | |||
a4aec5d492 | |||
23303c910e | |||
00b3f3ac0b | |||
3282934cf6 | |||
c157fab081 | |||
7c07b66cc9 | |||
7a906ccf5c | |||
ff7debfb81 | |||
92ba103f45 | |||
2c2d8d6b56 | |||
cfadb9f4fd | |||
c185f77a15 | |||
396817b2d1 | |||
96eb6d6b74 | |||
cb5d47f66f | |||
ea90d02d66 | |||
95f73d8eb8 | |||
0dff431f4a | |||
a37c686993 | |||
f12166097c | |||
61d238a097 | |||
d103a22fa0 | |||
04a60cfcbb | |||
8d723960f4 | |||
6d3cd2c699 | |||
87bf94fe0a | |||
af93823b6f | |||
f9deb80350 | |||
4a39ddf425 | |||
83c273b976 | |||
021d730814 | |||
7dd81beb03 | |||
1842d3923c | |||
26838635b6 | |||
ae62b15abf | |||
ba41c1a30e | |||
b9a85d9c4d | |||
11f2c35bb2 | |||
80eab8bad1 | |||
b2d8d640ae | |||
766f48c1ba | |||
da7b93f9b3 | |||
56e6fa66c2 | |||
99c095a69f | |||
f885e83505 | |||
928bc4c68a | |||
d5539c7ae4 | |||
c86a104fb6 | |||
9fa4707c82 | |||
7e2cfa36de | |||
aaa60a1545 | |||
717231fed2 | |||
7fb2bc9729 | |||
46fedc1a30 | |||
addeb635e9 | |||
608d63a7b0 | |||
10679e5f4f | |||
38b8f03b1a | |||
779a42c28c | |||
80c2504768 | |||
80359e3f16 | |||
3b6ef9b44b | |||
66aa4f099f | |||
aa6c6cb88b | |||
4e5cc840d7 | |||
c68edd9b7d | |||
11574b7c40 | |||
abc2cd2413 | |||
5d74882646 | |||
9fe7f230e6 | |||
de4c5b3729 | |||
2a7901914a | |||
73b0fc6f79 | |||
ddef16795c | |||
d188b9a056 | |||
f510f3edd0 | |||
e05b0bb562 | |||
713c5e9fb7 | |||
17bca04560 | |||
61bdadc33c | |||
e0c5bca47d | |||
cdc7c1af64 | |||
3158baa998 | |||
698508fde4 | |||
68a96989e1 | |||
46a6a43234 | |||
d41fc27b55 | |||
24bb96cc90 | |||
483ee173d6 | |||
469e93d916 | |||
f96dfcc942 | |||
063a6c0e51 | |||
7c289d76b6 | |||
c617a440eb | |||
8f81a45b9b | |||
666459be87 | |||
53df89aa5d | |||
8f553f6327 | |||
f91a64483b | |||
de8d63c09d | |||
58b4a6ebf5 | |||
9d45080526 | |||
d3fe2a6811 | |||
97b37edce4 | |||
d3443518d6 | |||
c33314a4bf | |||
59eb034ab4 | |||
67d53fb62b | |||
0fd637c0e9 | |||
169cc6617a | |||
a946325e95 | |||
1beba78111 | |||
2edfeaa606 | |||
2a1f9fd063 | |||
63fe92f8ea | |||
35f303ffa4 | |||
dd70bb470f | |||
250fee125a | |||
956029c786 | |||
b9ab599c35 | |||
5a690932e9 | |||
b00757150e | |||
8515a411bd | |||
3034ec016a | |||
a81af1ce34 | |||
64c5725687 | |||
72a3248123 | |||
ee4a81bdfd | |||
b90eb80584 | |||
ebaf702c59 | |||
3956cd1c06 | |||
064cf6747e | |||
8512b634c7 | |||
4220a8a68a | |||
e7e5116773 | |||
079201273e | |||
e4c9f156a7 | |||
42eeeea374 | |||
7b060509f5 | |||
3ca9e9ae56 | |||
984bede43b | |||
87d838c690 | |||
072bf361de | |||
4e39d9fb84 | |||
14eaf4e899 | |||
a31d10e708 | |||
f9e88321a3 | |||
9e12203b86 | |||
a5326a7b95 | |||
84874f22e6 | |||
d00e8b68a5 | |||
2907d6d79c | |||
a5a4bb87c5 | |||
389589d7f7 | |||
c02cbd1ba7 | |||
90b475e17b | |||
89a298f5b3 | |||
f990a14a3c | |||
6b00af6ece | |||
5ec8ac95c1 | |||
5f061001d6 | |||
ef3c4ccf47 | |||
1a86204637 | |||
0a1959df38 | |||
03a690a158 | |||
459a055455 | |||
502d29dd25 | |||
bf947a8835 | |||
f0fd0a9cc7 | |||
7282792da1 | |||
ec0291c54d | |||
4413c29abb | |||
91e1a144ed | |||
53440c31d5 | |||
5128af2531 | |||
eedc0f13bc | |||
4172fdf1d5 | |||
7c200df4c4 | |||
d1b28e079a | |||
23ec838643 | |||
271c619c63 | |||
0552769670 | |||
99a9f5b3ba | |||
4b535ade31 | |||
de8f8088e2 | |||
32c16416d3 | |||
0cf27c349b | |||
015aa8c637 | |||
4a07ddbefc | |||
a81009541c | |||
1b680cfaca | |||
311ff003c0 | |||
0cae61444d | |||
28a603def8 | |||
d2b7407674 | |||
fff89a6384 | |||
953b95f79c | |||
6ff5ce78e1 | |||
0857fd95a7 | |||
a1f2608e7c | |||
b7cea7b955 | |||
32da19a486 | |||
39d7dc8372 | |||
f76f537be7 | |||
a681d67e05 | |||
0a634eb490 | |||
fcca8fa8d9 | |||
829eecf1ae | |||
dc7f83754a | |||
51498c0e75 | |||
6dbdebbe05 | |||
c994227d5d | |||
761ca7ad56 | |||
7f49a7bc99 | |||
ca022b8a28 | |||
6f500d0d0b | |||
07d101ac1c | |||
cdf0dd0c10 | |||
c27c347d29 | |||
dc54eef2c9 | |||
57a68a24de | |||
2c1b4b4cfc | |||
437e8a0263 | |||
0a2e912091 | |||
87513a14b7 | |||
168c040f3c | |||
78487a48f6 | |||
a5a197680d | |||
d42bd44485 | |||
77b705ecc8 | |||
48af39a584 | |||
ec978f3a35 | |||
f13714e0c4 | |||
42ac3ef9af | |||
26ffe8efde | |||
87b4000b12 | |||
f790c9bd39 | |||
096c9688d5 | |||
1f19f480ce | |||
44f48a3e2a | |||
60fce4a003 | |||
f04b5b689e | |||
05d981768e | |||
e4a6c21101 | |||
d51dd8b7d0 | |||
aed5377ad2 | |||
459b2060a5 | |||
cbeb25c583 | |||
b38a9ad4ce | |||
ecf3e19f16 | |||
3b82fd5d8b | |||
e08b4ff0ab | |||
1f3ec77bf1 | |||
3f61db2067 | |||
ce1fdbddda | |||
5332d136b7 | |||
983ed7f0e7 | |||
11978cd869 | |||
00d1b5e69f | |||
6731283cf8 | |||
6b4cd25417 | |||
0cd2799d00 | |||
243b9cac24 | |||
316e0f24cf | |||
05f94edb49 | |||
e22458f09e | |||
cc2df8401b | |||
43a82ec05f | |||
fe2e29d69b | |||
60aecfe27e | |||
5d21a8b6fe | |||
500ab52476 | |||
b0bb723357 | |||
b7fffeab1c | |||
e339d743ed | |||
84f0d49d6f | |||
090e89d524 | |||
90dd53e954 | |||
6ab60ab52e | |||
8975bc8c55 | |||
55e5c49f6e | |||
f090f7ffe7 | |||
94b5617e63 | |||
41c79c6032 | |||
83c648cc26 | |||
76e7fec1a0 | |||
09bfd80f69 | |||
15a7d03e74 | |||
1dc9743484 | |||
8f05c57d1a | |||
81caa27cba | |||
74a7ef2565 | |||
649575fd2d | |||
b75b7a958a | |||
625b39d722 | |||
65f80f4c45 | |||
02cd3048c8 | |||
63feaef988 | |||
6095872682 | |||
dba55867f4 | |||
0da80c2a67 | |||
084df5329a | |||
49ff0c79db | |||
7a4b967a01 | |||
ddfa611c44 | |||
efca7c8e58 | |||
8900072239 | |||
a7e57c62f4 | |||
24bade2284 | |||
5fcc4eebe1 | |||
27a09239e3 | |||
8d86c914a1 | |||
fab04519c6 | |||
4a5e106709 | |||
4675fc8ae6 | |||
cf9336dae9 | |||
7f32d196e4 | |||
897d05276a | |||
3e6509ce6f | |||
576b843bd5 | |||
9b8b63a0d8 | |||
a1b1fa464e | |||
95f855f905 | |||
5b00246cc0 | |||
47c388450f | |||
34a149661c | |||
4c4b587d9c | |||
b8b838f490 | |||
8cb29ba4a9 | |||
ece6223b23 | |||
ecabd00b0c | |||
768df84f10 | |||
f8b3899bb9 | |||
3b046db4f8 | |||
97f829da9f | |||
fb1eed1982 | |||
dd9b567025 | |||
fa83b4867c | |||
47d4fc9103 | |||
dabd0bff29 | |||
51c70d02d7 | |||
b74733bf3f | |||
84ae65536b | |||
71424dcf8d | |||
2c40396139 | |||
16a0bf9d6c | |||
5498ba8e1e | |||
0f9d7d2832 | |||
9bd1c51a6e | |||
fd74fb0ec4 | |||
3626ff9947 | |||
fd568409d3 | |||
14e3500a88 | |||
83d08ae369 | |||
39bf601ee1 | |||
cfafecdd64 | |||
629ca089cf | |||
89ae6ef8c7 | |||
300acd6ec8 | |||
ba780981a5 | |||
62774678a7 | |||
ac13bc5850 | |||
e526626e09 | |||
564be6f8ba | |||
371a7d3a3e | |||
8539d60562 | |||
d333fd5822 | |||
364191eddc | |||
583f4dac85 | |||
28255dce01 | |||
c9419d3e2d | |||
b386275330 | |||
d2a45e46f1 | |||
35d1727dbf | |||
8125a4f321 | |||
1a409afb03 | |||
e57c18fd69 | |||
3aec9ec6ff | |||
0e9bf74332 | |||
2609a41ee8 | |||
b8dc5acf00 | |||
fbbdf8cff7 | |||
b0edb19239 | |||
85902b878e | |||
9d5c7a4015 | |||
fc53e855c4 | |||
467c57b7c6 | |||
a51c0d5139 | |||
d46310ca7d | |||
8f87cc78e8 | |||
ee6e0f310e | |||
f328a1078e | |||
b4cf81a0ef | |||
1b9b7e2345 | |||
4456633010 | |||
cda97d142a | |||
858199e396 | |||
f504a335af | |||
463ec47af6 | |||
ec4d121a98 | |||
fea6d2df96 | |||
598d2b31e9 | |||
25e28ab97c | |||
43056c4068 | |||
614206b10c | |||
30a493d038 | |||
96e433beaa | |||
0915aaea3b | |||
80656c1be0 | |||
acd75f86f4 | |||
486dae54bd | |||
92ecf3af1d | |||
fd0c19026d | |||
3109c8d825 | |||
78cf75323f | |||
b744467f21 | |||
a0b7999ea2 | |||
2bb2bb6a1b | |||
11ed7027e7 | |||
36bcfd5a41 | |||
70b5c627ca | |||
f4f1d0387b | |||
a1bf4a92e5 | |||
7d2256d70f | |||
5814d2a35e | |||
6ab84c0bc3 | |||
a7fc9b3caa | |||
67f493f012 | |||
0686e6d38f | |||
7fc825949c | |||
2a9ccdcffd | |||
6db78af69f | |||
038489256c | |||
53b785269b | |||
007b14b5c9 | |||
50b331c451 | |||
44fc62e0ba | |||
2635f085f0 | |||
22161fce7f | |||
386fc75047 | |||
fa6dd84945 | |||
d425bd564c | |||
93e3065b3e | |||
0ad2d59924 | |||
f4468a8233 | |||
dc08877bbb | |||
f08caa6135 | |||
ad7d029ce8 | |||
387912b4d3 | |||
53e0ed4d4a | |||
11c205b5c4 | |||
4ede6351ec | |||
24bbcb466f | |||
682601477c | |||
b6b5cfe403 | |||
b6ce9c6ed7 | |||
6e5e8f0ce8 | |||
2415d785cc | |||
2b5d6beb84 | |||
86316d8940 | |||
1f2b5ec5c8 | |||
0a9d23d86f | |||
379e37c881 | |||
37ef269dce | |||
8db6f8f633 | |||
79557165a3 | |||
ec1d4abde9 | |||
07c5e2800a | |||
79811d6662 | |||
67919ece16 | |||
7029dcf09e | |||
38deef6e72 | |||
b6220288ac | |||
fc03b3a79d | |||
096d6371f1 | |||
e580734c95 | |||
2690156a9d | |||
7ac7a40b1b | |||
dc9e572052 | |||
b15ebfd492 | |||
8baee7a0c9 | |||
dc85063467 | |||
be428b326e | |||
dd0e360709 | |||
04da88e3b4 | |||
9bcab02e35 | |||
1ff4d41b7c | |||
04a5c8f69f | |||
8886a94a01 | |||
f25eebdf3f | |||
f9ba46fe85 | |||
6f6fb859d6 | |||
1e3389b427 | |||
c977c64139 | |||
e419149378 | |||
a5a3e41d21 | |||
ecef8eaf86 | |||
de03cfbe64 | |||
03a6de55d6 | |||
3c5047df5e | |||
3cc630d4c2 | |||
b3488c54cd | |||
01b1d66bea | |||
2e82edb306 | |||
8f0e773ac1 | |||
223a0feada | |||
bc9cadaa77 | |||
0fd0da8331 | |||
f42ec7e2c5 | |||
d6a422fbdb | |||
d98b54bea7 | |||
0beda1d053 | |||
e912ab8f4e | |||
5055a18ca1 | |||
fa21911287 | |||
96746f4042 | |||
b22ad3ded9 | |||
7e0f475c63 | |||
efb3c2b71e | |||
862ea6986f | |||
cfa5dcb02e | |||
23aaada79d | |||
9e4458db10 | |||
a8da06033c | |||
0bf3597147 | |||
cfd9730055 | |||
e88ead30dc | |||
67b24ce846 | |||
709c3fff65 | |||
ab6e87ae6b | |||
cdb7066bed | |||
73d0507f1f | |||
4d263bcf32 | |||
2d8f61172d | |||
621c1dc11e | |||
dd136a5ff4 | |||
8fc785bbd6 | |||
82be503f4f | |||
7feb933519 | |||
e806e5a293 | |||
770951c6da | |||
7b7c7b528a | |||
a554aa20f8 | |||
ff1ef1f184 | |||
bf1efaf912 | |||
ff2bc41317 | |||
bdaf8aff15 | |||
1dc4f9f6bb | |||
d6320f5da1 | |||
276be5e857 | |||
3101e77ecc | |||
ab6dae16f1 | |||
36222d79c6 | |||
8034317796 | |||
a59f3a550e | |||
415b967c0b | |||
8df549e8d9 | |||
68949e070c | |||
654333dabe | |||
1589e4236a | |||
7db4ac8ff6 | |||
03829af2ad | |||
322cd94be7 | |||
5d31806fb7 | |||
384f0efcb3 |
@ -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
|
||||
|
2
API.md
2
API.md
@ -231,7 +231,7 @@ attributes
|
||||
of this object. This is used for specifying an icon to appear next to each
|
||||
object of this type.
|
||||
|
||||
The [Open MCT Tutorials](https://github.com/openmct/openmct-tutorial) provide a
|
||||
The [Open MCT Tutorials](https://github.com/nasa/openmct-tutorial) provide a
|
||||
step-by-step examples of writing code for Open MCT that includes a [section on
|
||||
defining a new object type](https://github.com/nasa/openmct-tutorial#step-3---providing-objects).
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
<!--
|
||||
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.
|
||||
|
||||
@ -18,4 +18,4 @@
|
||||
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.
|
||||
-->
|
||||
-->
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*
|
||||
@ -18,4 +18,4 @@
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
*****************************************************************************/
|
||||
|
@ -1,80 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Code coverage report for All files</title>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="stylesheet" href="prettify.css" />
|
||||
<link rel="stylesheet" href="base.css" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style type='text/css'>
|
||||
.coverage-summary .sorter {
|
||||
background-image: url(sort-arrow-sprite.png);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class='wrapper'>
|
||||
<div class='pad1'>
|
||||
<h1>
|
||||
/
|
||||
</h1>
|
||||
<div class='clearfix'>
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">100% </span>
|
||||
<span class="quiet">Statements</span>
|
||||
<span class='fraction'>0/0</span>
|
||||
</div>
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">100% </span>
|
||||
<span class="quiet">Branches</span>
|
||||
<span class='fraction'>0/0</span>
|
||||
</div>
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">100% </span>
|
||||
<span class="quiet">Functions</span>
|
||||
<span class='fraction'>0/0</span>
|
||||
</div>
|
||||
<div class='fl pad1y space-right2'>
|
||||
<span class="strong">100% </span>
|
||||
<span class="quiet">Lines</span>
|
||||
<span class='fraction'>0/0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='status-line high'></div>
|
||||
<div class="pad1">
|
||||
<table class="coverage-summary">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
|
||||
<th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>
|
||||
<th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>
|
||||
<th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||
<th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
|
||||
<th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||
<th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
|
||||
<th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||
<th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
|
||||
<th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div><div class='push'></div><!-- for sticky footer -->
|
||||
</div><!-- /wrapper -->
|
||||
<div class='footer quiet pad2 space-top1 center small'>
|
||||
Code coverage
|
||||
generated by <a href="http://istanbul-js.org/" target="_blank">istanbul</a> at Wed Dec 11 2019 13:15:10 GMT-0800 (Pacific Standard Time)
|
||||
</div>
|
||||
</div>
|
||||
<script src="prettify.js"></script>
|
||||
<script>
|
||||
window.onload = function () {
|
||||
if (typeof prettyPrint === 'function') {
|
||||
prettyPrint();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<script src="sorter.js"></script>
|
||||
</body>
|
||||
</html>
|
File diff suppressed because one or more lines are too long
Binary file not shown.
Before Width: | Height: | Size: 209 B |
@ -1,158 +0,0 @@
|
||||
var addSorting = (function () {
|
||||
"use strict";
|
||||
var cols,
|
||||
currentSort = {
|
||||
index: 0,
|
||||
desc: false
|
||||
};
|
||||
|
||||
// returns the summary table element
|
||||
function getTable() { return document.querySelector('.coverage-summary'); }
|
||||
// returns the thead element of the summary table
|
||||
function getTableHeader() { return getTable().querySelector('thead tr'); }
|
||||
// returns the tbody element of the summary table
|
||||
function getTableBody() { return getTable().querySelector('tbody'); }
|
||||
// returns the th element for nth column
|
||||
function getNthColumn(n) { return getTableHeader().querySelectorAll('th')[n]; }
|
||||
|
||||
// loads all columns
|
||||
function loadColumns() {
|
||||
var colNodes = getTableHeader().querySelectorAll('th'),
|
||||
colNode,
|
||||
cols = [],
|
||||
col,
|
||||
i;
|
||||
|
||||
for (i = 0; i < colNodes.length; i += 1) {
|
||||
colNode = colNodes[i];
|
||||
col = {
|
||||
key: colNode.getAttribute('data-col'),
|
||||
sortable: !colNode.getAttribute('data-nosort'),
|
||||
type: colNode.getAttribute('data-type') || 'string'
|
||||
};
|
||||
cols.push(col);
|
||||
if (col.sortable) {
|
||||
col.defaultDescSort = col.type === 'number';
|
||||
colNode.innerHTML = colNode.innerHTML + '<span class="sorter"></span>';
|
||||
}
|
||||
}
|
||||
return cols;
|
||||
}
|
||||
// attaches a data attribute to every tr element with an object
|
||||
// of data values keyed by column name
|
||||
function loadRowData(tableRow) {
|
||||
var tableCols = tableRow.querySelectorAll('td'),
|
||||
colNode,
|
||||
col,
|
||||
data = {},
|
||||
i,
|
||||
val;
|
||||
for (i = 0; i < tableCols.length; i += 1) {
|
||||
colNode = tableCols[i];
|
||||
col = cols[i];
|
||||
val = colNode.getAttribute('data-value');
|
||||
if (col.type === 'number') {
|
||||
val = Number(val);
|
||||
}
|
||||
data[col.key] = val;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
// loads all row data
|
||||
function loadData() {
|
||||
var rows = getTableBody().querySelectorAll('tr'),
|
||||
i;
|
||||
|
||||
for (i = 0; i < rows.length; i += 1) {
|
||||
rows[i].data = loadRowData(rows[i]);
|
||||
}
|
||||
}
|
||||
// sorts the table using the data for the ith column
|
||||
function sortByIndex(index, desc) {
|
||||
var key = cols[index].key,
|
||||
sorter = function (a, b) {
|
||||
a = a.data[key];
|
||||
b = b.data[key];
|
||||
return a < b ? -1 : a > b ? 1 : 0;
|
||||
},
|
||||
finalSorter = sorter,
|
||||
tableBody = document.querySelector('.coverage-summary tbody'),
|
||||
rowNodes = tableBody.querySelectorAll('tr'),
|
||||
rows = [],
|
||||
i;
|
||||
|
||||
if (desc) {
|
||||
finalSorter = function (a, b) {
|
||||
return -1 * sorter(a, b);
|
||||
};
|
||||
}
|
||||
|
||||
for (i = 0; i < rowNodes.length; i += 1) {
|
||||
rows.push(rowNodes[i]);
|
||||
tableBody.removeChild(rowNodes[i]);
|
||||
}
|
||||
|
||||
rows.sort(finalSorter);
|
||||
|
||||
for (i = 0; i < rows.length; i += 1) {
|
||||
tableBody.appendChild(rows[i]);
|
||||
}
|
||||
}
|
||||
// removes sort indicators for current column being sorted
|
||||
function removeSortIndicators() {
|
||||
var col = getNthColumn(currentSort.index),
|
||||
cls = col.className;
|
||||
|
||||
cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, '');
|
||||
col.className = cls;
|
||||
}
|
||||
// adds sort indicators for current column being sorted
|
||||
function addSortIndicators() {
|
||||
getNthColumn(currentSort.index).className += currentSort.desc ? ' sorted-desc' : ' sorted';
|
||||
}
|
||||
// adds event listeners for all sorter widgets
|
||||
function enableUI() {
|
||||
var i,
|
||||
el,
|
||||
ithSorter = function ithSorter(i) {
|
||||
var col = cols[i];
|
||||
|
||||
return function () {
|
||||
var desc = col.defaultDescSort;
|
||||
|
||||
if (currentSort.index === i) {
|
||||
desc = !currentSort.desc;
|
||||
}
|
||||
sortByIndex(i, desc);
|
||||
removeSortIndicators();
|
||||
currentSort.index = i;
|
||||
currentSort.desc = desc;
|
||||
addSortIndicators();
|
||||
};
|
||||
};
|
||||
for (i =0 ; i < cols.length; i += 1) {
|
||||
if (cols[i].sortable) {
|
||||
// add the click event handler on the th so users
|
||||
// dont have to click on those tiny arrows
|
||||
el = getNthColumn(i).querySelector('.sorter').parentElement;
|
||||
if (el.addEventListener) {
|
||||
el.addEventListener('click', ithSorter(i));
|
||||
} else {
|
||||
el.attachEvent('onclick', ithSorter(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// adds sorting functionality to the UI
|
||||
return function () {
|
||||
if (!getTable()) {
|
||||
return;
|
||||
}
|
||||
cols = loadColumns();
|
||||
loadData(cols);
|
||||
addSortIndicators();
|
||||
enableUI();
|
||||
};
|
||||
})();
|
||||
|
||||
window.addEventListener('load', addSorting);
|
@ -9,7 +9,8 @@ define([
|
||||
values: [
|
||||
{
|
||||
key: "name",
|
||||
name: "Name"
|
||||
name: "Name",
|
||||
format: "string"
|
||||
},
|
||||
{
|
||||
key: "utc",
|
||||
|
@ -31,6 +31,7 @@ define([
|
||||
period: 10,
|
||||
offset: 0,
|
||||
dataRateInHz: 1,
|
||||
randomness: 0,
|
||||
phase: 0
|
||||
};
|
||||
|
||||
@ -52,7 +53,8 @@ define([
|
||||
'period',
|
||||
'offset',
|
||||
'dataRateInHz',
|
||||
'phase'
|
||||
'phase',
|
||||
'randomness'
|
||||
];
|
||||
|
||||
request = request || {};
|
||||
|
@ -65,8 +65,8 @@
|
||||
name: data.name,
|
||||
utc: nextStep,
|
||||
yesterday: nextStep - 60*60*24*1000,
|
||||
sin: sin(nextStep, data.period, data.amplitude, data.offset, data.phase),
|
||||
cos: cos(nextStep, data.period, data.amplitude, data.offset, data.phase)
|
||||
sin: sin(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness),
|
||||
cos: cos(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness)
|
||||
}
|
||||
});
|
||||
nextStep += step;
|
||||
@ -99,6 +99,7 @@
|
||||
var offset = request.offset;
|
||||
var dataRateInHz = request.dataRateInHz;
|
||||
var phase = request.phase;
|
||||
var randomness = request.randomness;
|
||||
|
||||
var step = 1000 / dataRateInHz;
|
||||
var nextStep = start - (start % step) + step;
|
||||
@ -110,8 +111,8 @@
|
||||
name: request.name,
|
||||
utc: nextStep,
|
||||
yesterday: nextStep - 60*60*24*1000,
|
||||
sin: sin(nextStep, period, amplitude, offset, phase),
|
||||
cos: cos(nextStep, period, amplitude, offset, phase)
|
||||
sin: sin(nextStep, period, amplitude, offset, phase, randomness),
|
||||
cos: cos(nextStep, period, amplitude, offset, phase, randomness)
|
||||
});
|
||||
}
|
||||
self.postMessage({
|
||||
@ -120,14 +121,14 @@
|
||||
});
|
||||
}
|
||||
|
||||
function cos(timestamp, period, amplitude, offset, phase) {
|
||||
function cos(timestamp, period, amplitude, offset, phase, randomness) {
|
||||
return amplitude *
|
||||
Math.cos(phase + (timestamp / period / 1000 * Math.PI * 2)) + offset;
|
||||
Math.cos(phase + (timestamp / period / 1000 * Math.PI * 2)) + (amplitude * Math.random() * randomness) + offset;
|
||||
}
|
||||
|
||||
function sin(timestamp, period, amplitude, offset, phase) {
|
||||
function sin(timestamp, period, amplitude, offset, phase, randomness) {
|
||||
return amplitude *
|
||||
Math.sin(phase + (timestamp / period / 1000 * Math.PI * 2)) + offset;
|
||||
Math.sin(phase + (timestamp / period / 1000 * Math.PI * 2)) + (amplitude * Math.random() * randomness) + offset;
|
||||
}
|
||||
|
||||
function sendError(error, message) {
|
||||
|
@ -122,6 +122,17 @@ define([
|
||||
"telemetry",
|
||||
"phase"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Randomness",
|
||||
control: "numberfield",
|
||||
cssClass: "l-input-sm l-numeric",
|
||||
key: "randomness",
|
||||
required: true,
|
||||
property: [
|
||||
"telemetry",
|
||||
"randomness"
|
||||
]
|
||||
}
|
||||
],
|
||||
initialize: function (object) {
|
||||
@ -130,7 +141,8 @@ define([
|
||||
amplitude: 1,
|
||||
offset: 0,
|
||||
dataRateInHz: 1,
|
||||
phase: 0
|
||||
phase: 0,
|
||||
randomness: 0
|
||||
};
|
||||
}
|
||||
});
|
||||
|
@ -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.Snow());
|
||||
openmct.install(openmct.plugins.MyItems());
|
||||
openmct.install(openmct.plugins.LocalStorage());
|
||||
openmct.install(openmct.plugins.Espresso());
|
||||
openmct.install(openmct.plugins.MyItems());
|
||||
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: {
|
||||
|
18
package.json
18
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,7 +55,7 @@
|
||||
"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",
|
||||
"node-bourbon": "^4.2.3",
|
||||
@ -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);
|
||||
|
@ -33,7 +33,7 @@ define(
|
||||
var CONNECTED = {
|
||||
text: "Connected",
|
||||
glyphClass: "ok",
|
||||
statusClass: "s-status-ok",
|
||||
statusClass: "s-status-on",
|
||||
description: "Connected to the domain object database."
|
||||
},
|
||||
DISCONNECTED = {
|
||||
|
@ -71,7 +71,7 @@ define([
|
||||
},
|
||||
{
|
||||
"key": "ELASTIC_PATH",
|
||||
"value": "mct/domain_object",
|
||||
"value": "mct/_doc",
|
||||
"priority": "fallback"
|
||||
},
|
||||
{
|
||||
|
@ -32,7 +32,7 @@ define(
|
||||
var CONNECTED = {
|
||||
text: "Connected",
|
||||
glyphClass: "ok",
|
||||
statusClass: "s-status-ok",
|
||||
statusClass: "s-status-on",
|
||||
description: "Connected to the domain object database."
|
||||
},
|
||||
DISCONNECTED = {
|
||||
|
@ -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
|
@ -1,5 +1,5 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2019, 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.
|
||||
*
|
||||
@ -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());
|
||||
@ -265,6 +266,7 @@ define([
|
||||
this.install(this.plugins.ImportExport());
|
||||
this.install(this.plugins.WebPage());
|
||||
this.install(this.plugins.Condition());
|
||||
this.install(this.plugins.ConditionWidget());
|
||||
}
|
||||
|
||||
MCT.prototype = Object.create(EventEmitter.prototype);
|
||||
|
@ -81,7 +81,7 @@ define([
|
||||
function TelemetryMetadataManager(metadata) {
|
||||
this.metadata = metadata;
|
||||
|
||||
this.valueMetadatas = this.metadata.values.map(applyReasonableDefaults);
|
||||
this.valueMetadatas = this.metadata.values ? this.metadata.values.map(applyReasonableDefaults) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -81,6 +81,24 @@ define([
|
||||
return printj.sprintf(formatString, baseFormat.call(this, value));
|
||||
};
|
||||
}
|
||||
if (valueMetadata.format === 'string') {
|
||||
this.formatter.parse = function (value) {
|
||||
if (value === undefined) {
|
||||
return '';
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
return value;
|
||||
} else {
|
||||
return value.toString();
|
||||
}
|
||||
};
|
||||
this.formatter.format = function (value) {
|
||||
return value;
|
||||
};
|
||||
this.formatter.validate = function (value) {
|
||||
return typeof value === 'string';
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
TelemetryValueFormatter.prototype.parse = function (datum) {
|
||||
|
@ -21,22 +21,24 @@
|
||||
*****************************************************************************/
|
||||
|
||||
<template>
|
||||
<table class="c-table c-lad-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Timestamp</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<lad-row
|
||||
v-for="item in items"
|
||||
:key="item.key"
|
||||
:domain-object="item.domainObject"
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="c-lad-table-wrapper">
|
||||
<table class="c-table c-lad-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Timestamp</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<lad-row
|
||||
v-for="item in items"
|
||||
:key="item.key"
|
||||
:domain-object="item.domainObject"
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -30,7 +30,7 @@ define(
|
||||
// DISCONNECTED: HTTP failed; maybe misconfigured, disconnected.
|
||||
// PENDING: Still trying to connect, and haven't failed yet.
|
||||
var CONNECTED = {
|
||||
statusClass: "s-status-ok"
|
||||
statusClass: "s-status-on"
|
||||
},
|
||||
PENDING = {
|
||||
statusClass: "s-status-warning-lo"
|
||||
|
@ -122,7 +122,7 @@ define(
|
||||
it("indicates success if connection is nominal", function () {
|
||||
jasmine.clock().tick(pluginOptions.interval + 1);
|
||||
ajaxOptions.success();
|
||||
expect(indicatorElement.classList.contains('s-status-ok')).toBe(true);
|
||||
expect(indicatorElement.classList.contains('s-status-on')).toBe(true);
|
||||
});
|
||||
|
||||
it("indicates an error when the server cannot be reached", function () {
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2019, 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.
|
||||
*
|
||||
@ -20,83 +20,132 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import * as EventEmitter from 'eventemitter3';
|
||||
import EventEmitter from 'EventEmitter';
|
||||
import uuid from 'uuid';
|
||||
import TelemetryCriterion from "@/plugins/condition/criterion/TelemetryCriterion";
|
||||
import { TRIGGER } from "@/plugins/condition/utils/constants";
|
||||
import TelemetryCriterion from "./criterion/TelemetryCriterion";
|
||||
import { evaluateResults } from './utils/evaluator';
|
||||
import { getLatestTimestamp } from './utils/time';
|
||||
import AllTelemetryCriterion from "./criterion/AllTelemetryCriterion";
|
||||
|
||||
/*
|
||||
* conditionDefinition = {
|
||||
* trigger: 'any'/'all',
|
||||
* conditionConfiguration = {
|
||||
* id: uuid,
|
||||
* trigger: 'any'/'all'/'not','xor',
|
||||
* criteria: [
|
||||
* {
|
||||
* telemetry: '',
|
||||
* operation: '',
|
||||
* input: '',
|
||||
* metaDataKey: '',
|
||||
* key: 'someTelemetryObjectKey'
|
||||
* input: [],
|
||||
* metadata: ''
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
*/
|
||||
export default class ConditionClass extends EventEmitter {
|
||||
|
||||
constructor(conditionDefinition, openmct) {
|
||||
/**
|
||||
* Manages criteria and emits the result of - true or false - based on criteria evaluated.
|
||||
* @constructor
|
||||
* @param conditionConfiguration: {id: uuid,trigger: enum, criteria: Array of {id: uuid, operation: enum, input: Array, metaDataKey: string, key: {domainObject.identifier} }
|
||||
* @param openmct
|
||||
*/
|
||||
constructor(conditionConfiguration, openmct, conditionManager) {
|
||||
super();
|
||||
|
||||
this.openmct = openmct;
|
||||
this.id = this.openmct.objects.makeKeyString(conditionDefinition.identifier);
|
||||
if (conditionDefinition.criteria) {
|
||||
this.createCriteria(conditionDefinition.criteria);
|
||||
} else {
|
||||
this.criteria = [];
|
||||
this.conditionManager = conditionManager;
|
||||
this.id = conditionConfiguration.id;
|
||||
this.criteria = [];
|
||||
this.result = undefined;
|
||||
this.timeSystems = this.openmct.time.getAllTimeSystems();
|
||||
if (conditionConfiguration.configuration.criteria) {
|
||||
this.createCriteria(conditionConfiguration.configuration.criteria);
|
||||
}
|
||||
this.trigger = conditionDefinition.trigger;
|
||||
this.result = null;
|
||||
this.trigger = conditionConfiguration.configuration.trigger;
|
||||
}
|
||||
|
||||
updateTrigger(conditionDefinition) {
|
||||
if (this.trigger !== conditionDefinition.trigger) {
|
||||
this.trigger = conditionDefinition.trigger;
|
||||
this.handleConditionUpdated();
|
||||
getResult(datum) {
|
||||
if (!datum || !datum.id) {
|
||||
console.log('no data received');
|
||||
return;
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
generateCriterion(criterionDefinition) {
|
||||
return {
|
||||
id: uuid(),
|
||||
operation: criterionDefinition.operation || '',
|
||||
input: criterionDefinition.input === undefined ? [] : criterionDefinition.input,
|
||||
metaDataKey: criterionDefinition.metaDataKey || '',
|
||||
key: criterionDefinition.key || ''
|
||||
};
|
||||
isAnyOrAllTelemetry(criterion) {
|
||||
return (criterion.telemetry && (criterion.telemetry === 'all' || criterion.telemetry === 'any'));
|
||||
}
|
||||
|
||||
createCriteria(criterionDefinitions) {
|
||||
criterionDefinitions.forEach((criterionDefinition) => {
|
||||
this.addCriterion(criterionDefinition);
|
||||
isTelemetryUsed(id) {
|
||||
return this.criteria.some(criterion => {
|
||||
return this.isAnyOrAllTelemetry(criterion) || criterion.telemetryObjectIdAsString === id;
|
||||
});
|
||||
}
|
||||
|
||||
updateCriteria(criterionDefinitions) {
|
||||
update(conditionConfiguration) {
|
||||
this.updateTrigger(conditionConfiguration.configuration.trigger);
|
||||
this.updateCriteria(conditionConfiguration.configuration.criteria);
|
||||
}
|
||||
|
||||
updateTrigger(trigger) {
|
||||
if (this.trigger !== trigger) {
|
||||
this.trigger = trigger;
|
||||
}
|
||||
}
|
||||
|
||||
generateCriterion(criterionConfiguration) {
|
||||
return {
|
||||
id: criterionConfiguration.id || uuid(),
|
||||
telemetry: criterionConfiguration.telemetry || '',
|
||||
telemetryObject: this.conditionManager.telemetryObjects[this.openmct.objects.makeKeyString(criterionConfiguration.telemetry)],
|
||||
operation: criterionConfiguration.operation || '',
|
||||
input: criterionConfiguration.input === undefined ? [] : criterionConfiguration.input,
|
||||
metadata: criterionConfiguration.metadata || ''
|
||||
};
|
||||
}
|
||||
|
||||
createCriteria(criterionConfigurations) {
|
||||
criterionConfigurations.forEach((criterionConfiguration) => {
|
||||
this.addCriterion(criterionConfiguration);
|
||||
});
|
||||
}
|
||||
|
||||
updateCriteria(criterionConfigurations) {
|
||||
this.destroyCriteria();
|
||||
this.createCriteria(criterionDefinitions);
|
||||
this.createCriteria(criterionConfigurations);
|
||||
}
|
||||
|
||||
updateTelemetry() {
|
||||
this.criteria.forEach((criterion) => {
|
||||
criterion.updateTelemetry(this.conditionManager.telemetryObjects);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* adds criterion to the condition.
|
||||
*/
|
||||
addCriterion(criterionDefinition) {
|
||||
let criterionDefinitionWithId = this.generateCriterion(criterionDefinition || null);
|
||||
let criterion = new TelemetryCriterion(criterionDefinitionWithId, this.openmct);
|
||||
criterion.on('criterionUpdated', (data) => {
|
||||
this.handleCriterionUpdated(data);
|
||||
});
|
||||
addCriterion(criterionConfiguration) {
|
||||
let criterion;
|
||||
let criterionConfigurationWithId = this.generateCriterion(criterionConfiguration || null);
|
||||
if (criterionConfiguration.telemetry && (criterionConfiguration.telemetry === 'any' || criterionConfiguration.telemetry === 'all')) {
|
||||
criterion = new AllTelemetryCriterion(criterionConfigurationWithId, this.openmct);
|
||||
} else {
|
||||
criterion = new TelemetryCriterion(criterionConfigurationWithId, this.openmct);
|
||||
}
|
||||
criterion.on('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
|
||||
if (!this.criteria) {
|
||||
this.criteria = [];
|
||||
}
|
||||
this.criteria.push(criterion);
|
||||
this.handleConditionUpdated();
|
||||
return criterionDefinitionWithId.id;
|
||||
return criterionConfigurationWithId.id;
|
||||
}
|
||||
|
||||
findCriterion(id) {
|
||||
@ -114,24 +163,17 @@ export default class ConditionClass extends EventEmitter {
|
||||
return criterion;
|
||||
}
|
||||
|
||||
updateCriterion(id, criterionDefinition) {
|
||||
updateCriterion(id, criterionConfiguration) {
|
||||
let found = this.findCriterion(id);
|
||||
if (found) {
|
||||
const newCriterionDefinition = this.generateCriterion(criterionDefinition);
|
||||
let newCriterion = new TelemetryCriterion(newCriterionDefinition, this.openmct);
|
||||
const newCriterionConfiguration = this.generateCriterion(criterionConfiguration);
|
||||
let newCriterion = new TelemetryCriterion(newCriterionConfiguration, this.openmct);
|
||||
newCriterion.on('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
|
||||
|
||||
let criterion = found.item;
|
||||
criterion.unsubscribe();
|
||||
criterion.off('criterionUpdated', (result) => {
|
||||
this.handleCriterionUpdated(id, result);
|
||||
});
|
||||
criterion.off('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
|
||||
this.criteria.splice(found.index, 1, newCriterion);
|
||||
this.handleConditionUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
removeCriterion(id) {
|
||||
if (this.destroyCriterion(id)) {
|
||||
this.handleConditionUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
@ -139,31 +181,53 @@ export default class ConditionClass extends EventEmitter {
|
||||
let found = this.findCriterion(id);
|
||||
if (found) {
|
||||
let criterion = found.item;
|
||||
criterion.unsubscribe();
|
||||
criterion.off('criterionUpdated', (result) => {
|
||||
this.handleCriterionUpdated(id, result);
|
||||
criterion.off('criterionUpdated', (obj) => {
|
||||
this.handleCriterionUpdated(obj);
|
||||
});
|
||||
criterion.destroy();
|
||||
this.criteria.splice(found.index, 1);
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
handleCriterionUpdated(criterion) {
|
||||
// reevaluate the condition's output
|
||||
// TODO: should we save the result of a criterion here or in the criterion object itself?
|
||||
let found = this.findCriterion(criterion.id);
|
||||
if (found) {
|
||||
this.criteria[found.index] = criterion;
|
||||
this.handleConditionUpdated();
|
||||
this.criteria[found.index] = criterion.data;
|
||||
}
|
||||
// this.handleConditionUpdated();
|
||||
}
|
||||
|
||||
handleConditionUpdated() {
|
||||
console.log(this);
|
||||
// trigger an updated event so that consumers can react accordingly
|
||||
this.emitResult();
|
||||
requestLADConditionResult() {
|
||||
let latestTimestamp;
|
||||
let criteriaResults = {};
|
||||
const criteriaRequests = this.criteria
|
||||
.map(criterion => criterion.requestLAD(this.conditionManager.telemetryObjects));
|
||||
|
||||
return Promise.all(criteriaRequests)
|
||||
.then(results => {
|
||||
results.forEach(resultObj => {
|
||||
const { id, data, data: { result } } = resultObj;
|
||||
if (this.findCriterion(id)) {
|
||||
criteriaResults[id] = !!result;
|
||||
}
|
||||
latestTimestamp = getLatestTimestamp(
|
||||
latestTimestamp,
|
||||
data,
|
||||
this.timeSystems,
|
||||
this.openmct.time.timeSystem()
|
||||
);
|
||||
});
|
||||
return {
|
||||
id: this.id,
|
||||
data: Object.assign(
|
||||
{},
|
||||
latestTimestamp,
|
||||
{ result: evaluateResults(Object.values(criteriaResults), this.trigger) }
|
||||
)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
getCriteria() {
|
||||
@ -179,20 +243,7 @@ export default class ConditionClass extends EventEmitter {
|
||||
return success;
|
||||
}
|
||||
|
||||
//TODO: implement as part of the evaluator class task.
|
||||
evaluate() {
|
||||
if (this.trigger === TRIGGER.ANY) {
|
||||
this.result = false;
|
||||
} else if (this.trigger === TRIGGER.ALL) {
|
||||
this.result = false;
|
||||
}
|
||||
}
|
||||
|
||||
emitResult() {
|
||||
this.emit('conditionUpdated', {
|
||||
id: this.id,
|
||||
trigger: this.trigger,
|
||||
criteria: this.criteria
|
||||
});
|
||||
destroy() {
|
||||
this.destroyCriteria();
|
||||
}
|
||||
}
|
||||
|
372
src/plugins/condition/ConditionManager.js
Normal file
372
src/plugins/condition/ConditionManager.js
Normal file
@ -0,0 +1,372 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
import Condition from "./Condition";
|
||||
import { getLatestTimestamp } from './utils/time';
|
||||
import uuid from "uuid";
|
||||
import EventEmitter from 'EventEmitter';
|
||||
|
||||
export default class ConditionManager extends EventEmitter {
|
||||
constructor(conditionSetDomainObject, openmct) {
|
||||
super();
|
||||
this.openmct = openmct;
|
||||
this.conditionSetDomainObject = conditionSetDomainObject;
|
||||
this.timeSystems = this.openmct.time.getAllTimeSystems();
|
||||
this.composition = this.openmct.composition.get(conditionSetDomainObject);
|
||||
this.composition.on('add', this.subscribeToTelemetry, this);
|
||||
this.composition.on('remove', this.unsubscribeFromTelemetry, this);
|
||||
this.compositionLoad = this.composition.load();
|
||||
this.subscriptions = {};
|
||||
this.telemetryObjects = {};
|
||||
this.testData = {conditionTestData: [], applied: false};
|
||||
this.initialize();
|
||||
|
||||
this.stopObservingForChanges = this.openmct.objects.observe(this.conditionSetDomainObject, '*', (newDomainObject) => {
|
||||
this.conditionSetDomainObject = newDomainObject;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
subscribeToTelemetry(endpoint) {
|
||||
const id = this.openmct.objects.makeKeyString(endpoint.identifier);
|
||||
if (this.subscriptions[id]) {
|
||||
console.log('subscription already exists');
|
||||
return;
|
||||
}
|
||||
this.telemetryObjects[id] = Object.assign({}, endpoint, {telemetryMetaData: this.openmct.telemetry.getMetadata(endpoint).valueMetadatas});
|
||||
this.subscriptions[id] = this.openmct.telemetry.subscribe(
|
||||
endpoint,
|
||||
this.telemetryReceived.bind(this, endpoint)
|
||||
);
|
||||
// TODO check if this is needed
|
||||
this.updateConditionTelemetry();
|
||||
}
|
||||
|
||||
unsubscribeFromTelemetry(endpointIdentifier) {
|
||||
const id = this.openmct.objects.makeKeyString(endpointIdentifier);
|
||||
if (!this.subscriptions[id]) {
|
||||
console.log('no subscription to remove');
|
||||
return;
|
||||
}
|
||||
|
||||
this.subscriptions[id]();
|
||||
delete this.subscriptions[id];
|
||||
delete this.telemetryObjects[id];
|
||||
this.removeConditionTelemetry();
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.conditionClassCollection = [];
|
||||
if (this.conditionSetDomainObject.configuration.conditionCollection.length) {
|
||||
this.conditionSetDomainObject.configuration.conditionCollection.forEach((conditionConfiguration, index) => {
|
||||
this.initCondition(conditionConfiguration, index);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
updateConditionTelemetry() {
|
||||
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);
|
||||
this.conditionSetDomainObject.configuration.conditionCollection[index] = conditionConfiguration;
|
||||
this.persistConditions();
|
||||
}
|
||||
|
||||
initCondition(conditionConfiguration, index) {
|
||||
let condition = new Condition(conditionConfiguration, this.openmct, this);
|
||||
if (index !== undefined) {
|
||||
this.conditionClassCollection.splice(index + 1, 0, condition);
|
||||
} else {
|
||||
this.conditionClassCollection.unshift(condition);
|
||||
}
|
||||
}
|
||||
|
||||
createCondition(conditionConfiguration) {
|
||||
let conditionObj;
|
||||
if (conditionConfiguration) {
|
||||
conditionObj = {
|
||||
...conditionConfiguration,
|
||||
id: uuid(),
|
||||
configuration: {
|
||||
...conditionConfiguration.configuration,
|
||||
name: `Copy of ${conditionConfiguration.configuration.name}`
|
||||
}
|
||||
};
|
||||
} else {
|
||||
conditionObj = {
|
||||
id: uuid(),
|
||||
configuration: {
|
||||
name: 'Unnamed Condition',
|
||||
output: 'false',
|
||||
trigger: 'all',
|
||||
criteria: [{
|
||||
id: uuid(),
|
||||
telemetry: '',
|
||||
operation: '',
|
||||
input: [],
|
||||
metadata: ''
|
||||
}]
|
||||
},
|
||||
summary: ''
|
||||
};
|
||||
}
|
||||
|
||||
return conditionObj;
|
||||
}
|
||||
|
||||
addCondition() {
|
||||
this.createAndSaveCondition();
|
||||
}
|
||||
|
||||
cloneCondition(conditionConfiguration, index) {
|
||||
let clonedConfig = JSON.parse(JSON.stringify(conditionConfiguration));
|
||||
clonedConfig.configuration.criteria.forEach((criterion) => criterion.id = uuid());
|
||||
this.createAndSaveCondition(index, clonedConfig);
|
||||
}
|
||||
|
||||
createAndSaveCondition(index, conditionConfiguration) {
|
||||
const newCondition = this.createCondition(conditionConfiguration);
|
||||
if (index !== undefined) {
|
||||
this.conditionSetDomainObject.configuration.conditionCollection.splice(index + 1, 0, newCondition);
|
||||
} else {
|
||||
this.conditionSetDomainObject.configuration.conditionCollection.unshift(newCondition);
|
||||
}
|
||||
this.initCondition(newCondition, index);
|
||||
this.persistConditions();
|
||||
}
|
||||
|
||||
removeCondition(index) {
|
||||
let condition = this.conditionClassCollection[index];
|
||||
condition.destroy();
|
||||
this.conditionClassCollection.splice(index, 1);
|
||||
this.conditionSetDomainObject.configuration.conditionCollection.splice(index, 1);
|
||||
this.persistConditions();
|
||||
}
|
||||
|
||||
findConditionById(id) {
|
||||
return this.conditionClassCollection.find(conditionClass => conditionClass.id === id);
|
||||
}
|
||||
|
||||
reorderConditions(reorderPlan) {
|
||||
let oldConditions = Array.from(this.conditionSetDomainObject.configuration.conditionCollection);
|
||||
let newCollection = [];
|
||||
reorderPlan.forEach((reorderEvent) => {
|
||||
let item = oldConditions[reorderEvent.oldIndex];
|
||||
newCollection.push(item);
|
||||
this.conditionSetDomainObject.configuration.conditionCollection = newCollection;
|
||||
});
|
||||
this.persistConditions();
|
||||
}
|
||||
|
||||
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];
|
||||
|
||||
for (let i = 0; i < conditionCollection.length - 1; i++) {
|
||||
if (conditionResults[conditionCollection[i].id]) {
|
||||
//first condition to be true wins
|
||||
currentCondition = conditionCollection[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
return currentCondition;
|
||||
}
|
||||
|
||||
requestLADConditionSetOutput() {
|
||||
if (!this.conditionClassCollection.length) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
return this.compositionLoad.then(() => {
|
||||
let latestTimestamp;
|
||||
let conditionResults = {};
|
||||
const conditionRequests = this.conditionClassCollection
|
||||
.map(condition => condition.requestLADConditionResult());
|
||||
|
||||
return Promise.all(conditionRequests)
|
||||
.then((results) => {
|
||||
results.forEach(resultObj => {
|
||||
const { id, data, data: { result } } = resultObj;
|
||||
if (this.findConditionById(id)) {
|
||||
conditionResults[id] = !!result;
|
||||
}
|
||||
latestTimestamp = getLatestTimestamp(
|
||||
latestTimestamp,
|
||||
data,
|
||||
this.timeSystems,
|
||||
this.openmct.time.timeSystem()
|
||||
);
|
||||
});
|
||||
|
||||
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,
|
||||
conditionId: currentCondition.id
|
||||
},
|
||||
latestTimestamp
|
||||
);
|
||||
|
||||
return [currentOutput];
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
let data = undefined;
|
||||
if (this.testData.applied) {
|
||||
const found = this.testData.conditionTestInputs.find((testInput) => (testInput.metadata === metadatum.source));
|
||||
if (found) {
|
||||
data = found.value;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
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);
|
||||
datum[metadatum.key] = testValue !== undefined ? formatter.parse(testValue) : formatter.parse(telemetryDatum[metadatum.source]);
|
||||
return datum;
|
||||
}, {});
|
||||
|
||||
normalizedDatum.id = id;
|
||||
|
||||
return normalizedDatum;
|
||||
}
|
||||
|
||||
updateTestData(testData) {
|
||||
this.testData = testData;
|
||||
this.openmct.objects.mutate(this.conditionSetDomainObject, 'configuration.conditionTestData', this.testData.conditionTestInputs);
|
||||
}
|
||||
|
||||
persistConditions() {
|
||||
this.openmct.objects.mutate(this.conditionSetDomainObject, 'configuration.conditionCollection', this.conditionSetDomainObject.configuration.conditionCollection);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.composition.off('add', this.subscribeToTelemetry, this);
|
||||
this.composition.off('remove', this.unsubscribeFromTelemetry, this);
|
||||
Object.values(this.subscriptions).forEach(unsubscribe => unsubscribe());
|
||||
delete this.subscriptions;
|
||||
|
||||
if(this.stopObservingForChanges) {
|
||||
this.stopObservingForChanges();
|
||||
}
|
||||
|
||||
this.conditionClassCollection.forEach((condition) => {
|
||||
condition.destroy();
|
||||
})
|
||||
}
|
||||
}
|
133
src/plugins/condition/ConditionManagerSpec.js
Normal file
133
src/plugins/condition/ConditionManagerSpec.js
Normal file
@ -0,0 +1,133 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
import ConditionManager from './ConditionManager';
|
||||
|
||||
describe('ConditionManager', () => {
|
||||
|
||||
let conditionMgr;
|
||||
let mockListener;
|
||||
let openmct = {};
|
||||
let mockCondition = {
|
||||
isDefault: true,
|
||||
id: '1234-5678',
|
||||
configuration: {
|
||||
criteria: []
|
||||
}
|
||||
};
|
||||
let conditionSetDomainObject = {
|
||||
identifier: {
|
||||
namespace: "",
|
||||
key: "600a7372-8d48-4dc4-98b6-548611b1ff7e"
|
||||
},
|
||||
type: "conditionSet",
|
||||
location: "mine",
|
||||
configuration: {
|
||||
conditionCollection: [
|
||||
mockCondition
|
||||
]
|
||||
}
|
||||
};
|
||||
let mockComposition;
|
||||
let loader;
|
||||
let mockTimeSystems;
|
||||
|
||||
function mockAngularComponents() {
|
||||
let mockInjector = jasmine.createSpyObj('$injector', ['get']);
|
||||
|
||||
let mockInstantiate = jasmine.createSpy('mockInstantiate');
|
||||
mockInstantiate.and.returnValue(mockInstantiate);
|
||||
|
||||
let mockDomainObject = {
|
||||
useCapability: function () {
|
||||
return mockCondition;
|
||||
}
|
||||
};
|
||||
mockInstantiate.and.callFake(function () {
|
||||
return mockDomainObject;
|
||||
});
|
||||
mockInjector.get.and.callFake(function (service) {
|
||||
return {
|
||||
'instantiate': mockInstantiate
|
||||
}[service];
|
||||
});
|
||||
|
||||
openmct.$injector = mockInjector;
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
|
||||
mockAngularComponents();
|
||||
mockListener = jasmine.createSpy('mockListener');
|
||||
loader = {};
|
||||
loader.promise = new Promise(function (resolve, reject) {
|
||||
loader.resolve = resolve;
|
||||
loader.reject = reject;
|
||||
});
|
||||
|
||||
mockComposition = jasmine.createSpyObj('compositionCollection', [
|
||||
'load',
|
||||
'on',
|
||||
'off'
|
||||
]);
|
||||
mockComposition.load.and.callFake(() => {
|
||||
setTimeout(() => {
|
||||
loader.resolve();
|
||||
});
|
||||
return loader.promise;
|
||||
});
|
||||
mockComposition.on('add', mockListener);
|
||||
mockComposition.on('remove', mockListener);
|
||||
openmct.composition = jasmine.createSpyObj('compositionAPI', [
|
||||
'get'
|
||||
]);
|
||||
openmct.composition.get.and.returnValue(mockComposition);
|
||||
|
||||
openmct.objects = jasmine.createSpyObj('objects', ['get', 'makeKeyString', 'observe', 'mutate']);
|
||||
openmct.objects.get.and.returnValues(new Promise(function (resolve, reject) {
|
||||
resolve(conditionSetDomainObject);
|
||||
}), new Promise(function (resolve, reject) {
|
||||
resolve(mockCondition);
|
||||
}));
|
||||
openmct.objects.makeKeyString.and.returnValue(conditionSetDomainObject.identifier.key);
|
||||
openmct.objects.observe.and.returnValue(function () {});
|
||||
openmct.objects.mutate.and.returnValue(function () {});
|
||||
|
||||
mockTimeSystems = {
|
||||
key: 'utc'
|
||||
};
|
||||
openmct.time = jasmine.createSpyObj('time', ['getAllTimeSystems']);
|
||||
openmct.time.getAllTimeSystems.and.returnValue([mockTimeSystems]);
|
||||
|
||||
conditionMgr = new ConditionManager(conditionSetDomainObject, openmct);
|
||||
|
||||
conditionMgr.on('conditionSetResultUpdated', mockListener);
|
||||
conditionMgr.on('telemetryReceived', mockListener);
|
||||
});
|
||||
|
||||
it('creates a conditionCollection with a default condition', function () {
|
||||
expect(conditionMgr.conditionSetDomainObject.configuration.conditionCollection.length).toEqual(1);
|
||||
let defaultConditionId = conditionMgr.conditionClassCollection[0].id;
|
||||
expect(defaultConditionId).toEqual(mockCondition.id);
|
||||
});
|
||||
|
||||
});
|
@ -1,5 +1,5 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2019, 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.
|
||||
*
|
||||
@ -23,8 +23,7 @@
|
||||
export default function ConditionSetCompositionPolicy(openmct) {
|
||||
return {
|
||||
allow: function (parent, child) {
|
||||
let parentType = parent.type;
|
||||
if (parentType === 'conditionSet' && !openmct.telemetry.isTelemetryObject(child)) {
|
||||
if (parent.type === 'conditionSet' && !openmct.telemetry.isTelemetryObject(child)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2019, 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.
|
||||
*
|
||||
@ -22,13 +22,12 @@
|
||||
|
||||
import ConditionSetCompositionPolicy from './ConditionSetCompositionPolicy';
|
||||
|
||||
fdescribe('ConditionSetCompositionPolicy', () => {
|
||||
describe('ConditionSetCompositionPolicy', () => {
|
||||
|
||||
let policy;
|
||||
let testTelemetryObject;
|
||||
let openmct = {};
|
||||
let parentDomainObject;
|
||||
let composition;
|
||||
|
||||
beforeAll(function () {
|
||||
testTelemetryObject = {
|
||||
@ -57,7 +56,6 @@ fdescribe('ConditionSetCompositionPolicy', () => {
|
||||
openmct.telemetry = jasmine.createSpyObj('telemetry', ['isTelemetryObject']);
|
||||
policy = new ConditionSetCompositionPolicy(openmct);
|
||||
parentDomainObject = {};
|
||||
composition = {};
|
||||
});
|
||||
|
||||
it('returns true for object types that are not conditionSets', function () {
|
||||
|
77
src/plugins/condition/ConditionSetMetadataProvider.js
Normal file
77
src/plugins/condition/ConditionSetMetadataProvider.js
Normal file
@ -0,0 +1,77 @@
|
||||
/*****************************************************************************
|
||||
* 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 ConditionSetMetadataProvider {
|
||||
constructor(openmct) {
|
||||
this.openmct = openmct;
|
||||
}
|
||||
|
||||
supportsMetadata(domainObject) {
|
||||
return domainObject.type === 'conditionSet';
|
||||
}
|
||||
|
||||
getDomains(domainObject) {
|
||||
return this.openmct.time.getAllTimeSystems().map(function (ts, i) {
|
||||
return {
|
||||
key: ts.key,
|
||||
name: ts.name,
|
||||
format: ts.timeFormat,
|
||||
hints: {
|
||||
domain: i
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
getMetadata(domainObject) {
|
||||
const enumerations = domainObject.configuration.conditionCollection
|
||||
.map((condition, index) => {
|
||||
return {
|
||||
string: condition.configuration.output,
|
||||
value: index
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
values: this.getDomains().concat([
|
||||
{
|
||||
key: "state",
|
||||
source: "output",
|
||||
name: "State",
|
||||
format: "enum",
|
||||
enumerations: enumerations,
|
||||
hints: {
|
||||
range: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "output",
|
||||
name: "Value",
|
||||
format: "string",
|
||||
hints: {
|
||||
range: 2
|
||||
}
|
||||
}
|
||||
])
|
||||
};
|
||||
}
|
||||
}
|
86
src/plugins/condition/ConditionSetTelemetryProvider.js
Normal file
86
src/plugins/condition/ConditionSetTelemetryProvider.js
Normal file
@ -0,0 +1,86 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
import ConditionManager from './ConditionManager'
|
||||
|
||||
export default class ConditionSetTelemetryProvider {
|
||||
constructor(openmct) {
|
||||
this.openmct = openmct;
|
||||
this.conditionManagerPool = {};
|
||||
}
|
||||
|
||||
isTelemetryObject(domainObject) {
|
||||
return domainObject.type === 'conditionSet';
|
||||
}
|
||||
|
||||
supportsRequest(domainObject) {
|
||||
return domainObject.type === 'conditionSet';
|
||||
}
|
||||
|
||||
supportsSubscribe(domainObject) {
|
||||
return domainObject.type === 'conditionSet';
|
||||
}
|
||||
|
||||
request(domainObject) {
|
||||
let conditionManager = this.getConditionManager(domainObject);
|
||||
|
||||
return conditionManager.requestLADConditionSetOutput()
|
||||
.then(latestOutput => {
|
||||
return latestOutput;
|
||||
});
|
||||
}
|
||||
|
||||
subscribe(domainObject, callback) {
|
||||
let conditionManager = this.getConditionManager(domainObject);
|
||||
|
||||
conditionManager.on('conditionSetResultUpdated', callback);
|
||||
|
||||
return this.destroyConditionManager.bind(this, this.openmct.objects.makeKeyString(domainObject.identifier));
|
||||
}
|
||||
|
||||
/**
|
||||
* returns conditionManager instance for corresponding domain object
|
||||
* creates the instance if it is not yet created
|
||||
* @private
|
||||
*/
|
||||
getConditionManager(domainObject) {
|
||||
const id = this.openmct.objects.makeKeyString(domainObject.identifier);
|
||||
|
||||
if (!this.conditionManagerPool[id]) {
|
||||
this.conditionManagerPool[id] = new ConditionManager(domainObject, this.openmct);
|
||||
}
|
||||
|
||||
return this.conditionManagerPool[id];
|
||||
}
|
||||
|
||||
/**
|
||||
* cleans up and destroys conditionManager instance for corresponding domain object id
|
||||
* can be called manually for views that only request but do not subscribe to data
|
||||
*/
|
||||
destroyConditionManager(id) {
|
||||
if (this.conditionManagerPool[id]) {
|
||||
this.conditionManagerPool[id].off('conditionSetResultUpdated');
|
||||
this.conditionManagerPool[id].destroy();
|
||||
delete this.conditionManagerPool[id];
|
||||
}
|
||||
}
|
||||
}
|
33
src/plugins/condition/ConditionSetViewPolicy.js
Normal file
33
src/plugins/condition/ConditionSetViewPolicy.js
Normal file
@ -0,0 +1,33 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
function ConditionSetViewPolicy() {
|
||||
}
|
||||
|
||||
ConditionSetViewPolicy.prototype.allow = function (view, domainObject) {
|
||||
if (domainObject.getModel().type === 'conditionSet') {
|
||||
return view.key === 'conditionSet.view';
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export default ConditionSetViewPolicy;
|
@ -1,5 +1,5 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2019, 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.
|
||||
*
|
||||
@ -23,11 +23,14 @@
|
||||
import ConditionSet from './components/ConditionSet.vue';
|
||||
import Vue from 'vue';
|
||||
|
||||
const DEFAULT_VIEW_PRIORITY = 100;
|
||||
|
||||
export default class ConditionSetViewProvider {
|
||||
constructor(openmct) {
|
||||
this.openmct = openmct;
|
||||
this.name = 'Conditions View';
|
||||
this.key = 'conditionSet.view';
|
||||
this.cssClass = 'icon-summary-widget'; // TODO: replace with class for new icon
|
||||
this.cssClass = 'icon-conditional';
|
||||
}
|
||||
|
||||
canView(domainObject) {
|
||||
@ -58,7 +61,7 @@ export default class ConditionSetViewProvider {
|
||||
isEditing
|
||||
}
|
||||
},
|
||||
template: '<condition-set ref="conditionSet" :isEditing="isEditing"></condition-set>'
|
||||
template: '<condition-set :isEditing="isEditing"></condition-set>'
|
||||
});
|
||||
},
|
||||
onEditModeChange: (isEditing) => {
|
||||
@ -70,4 +73,12 @@ export default class ConditionSetViewProvider {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
priority(domainObject) {
|
||||
if (domainObject.type === 'conditionSet') {
|
||||
return Number.MAX_VALUE;
|
||||
} else {
|
||||
return DEFAULT_VIEW_PRIORITY;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2019, 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.
|
||||
*
|
||||
@ -25,15 +25,22 @@ import {TRIGGER} from "./utils/constants";
|
||||
import TelemetryCriterion from "./criterion/TelemetryCriterion";
|
||||
|
||||
let openmct = {},
|
||||
mockListener,
|
||||
testConditionDefinition,
|
||||
testTelemetryObject,
|
||||
conditionObj;
|
||||
conditionObj,
|
||||
conditionManager,
|
||||
mockTelemetryReceived,
|
||||
mockTimeSystems;
|
||||
|
||||
describe("The condition", function () {
|
||||
|
||||
beforeEach (() => {
|
||||
mockListener = jasmine.createSpy('listener');
|
||||
conditionManager = jasmine.createSpyObj('conditionManager',
|
||||
['on']
|
||||
);
|
||||
mockTelemetryReceived = jasmine.createSpy('listener');
|
||||
conditionManager.on('telemetryReceived', mockTelemetryReceived);
|
||||
|
||||
testTelemetryObject = {
|
||||
identifier:{ namespace: "", key: "test-object"},
|
||||
type: "test-object",
|
||||
@ -54,42 +61,57 @@ describe("The condition", function () {
|
||||
}]
|
||||
}
|
||||
};
|
||||
conditionManager.telemetryObjects = {
|
||||
"test-object": testTelemetryObject
|
||||
};
|
||||
openmct.objects = jasmine.createSpyObj('objects', ['get', 'makeKeyString']);
|
||||
openmct.objects.get.and.returnValue(testTelemetryObject);
|
||||
openmct.objects.makeKeyString.and.returnValue(testTelemetryObject.identifier.key);
|
||||
openmct.telemetry = jasmine.createSpyObj('telemetry', ['isTelemetryObject', 'subscribe']);
|
||||
openmct.objects.get.and.returnValue(new Promise(function (resolve, reject) {
|
||||
resolve(testTelemetryObject);
|
||||
})); openmct.objects.makeKeyString.and.returnValue(testTelemetryObject.identifier.key);
|
||||
openmct.telemetry = jasmine.createSpyObj('telemetry', ['isTelemetryObject', 'subscribe', 'getMetadata']);
|
||||
openmct.telemetry.isTelemetryObject.and.returnValue(true);
|
||||
openmct.telemetry.subscribe.and.returnValue(function () {});
|
||||
openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry.values);
|
||||
|
||||
mockTimeSystems = {
|
||||
key: 'utc'
|
||||
};
|
||||
openmct.time = jasmine.createSpyObj('time', ['getAllTimeSystems']);
|
||||
openmct.time.getAllTimeSystems.and.returnValue([mockTimeSystems]);
|
||||
|
||||
testConditionDefinition = {
|
||||
trigger: TRIGGER.ANY,
|
||||
criteria: [
|
||||
{
|
||||
operation: 'equalTo',
|
||||
input: false,
|
||||
metaDataKey: 'value',
|
||||
key: testTelemetryObject.identifier
|
||||
}
|
||||
]
|
||||
id: '123-456',
|
||||
configuration: {
|
||||
name: 'mock condition',
|
||||
output: 'mock output',
|
||||
trigger: TRIGGER.ANY,
|
||||
criteria: [
|
||||
{
|
||||
id: '1234-5678-9999-0000',
|
||||
operation: 'equalTo',
|
||||
input: ['0'],
|
||||
metadata: 'value',
|
||||
telemetry: testTelemetryObject.identifier
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
conditionObj = new Condition(
|
||||
testConditionDefinition,
|
||||
openmct
|
||||
openmct,
|
||||
conditionManager
|
||||
);
|
||||
|
||||
conditionObj.on('conditionUpdated', mockListener);
|
||||
|
||||
});
|
||||
|
||||
it("generates criteria with an id", function () {
|
||||
const testCriterion = testConditionDefinition.criteria[0];
|
||||
it("generates criteria with the correct properties", function () {
|
||||
const testCriterion = testConditionDefinition.configuration.criteria[0];
|
||||
let criterion = conditionObj.generateCriterion(testCriterion);
|
||||
expect(criterion.id).toBeDefined();
|
||||
expect(criterion.operation).toEqual(testCriterion.operation);
|
||||
expect(criterion.input).toEqual(testCriterion.input);
|
||||
expect(criterion.metaDataKey).toEqual(testCriterion.metaDataKey);
|
||||
expect(criterion.key).toEqual(testCriterion.key);
|
||||
expect(criterion.metadata).toEqual(testCriterion.metadata);
|
||||
expect(criterion.telemetry).toEqual(testCriterion.telemetry);
|
||||
});
|
||||
|
||||
it("initializes with an id", function () {
|
||||
@ -100,14 +122,13 @@ describe("The condition", function () {
|
||||
expect(conditionObj.criteria.length).toEqual(1);
|
||||
let criterion = conditionObj.criteria[0];
|
||||
expect(criterion instanceof TelemetryCriterion).toBeTrue();
|
||||
expect(criterion.operator).toEqual(testConditionDefinition.operator);
|
||||
expect(criterion.input).toEqual(testConditionDefinition.input);
|
||||
expect(criterion.metaDataKey).toEqual(testConditionDefinition.metaDataKey);
|
||||
expect(criterion.key).toEqual(testConditionDefinition.key);
|
||||
expect(criterion.operator).toEqual(testConditionDefinition.configuration.criteria[0].operator);
|
||||
expect(criterion.input).toEqual(testConditionDefinition.configuration.criteria[0].input);
|
||||
expect(criterion.metadata).toEqual(testConditionDefinition.configuration.criteria[0].metadata);
|
||||
});
|
||||
|
||||
it("initializes with the trigger from the condition definition", function () {
|
||||
expect(conditionObj.trigger).toEqual(testConditionDefinition.trigger);
|
||||
expect(conditionObj.trigger).toEqual(testConditionDefinition.configuration.trigger);
|
||||
});
|
||||
|
||||
it("destroys all criteria for a condition", function () {
|
||||
|
@ -0,0 +1,163 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
import EventEmitter from 'EventEmitter';
|
||||
|
||||
export default class StyleRuleManager extends EventEmitter {
|
||||
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) {
|
||||
this.subscribeToConditionSet();
|
||||
} else {
|
||||
this.applyStaticStyle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 || []);
|
||||
}
|
||||
|
||||
subscribeToConditionSet() {
|
||||
if (this.stopProvidingTelemetry) {
|
||||
this.stopProvidingTelemetry();
|
||||
}
|
||||
this.openmct.objects.get(this.conditionSetIdentifier).then((conditionSetDomainObject) => {
|
||||
this.openmct.telemetry.request(conditionSetDomainObject)
|
||||
.then(output => {
|
||||
if (output && output.length) {
|
||||
this.handleConditionSetResultUpdated(output[0]);
|
||||
}
|
||||
});
|
||||
this.stopProvidingTelemetry = this.openmct.telemetry.subscribe(conditionSetDomainObject, this.handleConditionSetResultUpdated.bind(this));
|
||||
});
|
||||
}
|
||||
|
||||
updateObjectStyleConfig(styleConfiguration) {
|
||||
if (!styleConfiguration || !styleConfiguration.conditionSetIdentifier) {
|
||||
this.initialize(styleConfiguration || {});
|
||||
this.destroy();
|
||||
} else {
|
||||
let isNewConditionSet = !this.conditionSetIdentifier ||
|
||||
!this.openmct.objects.areIdsEqual(this.conditionSetIdentifier, styleConfiguration.conditionSetIdentifier);
|
||||
this.initialize(styleConfiguration);
|
||||
if (this.isEditing) {
|
||||
this.applySelectedConditionStyle();
|
||||
} else {
|
||||
//Only resubscribe if the conditionSet has changed.
|
||||
if (isNewConditionSet) {
|
||||
this.subscribeToConditionSet();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateConditionStylesMap(conditionStyles) {
|
||||
let conditionStyleMap = {};
|
||||
conditionStyles.forEach((conditionStyle) => {
|
||||
if (conditionStyle.conditionId) {
|
||||
conditionStyleMap[conditionStyle.conditionId] = conditionStyle.style;
|
||||
} else {
|
||||
conditionStyleMap.static = conditionStyle.style;
|
||||
}
|
||||
});
|
||||
this.conditionalStyleMap = conditionStyleMap;
|
||||
}
|
||||
|
||||
handleConditionSetResultUpdated(resultData) {
|
||||
let foundStyle = this.conditionalStyleMap[resultData.conditionId];
|
||||
if (foundStyle) {
|
||||
if (foundStyle !== this.currentStyle) {
|
||||
this.currentStyle = foundStyle;
|
||||
}
|
||||
this.updateDomainObjectStyle();
|
||||
} else {
|
||||
this.applyStaticStyle();
|
||||
}
|
||||
}
|
||||
|
||||
updateDomainObjectStyle() {
|
||||
if (this.callback) {
|
||||
this.callback(Object.assign({}, this.currentStyle));
|
||||
}
|
||||
}
|
||||
|
||||
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] = '__no_value';
|
||||
});
|
||||
}
|
||||
}
|
||||
this.updateDomainObjectStyle();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.applyStaticStyle();
|
||||
if (this.stopProvidingTelemetry) {
|
||||
this.stopProvidingTelemetry();
|
||||
}
|
||||
delete this.stopProvidingTelemetry;
|
||||
this.conditionSetIdentifier = undefined;
|
||||
this.isEditing = undefined;
|
||||
this.callback = undefined;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,39 +1,371 @@
|
||||
/*****************************************************************************
|
||||
* 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 id="conditionArea"
|
||||
class="c-cs-ui__conditions"
|
||||
:class="['widget-condition', { 'widget-condition--current': condition.isCurrent }]"
|
||||
<div class="c-condition-h"
|
||||
:class="{ 'is-drag-target': draggingOver }"
|
||||
@dragover.prevent
|
||||
@drop.prevent="dropCondition($event, conditionIndex)"
|
||||
@dragenter="dragEnter($event, conditionIndex)"
|
||||
@dragleave="dragLeave($event, conditionIndex)"
|
||||
>
|
||||
<div class="title-bar">
|
||||
<span class="condition-name">
|
||||
{{ condition.name }}
|
||||
</span>
|
||||
<span class="condition-output">
|
||||
Output: {{ condition.output }}
|
||||
</span>
|
||||
<div class="c-condition-h__drop-target"></div>
|
||||
<div v-if="isEditing"
|
||||
class="c-condition c-condition--edit"
|
||||
>
|
||||
<!-- Edit view -->
|
||||
<div class="c-condition__header">
|
||||
<span class="c-condition__drag-grippy c-grippy c-grippy--vertical-drag"
|
||||
title="Drag to reorder conditions"
|
||||
:class="[{ 'is-enabled': !condition.isDefault }, { 'hide-nice': condition.isDefault }]"
|
||||
:draggable="!condition.isDefault"
|
||||
@dragstart="dragStart"
|
||||
@dragend="dragEnd"
|
||||
></span>
|
||||
|
||||
<span class="c-condition__disclosure c-disclosure-triangle c-tree__item__view-control is-enabled"
|
||||
:class="{ 'c-disclosure-triangle--expanded': expanded }"
|
||||
@click="expanded = !expanded"
|
||||
></span>
|
||||
|
||||
<span class="c-condition__name">{{ condition.configuration.name }}</span>
|
||||
<span class="c-condition__summary">
|
||||
<template v-if="!canEvaluateCriteria">
|
||||
Define criteria
|
||||
</template>
|
||||
<span v-else>
|
||||
<condition-description :show-label="false"
|
||||
:condition="condition"
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<div class="c-condition__buttons">
|
||||
<button v-if="!condition.isDefault"
|
||||
class="c-click-icon c-condition__duplicate-button icon-duplicate"
|
||||
title="Duplicate this condition"
|
||||
@click="cloneCondition"
|
||||
></button>
|
||||
|
||||
<button v-if="!condition.isDefault"
|
||||
class="c-click-icon c-condition__delete-button icon-trash"
|
||||
title="Delete this condition"
|
||||
@click="removeCondition"
|
||||
></button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="expanded"
|
||||
class="c-condition__definition c-cdef"
|
||||
>
|
||||
<span class="c-cdef__separator c-row-separator"></span>
|
||||
<span class="c-cdef__label">Condition Name</span>
|
||||
<span class="c-cdef__controls">
|
||||
<input v-model="condition.configuration.name"
|
||||
class="t-condition-input__name"
|
||||
type="text"
|
||||
@change="persist"
|
||||
>
|
||||
</span>
|
||||
|
||||
<span class="c-cdef__label">Output</span>
|
||||
<span class="c-cdef__controls">
|
||||
<span class="c-cdef__control">
|
||||
<select v-model="selectedOutputSelection"
|
||||
@change="setOutputValue"
|
||||
>
|
||||
<option v-for="option in outputOptions"
|
||||
:key="option"
|
||||
:value="option"
|
||||
>
|
||||
{{ initCap(option) }}
|
||||
</option>
|
||||
</select>
|
||||
</span>
|
||||
<span class="c-cdef__control">
|
||||
<input v-if="selectedOutputSelection === outputOptions[2]"
|
||||
v-model="condition.configuration.output"
|
||||
class="t-condition-name-input"
|
||||
type="text"
|
||||
@change="persist"
|
||||
>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<div v-if="!condition.isDefault"
|
||||
class="c-cdef__match-and-criteria"
|
||||
>
|
||||
<span class="c-cdef__separator c-row-separator"></span>
|
||||
<span class="c-cdef__label">Match</span>
|
||||
<span class="c-cdef__controls">
|
||||
<select v-model="condition.configuration.trigger"
|
||||
@change="persist"
|
||||
>
|
||||
<option v-for="option in triggers"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
> {{ option.label }}</option>
|
||||
</select>
|
||||
</span>
|
||||
|
||||
<template v-if="telemetry.length || condition.configuration.criteria.length">
|
||||
<div v-for="(criterion, index) in condition.configuration.criteria"
|
||||
:key="criterion.id"
|
||||
class="c-cdef__criteria"
|
||||
>
|
||||
<Criterion :telemetry="telemetry"
|
||||
:criterion="criterion"
|
||||
:index="index"
|
||||
:trigger="condition.configuration.trigger"
|
||||
:is-default="condition.configuration.criteria.length === 1"
|
||||
@persist="persist"
|
||||
/>
|
||||
<div class="c-cdef__criteria__buttons">
|
||||
<button class="c-click-icon c-cdef__criteria-duplicate-button icon-duplicate"
|
||||
title="Duplicate this criteria"
|
||||
@click="cloneCriterion(index)"
|
||||
></button>
|
||||
<button v-if="!(condition.configuration.criteria.length === 1)"
|
||||
class="c-click-icon c-cdef__criteria-duplicate-button icon-trash"
|
||||
title="Delete this criteria"
|
||||
@click="removeCriterion(index)"
|
||||
></button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="c-cdef__separator c-row-separator"></div>
|
||||
<div class="c-cdef__controls"
|
||||
:disabled="!telemetry.length"
|
||||
>
|
||||
<button
|
||||
class="c-cdef__add-criteria-button c-button c-button--labeled icon-plus"
|
||||
@click="addCriteria"
|
||||
>
|
||||
<span class="c-button__label">Add Criteria</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="condition-config">
|
||||
<span class="condition-description">
|
||||
{{ condition.description }}
|
||||
</span>
|
||||
<div v-else
|
||||
class="c-condition c-condition--browse"
|
||||
>
|
||||
<!-- Browse view -->
|
||||
<div class="c-condition__header">
|
||||
<span class="c-condition__name">
|
||||
{{ condition.configuration.name }}
|
||||
</span>
|
||||
<span class="c-condition__output">
|
||||
Output: {{ condition.configuration.output }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="c-condition__summary">
|
||||
<condition-description :show-label="false"
|
||||
:condition="condition"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Criterion from './Criterion.vue';
|
||||
import ConditionDescription from "./ConditionDescription.vue";
|
||||
import { TRIGGER, TRIGGER_LABEL } from "@/plugins/condition/utils/constants";
|
||||
import uuid from 'uuid';
|
||||
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
components: {
|
||||
Criterion,
|
||||
ConditionDescription
|
||||
},
|
||||
props: {
|
||||
condition: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
conditionIndex: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
isEditing: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
telemetry: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => []
|
||||
},
|
||||
isDragging: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
moveIndex: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
conditionData: {}
|
||||
currentCriteria: this.currentCriteria,
|
||||
expanded: true,
|
||||
trigger: 'all',
|
||||
selectedOutputSelection: '',
|
||||
outputOptions: ['false', 'true', 'string'],
|
||||
criterionIndex: 0,
|
||||
draggingOver: false,
|
||||
isDefault: this.condition.isDefault
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
triggers() {
|
||||
const keys = Object.keys(TRIGGER);
|
||||
const triggerOptions = [];
|
||||
keys.forEach((trigger) => {
|
||||
triggerOptions.push({
|
||||
value: TRIGGER[trigger],
|
||||
label: TRIGGER_LABEL[TRIGGER[trigger]]
|
||||
});
|
||||
});
|
||||
return triggerOptions;
|
||||
},
|
||||
canEvaluateCriteria: function () {
|
||||
let criteria = this.condition.configuration.criteria;
|
||||
if (criteria.length) {
|
||||
let lastCriterion = criteria[criteria.length - 1];
|
||||
if (lastCriterion.telemetry &&
|
||||
lastCriterion.operation &&
|
||||
(lastCriterion.input.length ||
|
||||
lastCriterion.operation === 'isDefined' ||
|
||||
lastCriterion.operation === 'isUndefined')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
},
|
||||
destroyed() {
|
||||
this.destroy();
|
||||
},
|
||||
mounted() {
|
||||
this.setOutputSelection();
|
||||
},
|
||||
methods: {
|
||||
setOutputSelection() {
|
||||
let conditionOutput = this.condition.configuration.output;
|
||||
if (conditionOutput) {
|
||||
if (conditionOutput !== 'false' && conditionOutput !== 'true') {
|
||||
this.selectedOutputSelection = 'string';
|
||||
} else {
|
||||
this.selectedOutputSelection = conditionOutput;
|
||||
}
|
||||
}
|
||||
},
|
||||
setOutputValue() {
|
||||
if (this.selectedOutputSelection === 'string') {
|
||||
this.condition.configuration.output = '';
|
||||
} else {
|
||||
this.condition.configuration.output = this.selectedOutputSelection;
|
||||
}
|
||||
this.persist();
|
||||
},
|
||||
addCriteria() {
|
||||
const criteriaObject = {
|
||||
id: uuid(),
|
||||
telemetry: '',
|
||||
operation: '',
|
||||
input: '',
|
||||
metadata: ''
|
||||
};
|
||||
this.condition.configuration.criteria.push(criteriaObject);
|
||||
},
|
||||
dragStart(e) {
|
||||
e.dataTransfer.setData('dragging', e.target); // required for FF to initiate drag
|
||||
e.dataTransfer.effectAllowed = "copyMove";
|
||||
e.dataTransfer.setDragImage(e.target.closest('.c-condition-h'), 0, 0);
|
||||
this.$emit('setMoveIndex', this.conditionIndex);
|
||||
},
|
||||
dragEnd(event) {
|
||||
this.dragStarted = false;
|
||||
event.dataTransfer.clearData();
|
||||
this.$emit('dragComplete');
|
||||
},
|
||||
dropCondition(event, targetIndex) {
|
||||
if (!this.isDragging) { return }
|
||||
if (targetIndex > this.moveIndex) { targetIndex-- } // for 'downward' move
|
||||
if (this.isValidTarget(targetIndex)) {
|
||||
this.dragElement = undefined;
|
||||
this.draggingOver = false;
|
||||
this.$emit('dropCondition', targetIndex);
|
||||
}
|
||||
},
|
||||
dragEnter(event, targetIndex) {
|
||||
if (!this.isDragging) { return }
|
||||
if (targetIndex > this.moveIndex) { targetIndex-- } // for 'downward' move
|
||||
if (this.isValidTarget(targetIndex)) {
|
||||
this.dragElement = event.target.parentElement;
|
||||
this.draggingOver = true;
|
||||
}
|
||||
},
|
||||
dragLeave(event) {
|
||||
if (event.target.parentElement === this.dragElement) {
|
||||
this.draggingOver = false;
|
||||
this.dragElement = undefined;
|
||||
}
|
||||
},
|
||||
isValidTarget(targetIndex) {
|
||||
return this.moveIndex !== targetIndex;
|
||||
},
|
||||
destroy() {
|
||||
},
|
||||
removeCondition(ev) {
|
||||
this.$emit('removeCondition', this.conditionIndex);
|
||||
},
|
||||
cloneCondition(ev) {
|
||||
this.$emit('cloneCondition', {
|
||||
condition: this.condition,
|
||||
index: this.conditionIndex
|
||||
});
|
||||
},
|
||||
removeCriterion(index) {
|
||||
this.condition.configuration.criteria.splice(index, 1);
|
||||
this.persist();
|
||||
},
|
||||
cloneCriterion(index) {
|
||||
const clonedCriterion = JSON.parse(JSON.stringify(this.condition.configuration.criteria[index]));
|
||||
clonedCriterion.id = uuid();
|
||||
this.condition.configuration.criteria.splice(index + 1, 0, clonedCriterion);
|
||||
this.persist();
|
||||
},
|
||||
persist() {
|
||||
this.$emit('updateCondition', {
|
||||
condition: this.condition,
|
||||
index: this.conditionIndex
|
||||
});
|
||||
},
|
||||
initCap(str) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -1,132 +1,228 @@
|
||||
/*****************************************************************************
|
||||
* 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>
|
||||
<section id="conditionCollection"
|
||||
class="c-cs__ui_section"
|
||||
:class="{ 'is-expanded': expanded }"
|
||||
>
|
||||
<div class="c-cs__ui__header">
|
||||
<span class="c-cs__ui__header-label">Conditions</span>
|
||||
<div class="c-cs__header c-section__header">
|
||||
<span
|
||||
class="is-enabled flex-elem"
|
||||
:class="['c-cs__disclosure-triangle', { 'c-cs__disclosure-triangle--expanded': expanded }]"
|
||||
class="c-disclosure-triangle c-tree__item__view-control is-enabled"
|
||||
:class="{ 'c-disclosure-triangle--expanded': expanded }"
|
||||
@click="expanded = !expanded"
|
||||
></span>
|
||||
<div class="c-cs__header-label c-section__label">Conditions</div>
|
||||
</div>
|
||||
<div v-if="expanded"
|
||||
class="c-cs__ui_content"
|
||||
class="c-cs__content"
|
||||
>
|
||||
<div v-show="isEditing"
|
||||
class="help"
|
||||
class="hint"
|
||||
:class="{ 's-status-icon-warning-lo': !telemetryObjs.length }"
|
||||
>
|
||||
<span>The first condition to match is the one that wins. Drag conditions to rearrange.</span>
|
||||
<template v-if="!telemetryObjs.length">Drag telemetry into this Condition Set to configure Conditions and add criteria.</template>
|
||||
<template v-else>The first condition to match is the one that is applied. Drag conditions to reorder.</template>
|
||||
</div>
|
||||
<div class="holder add-condition-button-wrapper align-left">
|
||||
<button
|
||||
v-show="isEditing"
|
||||
id="addCondition"
|
||||
class="c-cs-button c-cs-button--major add-condition-button"
|
||||
@click="addCondition"
|
||||
>
|
||||
<span class="c-cs-button__label">Add Condition</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="condition-collection">
|
||||
<div v-for="condition in conditionCollection"
|
||||
:key="condition.identifier.key"
|
||||
class="conditionArea"
|
||||
>
|
||||
<div v-if="isEditing">
|
||||
<ConditionEdit :condition="condition" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<Condition :condition="condition" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-show="isEditing"
|
||||
id="addCondition"
|
||||
class="c-button c-button--major icon-plus labeled"
|
||||
@click="addCondition"
|
||||
>
|
||||
<span class="c-cs-button__label">Add Condition</span>
|
||||
</button>
|
||||
|
||||
<div class="c-cs__conditions-h"
|
||||
:class="{ 'is-active-dragging': isDragging }"
|
||||
>
|
||||
<Condition v-for="(condition, index) in conditionCollection"
|
||||
:key="condition.id"
|
||||
:condition="condition"
|
||||
:condition-index="index"
|
||||
:telemetry="telemetryObjs"
|
||||
:is-editing="isEditing"
|
||||
:move-index="moveIndex"
|
||||
:is-dragging="isDragging"
|
||||
@updateCondition="updateCondition"
|
||||
@removeCondition="removeCondition"
|
||||
@cloneCondition="cloneCondition"
|
||||
@setMoveIndex="setMoveIndex"
|
||||
@dragComplete="dragComplete"
|
||||
@dropCondition="dropCondition"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Condition from '../../condition/components/Condition.vue';
|
||||
import ConditionEdit from '../../condition/components/ConditionEdit.vue';
|
||||
import uuid from 'uuid';
|
||||
import ConditionClass from '../Condition';
|
||||
import Condition from './Condition.vue';
|
||||
import ConditionManager from '../ConditionManager';
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject'],
|
||||
components: {
|
||||
Condition,
|
||||
ConditionEdit
|
||||
Condition
|
||||
},
|
||||
props: {
|
||||
isEditing: Boolean
|
||||
isEditing: Boolean,
|
||||
testData: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => {
|
||||
return {
|
||||
applied: false,
|
||||
conditionTestInputs: []
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
expanded: true,
|
||||
parentKeyString: this.openmct.objects.makeKeyString(this.domainObject.identifier),
|
||||
conditionCollection: [],
|
||||
conditions: []
|
||||
conditionResults: {},
|
||||
conditions: [],
|
||||
telemetryObjs: [],
|
||||
moveIndex: undefined,
|
||||
isDragging: false,
|
||||
defaultOutput: undefined,
|
||||
dragCounter: 0
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
defaultOutput(newOutput, oldOutput) {
|
||||
this.$emit('updateDefaultOutput', newOutput);
|
||||
},
|
||||
testData: {
|
||||
handler() {
|
||||
this.updateTestData();
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
destroyed() {
|
||||
this.composition.off('add', this.addTelemetry);
|
||||
this.composition.off('add', this.addTelemetryObject);
|
||||
this.composition.off('remove', this.removeTelemetryObject);
|
||||
if(this.conditionManager) {
|
||||
this.conditionManager.off('conditionSetResultUpdated', this.handleConditionSetResultUpdated);
|
||||
this.conditionManager.destroy();
|
||||
}
|
||||
if (this.stopObservingForChanges) {
|
||||
this.stopObservingForChanges();
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.telemetryObjs = [];
|
||||
this.instantiate = this.openmct.$injector.get('instantiate');
|
||||
this.composition = this.openmct.composition.get(this.domainObject);
|
||||
this.composition.on('add', this.addTelemetry);
|
||||
this.composition.on('add', this.addTelemetryObject);
|
||||
this.composition.on('remove', this.removeTelemetryObject);
|
||||
this.composition.load();
|
||||
this.conditionCollection = this.domainObject.configuration ? this.domainObject.configuration.conditionCollection : [];
|
||||
if (!this.conditionCollection.length) {this.addCondition(null, true)}
|
||||
this.conditionCollection = this.domainObject.configuration.conditionCollection;
|
||||
this.observeForChanges();
|
||||
this.conditionManager = new ConditionManager(this.domainObject, this.openmct);
|
||||
this.conditionManager.on('conditionSetResultUpdated', this.handleConditionSetResultUpdated);
|
||||
this.updateDefaultCondition();
|
||||
},
|
||||
methods: {
|
||||
addTelemetry(telemetryDomainObject) {
|
||||
this.telemetryObjs.push(telemetryDomainObject);
|
||||
handleConditionSetResultUpdated(data) {
|
||||
this.$emit('conditionSetResultUpdated', data)
|
||||
},
|
||||
addCondition(event, isDefault) {
|
||||
let conditionDO = this.getConditionDomainObject(!!isDefault);
|
||||
this.conditionCollection.unshift(conditionDO);
|
||||
observeForChanges() {
|
||||
this.stopObservingForChanges = this.openmct.objects.observe(this.domainObject, 'configuration.conditionCollection', (newConditionCollection) => {
|
||||
this.conditionCollection = newConditionCollection;
|
||||
this.updateDefaultCondition();
|
||||
});
|
||||
},
|
||||
updateDefaultCondition() {
|
||||
const defaultCondition = this.domainObject.configuration.conditionCollection
|
||||
.find(conditionConfiguration => conditionConfiguration.isDefault);
|
||||
this.defaultOutput = defaultCondition.configuration.output;
|
||||
},
|
||||
setMoveIndex(index) {
|
||||
this.moveIndex = index;
|
||||
this.isDragging = true;
|
||||
},
|
||||
dropCondition(targetIndex) {
|
||||
const oldIndexArr = Object.keys(this.conditionCollection);
|
||||
const move = function (arr, old_index, new_index) {
|
||||
while (old_index < 0) {
|
||||
old_index += arr.length;
|
||||
}
|
||||
while (new_index < 0) {
|
||||
new_index += arr.length;
|
||||
}
|
||||
if (new_index >= arr.length) {
|
||||
var k = new_index - arr.length;
|
||||
while ((k--) + 1) {
|
||||
arr.push(undefined);
|
||||
}
|
||||
}
|
||||
arr.splice(new_index, 0, arr.splice(old_index, 1)[0]);
|
||||
return arr;
|
||||
}
|
||||
const newIndexArr = move(oldIndexArr, this.moveIndex, targetIndex);
|
||||
const reorderPlan = [];
|
||||
|
||||
let condition = new ConditionClass(conditionDO, this.openmct);
|
||||
this.conditions.push(condition);
|
||||
for (let i = 0; i < oldIndexArr.length; i++) {
|
||||
reorderPlan.push({oldIndex: Number(newIndexArr[i]), newIndex: i});
|
||||
}
|
||||
|
||||
this.reorder(reorderPlan);
|
||||
},
|
||||
getConditionDomainObject(isDefault) {
|
||||
let conditionObj = {
|
||||
isDefault: isDefault,
|
||||
isCurrent: true,
|
||||
identifier: {
|
||||
namespace: "",
|
||||
key: uuid()
|
||||
},
|
||||
name: isDefault ? 'Default' : 'Unnamed Condition',
|
||||
trigger: 'any',
|
||||
criteria: isDefault ? [] : [{
|
||||
operation: '',
|
||||
input: '',
|
||||
metaDataKey: this.openmct.telemetry.getMetadata(this.telemetryObjs[0]).values()[0].key,
|
||||
key: this.telemetryObjs.length ? this.openmct.objects.makeKeyString(this.telemetryObjs[0].identifier) : null
|
||||
}],
|
||||
output: 'false',
|
||||
summary: 'summary description'
|
||||
};
|
||||
let conditionDOKeyString = this.openmct.objects.makeKeyString(conditionObj.identifier);
|
||||
let newDO = this.instantiate(conditionObj, conditionDOKeyString);
|
||||
return newDO.useCapability('adapter');
|
||||
dragComplete() {
|
||||
this.isDragging = false;
|
||||
},
|
||||
updateCondition(updatedCondition) {
|
||||
let index = _.findIndex(this.conditions, (condition) => condition.id === updatedCondition.id);
|
||||
this.conditions[index] = updatedCondition;
|
||||
addTelemetryObject(domainObject) {
|
||||
this.telemetryObjs.push(domainObject);
|
||||
this.$emit('telemetryUpdated', this.telemetryObjs);
|
||||
},
|
||||
removeCondition(identifier) {
|
||||
let index = _.findIndex(this.conditionCollection, (condition) => this.openmct.objects.makeKeyString(identifier) === condition.identifier.key);
|
||||
this.conditionCollection.splice(index, 1);
|
||||
removeTelemetryObject(identifier) {
|
||||
let index = _.findIndex(this.telemetryObjs, (obj) => {
|
||||
let objId = this.openmct.objects.makeKeyString(obj.identifier);
|
||||
let id = this.openmct.objects.makeKeyString(identifier);
|
||||
return objId === id;
|
||||
});
|
||||
if (index > -1) {
|
||||
this.telemetryObjs.splice(index, 1);
|
||||
}
|
||||
},
|
||||
addCondition() {
|
||||
this.conditionManager.addCondition();
|
||||
},
|
||||
updateCondition(data) {
|
||||
this.conditionManager.updateCondition(data.condition, data.index);
|
||||
},
|
||||
removeCondition(index) {
|
||||
this.conditionManager.removeCondition(index);
|
||||
},
|
||||
reorder(reorderPlan) {
|
||||
let oldConditions = this.conditionCollection.slice();
|
||||
reorderPlan.forEach((reorderEvent) => {
|
||||
this.$set(this.conditionCollection, reorderEvent.newIndex, oldConditions[reorderEvent.oldIndex]);
|
||||
});
|
||||
this.conditionManager.reorderConditions(reorderPlan);
|
||||
},
|
||||
cloneCondition(data) {
|
||||
this.conditionManager.cloneCondition(data.condition, data.index);
|
||||
},
|
||||
updateTestData() {
|
||||
this.conditionManager.updateTestData(this.testData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
154
src/plugins/condition/components/ConditionDescription.vue
Normal file
154
src/plugins/condition/components/ConditionDescription.vue
Normal file
@ -0,0 +1,154 @@
|
||||
/*****************************************************************************
|
||||
* 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-style__condition-desc">
|
||||
<span v-if="showLabel && condition"
|
||||
class="c-style__condition-desc__name c-condition__name"
|
||||
>
|
||||
{{ condition.configuration.name }}
|
||||
</span>
|
||||
<span v-for="(criterionDescription, index) in criterionDescriptions"
|
||||
:key="criterionDescription"
|
||||
class="c-style__condition-desc__text"
|
||||
>
|
||||
<template v-if="!index">When</template>
|
||||
{{ criterionDescription }}
|
||||
<template v-if="index < (criterionDescriptions.length-1)">{{ triggerDescription }}</template>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { TRIGGER } from "@/plugins/condition/utils/constants";
|
||||
import { OPERATIONS } from "@/plugins/condition/utils/operations";
|
||||
|
||||
export default {
|
||||
name: 'ConditionDescription',
|
||||
inject: [
|
||||
'openmct'
|
||||
],
|
||||
props: {
|
||||
showLabel: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
condition: {
|
||||
type: Object,
|
||||
default() {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
criterionDescriptions: [],
|
||||
triggerDescription: ''
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
condition: {
|
||||
handler(val) {
|
||||
this.getConditionDescription();
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getConditionDescription();
|
||||
},
|
||||
methods: {
|
||||
getTriggerDescription(trigger) {
|
||||
let description = '';
|
||||
switch(trigger) {
|
||||
case TRIGGER.ANY:
|
||||
case TRIGGER.XOR:
|
||||
description = 'or';
|
||||
break;
|
||||
case TRIGGER.ALL:
|
||||
case TRIGGER.NOT: description = 'and';
|
||||
break;
|
||||
}
|
||||
return description;
|
||||
},
|
||||
getConditionDescription() {
|
||||
if (this.condition) {
|
||||
this.triggerDescription = this.getTriggerDescription(this.condition.configuration.trigger);
|
||||
this.criterionDescriptions = [];
|
||||
this.condition.configuration.criteria.forEach((criterion, index) => {
|
||||
this.getCriterionDescription(criterion, index);
|
||||
});
|
||||
if (this.condition.isDefault) {
|
||||
this.criterionDescriptions.splice(0, 0, 'all else fails');
|
||||
}
|
||||
} else {
|
||||
this.criterionDescriptions = [];
|
||||
}
|
||||
},
|
||||
getCriterionDescription(criterion, index) {
|
||||
if (!criterion.telemetry) {
|
||||
let description = `Unknown ${criterion.metadata} ${this.getOperatorText(criterion.operation, criterion.input)}`;
|
||||
this.criterionDescriptions.splice(index, 0, description);
|
||||
} else if (criterion.telemetry === 'all' || criterion.telemetry === 'any') {
|
||||
const telemetryDescription = criterion.telemetry === 'all' ? 'All telemetry' : 'Any telemetry';
|
||||
let description = `${telemetryDescription} ${criterion.metadata} ${this.getOperatorText(criterion.operation, criterion.input)}`;
|
||||
this.criterionDescriptions.splice(index, 0, description);
|
||||
} else {
|
||||
this.openmct.objects.get(criterion.telemetry).then((telemetryObject) => {
|
||||
if (telemetryObject.type === 'unknown') {
|
||||
let description = `Unknown ${criterion.metadata} ${this.getOperatorText(criterion.operation, criterion.input)}`;
|
||||
this.criterionDescriptions.splice(index, 0, description);
|
||||
} else {
|
||||
let metadataValue = criterion.metadata;
|
||||
let inputValue = criterion.input;
|
||||
if (criterion.metadata) {
|
||||
this.telemetryMetadata = this.openmct.telemetry.getMetadata(telemetryObject);
|
||||
|
||||
const metadataObj = this.telemetryMetadata.valueMetadatas.find((metadata) => metadata.key === criterion.metadata);
|
||||
if (metadataObj) {
|
||||
if (metadataObj.name) {
|
||||
metadataValue = metadataObj.name;
|
||||
}
|
||||
if(metadataObj.enumerations && inputValue.length) {
|
||||
if (metadataObj.enumerations[inputValue[0]] && metadataObj.enumerations[inputValue[0]].string) {
|
||||
inputValue = [metadataObj.enumerations[inputValue[0]].string];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let description = `${telemetryObject.name} ${metadataValue} ${this.getOperatorText(criterion.operation, inputValue)}`;
|
||||
if (this.criterionDescriptions[index]) {
|
||||
this.criterionDescriptions[index] = description;
|
||||
} else {
|
||||
this.criterionDescriptions.splice(index, 0, description);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
getOperatorText(operationName, values) {
|
||||
const found = OPERATIONS.find((operation) => operation.name === operationName);
|
||||
return found ? found.getDescription(values) : '';
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -1,233 +0,0 @@
|
||||
<template>
|
||||
<div class="c-cs-editui__conditions"
|
||||
:class="['widget-condition', { 'widget-condition--current': condition.isCurrent }]"
|
||||
>
|
||||
<div class="title-bar">
|
||||
<span
|
||||
class="c-c__menu-hamburger"
|
||||
:class="{ 'is-enabled': !condition.isDefault }"
|
||||
@click="expanded = !condition.expanded"
|
||||
></span>
|
||||
<span
|
||||
class="is-enabled flex-elem"
|
||||
:class="['c-c__disclosure-triangle', { 'c-c__disclosure-triangle--expanded': expanded }]"
|
||||
@click="expanded = !condition.expanded"
|
||||
></span>
|
||||
<div class="condition-summary">
|
||||
<span class="condition-name">{{ condition.name }}</span>
|
||||
<span class="condition-description">{{ condition.description }}</span>
|
||||
</div>
|
||||
<span v-if="!condition.isDefault"
|
||||
class="is-enabled c-c__duplicate"
|
||||
></span>
|
||||
<span v-if="!condition.isDefault"
|
||||
class="is-enabled c-c__trash"
|
||||
@click="removeCondition"
|
||||
></span>
|
||||
</div>
|
||||
<div v-if="expanded"
|
||||
class="condition-config-edit widget-rule-content c-sw-editui__rules-wrapper holder widget-rules-wrapper flex-elem expanded"
|
||||
>
|
||||
<div id="ruleArea"
|
||||
class="c-sw-editui__rules widget-rules"
|
||||
>
|
||||
<div class="c-sw-rule">
|
||||
<div class="c-sw-rule__ui l-compact-form l-widget-rule has-local-controls">
|
||||
<div>
|
||||
<ul class="t-widget-rule-config">
|
||||
<li>
|
||||
<label>Condition Name</label>
|
||||
<span class="controls">
|
||||
<input v-model="condition.name"
|
||||
class="t-rule-name-input"
|
||||
type="text"
|
||||
>
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<label>Output</label>
|
||||
<span class="controls">
|
||||
<select ref="outputSelect"
|
||||
@change="getOutputBinary">
|
||||
<option value="false">False</option>
|
||||
<option value="true">True</option>
|
||||
<option value="string">String</option>
|
||||
</select>
|
||||
<input v-if="stringOutputField"
|
||||
ref="outputString"
|
||||
class="t-rule-name-input"
|
||||
type="text"
|
||||
:value="condition.output"
|
||||
@keyup="getOutputString"
|
||||
>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-if="!condition.isDefault"
|
||||
class="widget-rule-content expanded"
|
||||
>
|
||||
<ul class="t-widget-rule-config">
|
||||
<li class="has-local-controls t-condition">
|
||||
<label>Match when</label>
|
||||
<span class="controls">
|
||||
<select>
|
||||
<option value="all">all criteria are met</option>
|
||||
<option value="any">any criteria are met</option>
|
||||
</select>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="t-widget-rule-config">
|
||||
<li v-if="telemetryObject && telemetryMetadata"
|
||||
class="has-local-controls t-condition"
|
||||
>
|
||||
<label>when</label>
|
||||
<span class="t-configuration">
|
||||
<span class="controls">
|
||||
<select class="">
|
||||
<option value="">- Select Telemetry -</option>
|
||||
<option :value="telemetryObject.key">{{ telemetryObject.name }}</option>
|
||||
</select>
|
||||
</span>
|
||||
<span class="controls">
|
||||
<select v-model="selectedMetaDataKey">
|
||||
<option value="">- Select Field -</option>
|
||||
<option v-for="option in telemetryMetadata"
|
||||
:key="option.key"
|
||||
:value="option.key"
|
||||
>
|
||||
{{ option.name }}
|
||||
</option>
|
||||
</select>
|
||||
</span>
|
||||
<span class="controls">
|
||||
<select @change="getOperationKey">
|
||||
<option value="">- Select Comparison -</option>
|
||||
<option v-for="option in operations"
|
||||
:key="option.name"
|
||||
:value="option.name"
|
||||
>
|
||||
{{ option.text }}
|
||||
</option>
|
||||
</select>
|
||||
<input v-if="comparisonValueField"
|
||||
class="t-rule-name-input"
|
||||
type="text"
|
||||
@keyup="getOperationValue"
|
||||
>
|
||||
</span>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OPERATIONS } from '../utils/operations';
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject'],
|
||||
props: {
|
||||
condition: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
let conditionCollection = this.domainObject.configuration.conditionCollection;
|
||||
return {
|
||||
expanded: true,
|
||||
name: this.condition.name,
|
||||
description: this.condition.description,
|
||||
conditionCollection,
|
||||
telemetryObject: this.telemetryObject,
|
||||
telemetryMetadata: this.telemetryMetadata,
|
||||
operations: OPERATIONS,
|
||||
selectedMetaDataKey: null,
|
||||
selectedOperationKey: null,
|
||||
stringOutputField: false,
|
||||
comparisonValueField: false
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
if (this.condition.output !== 'false' && this.condition.output !== 'true') {
|
||||
this.$refs.outputSelect.value = 'string';
|
||||
this.stringOutputField = true;
|
||||
}
|
||||
this.updateTelemetry();
|
||||
},
|
||||
updated() {
|
||||
this.updateCurrentCondition();
|
||||
this.persist();
|
||||
},
|
||||
methods: {
|
||||
removeCondition(ev) {
|
||||
const conditionDiv = ev.target.closest('.conditionArea');
|
||||
const conditionCollectionDiv = conditionDiv.closest('.condition-collection');
|
||||
const index = Array.from(conditionCollectionDiv.children).indexOf(conditionDiv);
|
||||
|
||||
this.domainObject.configuration.conditionCollection.splice(index, 1);
|
||||
},
|
||||
updateTelemetry() {
|
||||
if (this.hasTelemetry()) {
|
||||
this.openmct.objects.get(this.condition.criteria[0].key).then((obj) => {
|
||||
this.telemetryObject = obj;
|
||||
this.telemetryMetadata = this.openmct.telemetry.getMetadata(this.telemetryObject).values();
|
||||
this.selectedMetaDataKey = this.telemetryMetadata[0].key;
|
||||
console.log('ConditionEdit', this.telemetryObject, this.telemetryMetadata);
|
||||
});
|
||||
} else {
|
||||
this.telemetryObject = null;
|
||||
}
|
||||
},
|
||||
hasTelemetry() {
|
||||
return this.condition.criteria.length && this.condition.criteria[0].key;
|
||||
},
|
||||
persist(index) {
|
||||
if (index) {
|
||||
this.openmct.objects.mutate(this.domainObject, `configuration.conditionCollection[${index}]`, this.domainObject.configuration.conditionCollection[index]);
|
||||
} else {
|
||||
this.openmct.objects.mutate(this.domainObject, 'configuration.conditionCollection', this.domainObject.configuration.conditionCollection);
|
||||
}
|
||||
},
|
||||
updateCurrentCondition() {
|
||||
// TODO: replace based on telemetry
|
||||
if (this.conditionCollection.length > 1) {
|
||||
this.conditionCollection.forEach((condition, index) => {
|
||||
index === 0 ? condition.isCurrent = true : condition.isCurrent = false
|
||||
});
|
||||
} else {
|
||||
this.conditionCollection[0].isCurrent = true;
|
||||
}
|
||||
},
|
||||
getOutputBinary(ev) {
|
||||
if (ev.target.value !== 'string') {
|
||||
this.condition.output = ev.target.value;
|
||||
this.stringOutputField = false;
|
||||
} else {
|
||||
this.stringOutputField = true;
|
||||
}
|
||||
},
|
||||
getOutputString(ev) {
|
||||
this.condition.output = ev.target.value;
|
||||
},
|
||||
getOperationKey(ev) {
|
||||
console.log(ev.target.value)
|
||||
if (ev.target.value !== 'isUndefined' && ev.target.value !== 'isDefined') {
|
||||
this.comparisonValueField = true;
|
||||
} else {
|
||||
this.comparisonValueField = false;
|
||||
}
|
||||
this.selectedOperationKey = ev.target.value;
|
||||
},
|
||||
getOperationValue(ev) {
|
||||
this.selectedOperationKey = ev.target.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
89
src/plugins/condition/components/ConditionError.vue
Normal file
89
src/plugins/condition/components/ConditionError.vue
Normal file
@ -0,0 +1,89 @@
|
||||
/*****************************************************************************
|
||||
* 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 v-if="conditionErrors.length"
|
||||
class="c-condition__errors"
|
||||
>
|
||||
<div v-for="(error, index) in conditionErrors"
|
||||
:key="index"
|
||||
class="u-alert u-alert--block u-alert--with-icon"
|
||||
>{{ error.message.errorText }} {{ error.additionalInfo }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import { ERROR } from "@/plugins/condition/utils/constants";
|
||||
|
||||
export default {
|
||||
name: 'ConditionError',
|
||||
inject: [
|
||||
'openmct'
|
||||
],
|
||||
props: {
|
||||
condition: {
|
||||
type: Object,
|
||||
default() {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
conditionErrors: []
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getConditionErrors();
|
||||
},
|
||||
methods: {
|
||||
getConditionErrors() {
|
||||
if (this.condition) {
|
||||
this.condition.configuration.criteria.forEach((criterion, index) => {
|
||||
this.getCriterionErrors(criterion, index);
|
||||
});
|
||||
}
|
||||
},
|
||||
getCriterionErrors(criterion, index) {
|
||||
if (!criterion.telemetry) {
|
||||
this.conditionErrors.push({
|
||||
message: ERROR.TELEMETRY_NOT_FOUND,
|
||||
additionalInfo: ''
|
||||
});
|
||||
} else {
|
||||
if (criterion.telemetry !== 'all' && criterion.telemetry !== 'any') {
|
||||
this.openmct.objects.get(criterion.telemetry).then((telemetryObject) => {
|
||||
if (telemetryObject.type === 'unknown') {
|
||||
this.conditionErrors.push({
|
||||
message: ERROR.TELEMETRY_NOT_FOUND,
|
||||
additionalInfo: criterion.telemetry ? `Key: ${this.openmct.objects.makeKeyString(criterion.telemetry)}` : ''
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -1,26 +1,65 @@
|
||||
/*****************************************************************************
|
||||
* 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-object-view u-contents">
|
||||
<div class="c-cs-edit w-condition-set">
|
||||
<div class="c-sw-edit__ui holder">
|
||||
<CurrentOutput :condition-collection="domainObject.conditionCollection" />
|
||||
<TestData :is-editing="isEditing" />
|
||||
<ConditionCollection :is-editing="isEditing"
|
||||
:condition-collection="domainObject.conditionCollection"
|
||||
/>
|
||||
<div class="c-cs">
|
||||
<section class="c-cs__current-output c-section">
|
||||
<div class="c-cs__content c-cs__current-output-value">
|
||||
<span class="c-cs__current-output-value__label">Current Output</span>
|
||||
<span class="c-cs__current-output-value__value">
|
||||
<template v-if="currentConditionOutput">
|
||||
{{ currentConditionOutput }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ defaultConditionOutput }}
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
<div class="c-cs__test-data-and-conditions-w">
|
||||
<TestData class="c-cs__test-data"
|
||||
:is-editing="isEditing"
|
||||
:test-data="testData"
|
||||
:telemetry="telemetryObjs"
|
||||
@updateTestData="updateTestData"
|
||||
/>
|
||||
<ConditionCollection class="c-cs__conditions"
|
||||
:is-editing="isEditing"
|
||||
:test-data="testData"
|
||||
@conditionSetResultUpdated="updateCurrentOutput"
|
||||
@updateDefaultOutput="updateDefaultOutput"
|
||||
@telemetryUpdated="updateTelemetry"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CurrentOutput from './CurrentOutput.vue';
|
||||
import TestData from './TestData.vue';
|
||||
import ConditionCollection from './ConditionCollection.vue';
|
||||
|
||||
export default {
|
||||
inject: ["openmct", "domainObject"],
|
||||
components: {
|
||||
CurrentOutput,
|
||||
TestData,
|
||||
ConditionCollection
|
||||
},
|
||||
@ -28,10 +67,32 @@ export default {
|
||||
isEditing: Boolean
|
||||
},
|
||||
data() {
|
||||
let conditionCollection = this.domainObject.configuration.conditionCollection;
|
||||
|
||||
return {
|
||||
conditionCollection
|
||||
currentConditionOutput: '',
|
||||
defaultConditionOutput: '',
|
||||
telemetryObjs: [],
|
||||
testData: {}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.conditionSetIdentifier = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
this.testData = {
|
||||
applied: false,
|
||||
conditionTestInputs: this.domainObject.configuration.conditionTestData || []
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
updateCurrentOutput(currentConditionResult) {
|
||||
this.currentConditionOutput = currentConditionResult.output;
|
||||
},
|
||||
updateDefaultOutput(output) {
|
||||
this.currentConditionOutput = output;
|
||||
},
|
||||
updateTelemetry(telemetryObjs) {
|
||||
this.telemetryObjs = telemetryObjs;
|
||||
},
|
||||
updateTestData(testData) {
|
||||
this.testData = testData;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
295
src/plugins/condition/components/Criterion.vue
Normal file
295
src/plugins/condition/components/Criterion.vue
Normal file
@ -0,0 +1,295 @@
|
||||
/*****************************************************************************
|
||||
* 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="u-contents">
|
||||
<div class="c-cdef__separator c-row-separator"></div>
|
||||
<span class="c-cdef__label">{{ setRowLabel }}</span>
|
||||
<span class="c-cdef__controls">
|
||||
<span class="c-cdef__control">
|
||||
<select ref="telemetrySelect"
|
||||
v-model="criterion.telemetry"
|
||||
@change="updateMetadataOptions"
|
||||
>
|
||||
<option value="">- Select Telemetry -</option>
|
||||
<option value="all">all telemetry</option>
|
||||
<option value="any">any telemetry</option>
|
||||
<option v-for="telemetryOption in telemetry"
|
||||
:key="telemetryOption.identifier.key"
|
||||
:value="telemetryOption.identifier"
|
||||
>
|
||||
{{ telemetryOption.name }}
|
||||
</option>
|
||||
</select>
|
||||
</span>
|
||||
<span v-if="criterion.telemetry"
|
||||
class="c-cdef__control"
|
||||
>
|
||||
<select ref="metadataSelect"
|
||||
v-model="criterion.metadata"
|
||||
@change="updateOperations"
|
||||
>
|
||||
<option value="">- Select Field -</option>
|
||||
<option v-for="option in telemetryMetadataOptions"
|
||||
:key="option.key"
|
||||
:value="option.key"
|
||||
>
|
||||
{{ option.name }}
|
||||
</option>
|
||||
</select>
|
||||
</span>
|
||||
<span v-if="criterion.telemetry && criterion.metadata"
|
||||
class="c-cdef__control"
|
||||
>
|
||||
<select v-model="criterion.operation"
|
||||
@change="updateInputVisibilityAndValues"
|
||||
>
|
||||
<option value="">- Select Comparison -</option>
|
||||
<option v-for="option in filteredOps"
|
||||
:key="option.name"
|
||||
:value="option.name"
|
||||
>
|
||||
{{ option.text }}
|
||||
</option>
|
||||
</select>
|
||||
<template v-if="!enumerations.length">
|
||||
<span v-for="(item, inputIndex) in inputCount"
|
||||
:key="inputIndex"
|
||||
class="c-cdef__control__inputs"
|
||||
>
|
||||
<input v-model="criterion.input[inputIndex]"
|
||||
class="c-cdef__control__input"
|
||||
:type="setInputType"
|
||||
@blur="persist"
|
||||
>
|
||||
<span v-if="inputIndex < inputCount-1">and</span>
|
||||
</span>
|
||||
</template>
|
||||
<span v-else>
|
||||
<span v-if="inputCount && criterion.operation"
|
||||
class="c-cdef__control"
|
||||
>
|
||||
<select v-model="criterion.input[0]"
|
||||
@change="persist"
|
||||
>
|
||||
<option v-for="option in enumerations"
|
||||
:key="option.string"
|
||||
:value="option.value.toString()"
|
||||
>
|
||||
{{ option.string }}
|
||||
</option>
|
||||
</select>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OPERATIONS } from '../utils/operations';
|
||||
import { INPUT_TYPES } from '../utils/operations';
|
||||
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
props: {
|
||||
criterion: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
telemetry: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => []
|
||||
},
|
||||
index: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
trigger: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
telemetryMetadataOptions: [],
|
||||
operations: OPERATIONS,
|
||||
inputCount: 0,
|
||||
rowLabel: '',
|
||||
operationFormat: '',
|
||||
enumerations: [],
|
||||
inputTypes: INPUT_TYPES
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
setRowLabel: function () {
|
||||
let operator = this.trigger === 'all' ? 'and ': 'or ';
|
||||
return (this.index !== 0 ? operator : '') + 'when';
|
||||
},
|
||||
filteredOps: function () {
|
||||
return this.operations.filter(op => op.appliesTo.indexOf(this.operationFormat) !== -1);
|
||||
},
|
||||
setInputType: function () {
|
||||
let type = '';
|
||||
for (let i = 0; i < this.filteredOps.length; i++) {
|
||||
if (this.criterion.operation === this.filteredOps[i].name) {
|
||||
if (this.filteredOps[i].appliesTo.length) {
|
||||
type = this.inputTypes[this.filteredOps[i].appliesTo[0]];
|
||||
} else {
|
||||
type = 'text'
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return type;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
telemetry: {
|
||||
handler(newTelemetry, oldTelemetry) {
|
||||
this.checkTelemetry();
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.updateMetadataOptions();
|
||||
},
|
||||
methods: {
|
||||
checkTelemetry() {
|
||||
if(this.criterion.telemetry) {
|
||||
if (this.criterion.telemetry === 'any' || this.criterion.telemetry === 'all') {
|
||||
this.updateMetadataOptions();
|
||||
} else {
|
||||
if (!this.telemetry.find((telemetryObj) => this.openmct.objects.areIdsEqual(this.criterion.telemetry, telemetryObj.identifier))) {
|
||||
//telemetry being used was removed. So reset this criterion.
|
||||
this.criterion.telemetry = '';
|
||||
this.criterion.metadata = '';
|
||||
this.criterion.input = [];
|
||||
this.criterion.operation = '';
|
||||
this.persist();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
updateOperationFormat() {
|
||||
this.enumerations = [];
|
||||
let foundMetadata = this.telemetryMetadataOptions.find((value) => {
|
||||
return value.key === this.criterion.metadata;
|
||||
});
|
||||
if (foundMetadata) {
|
||||
if (foundMetadata.enumerations !== undefined) {
|
||||
this.operationFormat = 'enum';
|
||||
this.enumerations = foundMetadata.enumerations;
|
||||
} else if (foundMetadata.format === 'string' || foundMetadata.format === 'number') {
|
||||
this.operationFormat = foundMetadata.format;
|
||||
} else if (foundMetadata.hints.hasOwnProperty('range')) {
|
||||
this.operationFormat = 'number';
|
||||
} else if (foundMetadata.hints.hasOwnProperty('domain')) {
|
||||
this.operationFormat = 'number';
|
||||
} else if (foundMetadata.key === 'name') {
|
||||
this.operationFormat = 'string';
|
||||
} else {
|
||||
this.operationFormat = 'number';
|
||||
}
|
||||
}
|
||||
this.updateInputVisibilityAndValues();
|
||||
},
|
||||
updateMetadataOptions(ev) {
|
||||
if (ev) {
|
||||
this.clearDependentFields(ev.target);
|
||||
this.persist();
|
||||
}
|
||||
if (this.criterion.telemetry) {
|
||||
const telemetry = (this.criterion.telemetry === 'all' || this.criterion.telemetry === 'any') ? this.telemetry : [{
|
||||
identifier: this.criterion.telemetry
|
||||
}];
|
||||
|
||||
let telemetryPromises = telemetry.map((telemetryObject) => this.openmct.objects.get(telemetryObject.identifier));
|
||||
Promise.all(telemetryPromises).then(telemetryObjects => {
|
||||
this.telemetryMetadataOptions = [];
|
||||
telemetryObjects.forEach(telemetryObject => {
|
||||
let telemetryMetadata = this.openmct.telemetry.getMetadata(telemetryObject);
|
||||
this.addMetaDataOptions(telemetryMetadata.values());
|
||||
});
|
||||
this.updateOperations();
|
||||
});
|
||||
}
|
||||
},
|
||||
addMetaDataOptions(options) {
|
||||
if (!this.telemetryMetadataOptions) {
|
||||
this.telemetryMetadataOptions = options;
|
||||
}
|
||||
options.forEach((option) => {
|
||||
const found = this.telemetryMetadataOptions.find((metadataOption) => {
|
||||
return (metadataOption.key && (metadataOption.key === option.key)) && (metadataOption.name && (metadataOption.name === option.name))
|
||||
});
|
||||
if (!found) {
|
||||
this.telemetryMetadataOptions.push(option);
|
||||
}
|
||||
});
|
||||
},
|
||||
updateOperations(ev) {
|
||||
this.updateOperationFormat();
|
||||
if (ev) {
|
||||
this.clearDependentFields(ev.target);
|
||||
this.persist();
|
||||
}
|
||||
},
|
||||
updateInputVisibilityAndValues(ev) {
|
||||
if (ev) {
|
||||
this.clearDependentFields();
|
||||
this.persist();
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.filteredOps.length; i++) {
|
||||
if (this.criterion.operation === this.filteredOps[i].name) {
|
||||
this.inputCount = this.filteredOps[i].inputCount;
|
||||
}
|
||||
}
|
||||
if (!this.inputCount) {
|
||||
this.criterion.input = [];
|
||||
}
|
||||
},
|
||||
clearDependentFields(el) {
|
||||
if (el === this.$refs.telemetrySelect) {
|
||||
this.criterion.metadata = '';
|
||||
} else if (el === this.$refs.metadataSelect) {
|
||||
if (!this.filteredOps.find(operation => operation.name === this.criterion.operation)) {
|
||||
this.criterion.operation = '';
|
||||
this.criterion.input = this.enumerations.length ? [this.enumerations[0].value.toString()] : [];
|
||||
this.inputCount = 0;
|
||||
}
|
||||
} else {
|
||||
if (this.enumerations.length && !this.criterion.input.length) {
|
||||
this.criterion.input = [this.enumerations[0].value.toString()];
|
||||
}
|
||||
this.inputCount = 0;
|
||||
}
|
||||
},
|
||||
persist() {
|
||||
this.$emit('persist', this.criterion);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
@ -1,52 +0,0 @@
|
||||
<template>
|
||||
<section id="current-output">
|
||||
<div class="c-cs__ui__header">
|
||||
<span class="c-cs__ui__header-label">Current Output</span>
|
||||
<span
|
||||
class="is-enabled flex-elem"
|
||||
:class="['c-cs__disclosure-triangle', { 'c-cs__disclosure-triangle--expanded': expanded }]"
|
||||
@click="expanded = !expanded"
|
||||
></span>
|
||||
</div>
|
||||
<div v-if="expanded"
|
||||
class="c-cs__ui_content"
|
||||
>
|
||||
<div>
|
||||
<span class="current-output">{{ conditionCollection[currentConditionIndex].output }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject'],
|
||||
props: {
|
||||
isEditing: Boolean
|
||||
},
|
||||
data() {
|
||||
let conditionCollection = this.domainObject.configuration.conditionCollection;
|
||||
let currentConditionIndex = 0;
|
||||
|
||||
return {
|
||||
expanded: true,
|
||||
conditionCollection,
|
||||
currentConditionIndex
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.currentConditionIndex = this.getCurrentConditionIndex();
|
||||
},
|
||||
methods: {
|
||||
getCurrentConditionIndex() {
|
||||
let currentConditionIndex;
|
||||
this.conditionCollection.forEach((condition, index) => {
|
||||
if (condition.isCurrent) {
|
||||
currentConditionIndex = index;
|
||||
}
|
||||
});
|
||||
return currentConditionIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -1,51 +1,120 @@
|
||||
/*****************************************************************************
|
||||
* 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>
|
||||
<section v-show="isEditing"
|
||||
id="test-data"
|
||||
class="test-data"
|
||||
:class="{ 'is-expanded': expanded }"
|
||||
>
|
||||
<div class="c-cs__ui__header">
|
||||
<span class="c-cs__ui__header-label">Test Data</span>
|
||||
<div class="c-cs__header c-section__header">
|
||||
<span
|
||||
class="is-enabled flex-elem"
|
||||
:class="['c-cs__disclosure-triangle', { 'c-cs__disclosure-triangle--expanded': expanded }]"
|
||||
class="c-disclosure-triangle c-tree__item__view-control is-enabled"
|
||||
:class="{ 'c-disclosure-triangle--expanded': expanded }"
|
||||
@click="expanded = !expanded"
|
||||
></span>
|
||||
<div class="c-cs__header-label c-section__label">Test Data</div>
|
||||
</div>
|
||||
<div v-if="expanded"
|
||||
class="c-cs__ui_content"
|
||||
class="c-cs__content"
|
||||
>
|
||||
<label class="checkbox custom">
|
||||
<input type="checkbox"
|
||||
class="t-test-data-checkbox"
|
||||
>
|
||||
<span>Apply Test Data</span>
|
||||
</label>
|
||||
<div class="t-test-data-config">
|
||||
<div class="c-cs-editui__conditions widget-condition">
|
||||
<form>
|
||||
<label>
|
||||
<span>Set</span>
|
||||
<select>
|
||||
<option>- Select Input -</option>
|
||||
</select>
|
||||
</label>
|
||||
<span class="is-enabled flex-elem c-cs__duplicate"></span>
|
||||
<span class="is-enabled flex-elem c-cs__trash"></span>
|
||||
</form>
|
||||
</div>
|
||||
<div class="c-cs-editui__conditions widget-condition">
|
||||
<form>
|
||||
<label>
|
||||
<span>Set</span>
|
||||
<select>
|
||||
<option>- Select Input -</option>
|
||||
</select>
|
||||
</label>
|
||||
<span class="is-enabled c-cs__duplicate"></span>
|
||||
<span class="is-enabled c-cs__trash"></span>
|
||||
</form>
|
||||
</div>
|
||||
<div class="c-cs__test-data__controls c-cdef__controls"
|
||||
:disabled="!telemetry.length"
|
||||
>
|
||||
<label class="c-toggle-switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="isApplied"
|
||||
@change="applyTestData"
|
||||
>
|
||||
<span class="c-toggle-switch__slider"></span>
|
||||
<span class="c-toggle-switch__label">Apply Test Data</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="c-cs-tests">
|
||||
<span v-for="(testInput, tIndex) in testInputs"
|
||||
:key="tIndex"
|
||||
class="c-test-datum c-cs-test"
|
||||
>
|
||||
<span class="c-cs-test__label">Set</span>
|
||||
<span class="c-cs-test__controls">
|
||||
<span class="c-cdef__control">
|
||||
<select v-model="testInput.telemetry"
|
||||
@change="updateMetadata(testInput)"
|
||||
>
|
||||
<option value="">- Select Telemetry -</option>
|
||||
<option v-for="(telemetryOption, index) in telemetry"
|
||||
:key="index"
|
||||
:value="telemetryOption.identifier"
|
||||
>
|
||||
{{ telemetryOption.name }}
|
||||
</option>
|
||||
</select>
|
||||
</span>
|
||||
<span v-if="testInput.telemetry"
|
||||
class="c-cdef__control"
|
||||
>
|
||||
<select v-model="testInput.metadata"
|
||||
@change="updateTestData"
|
||||
>
|
||||
<option value="">- Select Field -</option>
|
||||
<option v-for="(option, index) in telemetryMetadataOptions[getId(testInput.telemetry)]"
|
||||
:key="index"
|
||||
:value="option.key"
|
||||
>
|
||||
{{ option.name }}
|
||||
</option>
|
||||
</select>
|
||||
</span>
|
||||
<span v-if="testInput.metadata"
|
||||
class="c-cdef__control__inputs"
|
||||
>
|
||||
<input v-model="testInput.value"
|
||||
placeholder="Enter test input"
|
||||
type="text"
|
||||
class="c-cdef__control__input"
|
||||
@change="updateTestData"
|
||||
>
|
||||
</span>
|
||||
</span>
|
||||
<div class="c-cs-test__buttons">
|
||||
<button class="c-click-icon c-test-data__duplicate-button icon-duplicate"
|
||||
title="Duplicate this test datum"
|
||||
@click="addTestInput(testInput)"
|
||||
></button>
|
||||
<button class="c-click-icon c-test-data__delete-button icon-trash"
|
||||
title="Delete this test datum"
|
||||
@click="removeTestInput(tIndex)"
|
||||
></button>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
v-show="isEditing"
|
||||
id="addTestDatum"
|
||||
class="c-button c-button--major icon-plus labeled"
|
||||
@click="addTestInput"
|
||||
>
|
||||
<span class="c-cs-button__label">Add Test Datum</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@ -54,15 +123,114 @@
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
props: {
|
||||
isEditing: Boolean
|
||||
isEditing: Boolean,
|
||||
telemetry: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => []
|
||||
},
|
||||
testData: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => {
|
||||
return {
|
||||
applied: false,
|
||||
conditionTestInputs: []
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
expanded: true
|
||||
expanded: true,
|
||||
isApplied: false,
|
||||
testInputs: [],
|
||||
telemetryMetadataOptions: {}
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
isEditing(editing) {
|
||||
if (!editing) {
|
||||
this.resetApplied();
|
||||
}
|
||||
},
|
||||
telemetry: {
|
||||
handler() {
|
||||
this.initializeMetadata();
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
testData: {
|
||||
handler() {
|
||||
this.initialize();
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.resetApplied();
|
||||
},
|
||||
mounted() {
|
||||
this.initialize();
|
||||
this.initializeMetadata();
|
||||
},
|
||||
methods: {
|
||||
|
||||
applyTestData() {
|
||||
this.isApplied = !this.isApplied;
|
||||
this.updateTestData();
|
||||
},
|
||||
initialize() {
|
||||
if (this.testData && this.testData.conditionTestInputs) {
|
||||
this.testInputs = this.testData.conditionTestInputs;
|
||||
}
|
||||
if (!this.testInputs.length) {
|
||||
this.addTestInput();
|
||||
}
|
||||
},
|
||||
initializeMetadata() {
|
||||
this.telemetry.forEach((telemetryObject) => {
|
||||
const id = this.openmct.objects.makeKeyString(telemetryObject.identifier);
|
||||
let telemetryMetadata = this.openmct.telemetry.getMetadata(telemetryObject);
|
||||
this.telemetryMetadataOptions[id] = telemetryMetadata.values().slice();
|
||||
});
|
||||
},
|
||||
addTestInput(testInput) {
|
||||
this.testInputs.push(Object.assign({
|
||||
telemetry: '',
|
||||
metadata: '',
|
||||
input: ''
|
||||
}, testInput));
|
||||
},
|
||||
removeTestInput(index) {
|
||||
this.testInputs.splice(index, 1);
|
||||
this.updateTestData();
|
||||
},
|
||||
getId(identifier) {
|
||||
if (identifier) {
|
||||
return this.openmct.objects.makeKeyString(identifier);
|
||||
}
|
||||
return [];
|
||||
},
|
||||
updateMetadata(testInput) {
|
||||
if (testInput.telemetry) {
|
||||
const id = this.openmct.objects.makeKeyString(testInput.telemetry);
|
||||
if(this.telemetryMetadataOptions[id]) {
|
||||
return;
|
||||
}
|
||||
let telemetryMetadata = this.openmct.telemetry.getMetadata(testInput);
|
||||
this.telemetryMetadataOptions[id] = telemetryMetadata.values().slice();
|
||||
}
|
||||
},
|
||||
resetApplied() {
|
||||
this.isApplied = false;
|
||||
this.updateTestData();
|
||||
},
|
||||
updateTestData() {
|
||||
this.$emit('updateTestData', {
|
||||
applied: this.isApplied,
|
||||
conditionTestInputs: this.testInputs
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -1,142 +0,0 @@
|
||||
.c-cs-edit {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
section {
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.c-cs__ui__header {
|
||||
background-color: #D0D1D3;
|
||||
padding: 0.4em 0.6em;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.8em;
|
||||
font-weight: bold;
|
||||
color: #7C7D80;
|
||||
display: flex;
|
||||
justify-content: stretch;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.c-cs__ui__header-label {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.c-cs__ui_content {
|
||||
padding: 0.4em;
|
||||
}
|
||||
|
||||
.c-cs__ui_content .help {
|
||||
font-style: italic;
|
||||
padding: 0.4em 0;
|
||||
}
|
||||
|
||||
.current-output {
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
margin: 0.4em 0.6em;
|
||||
}
|
||||
|
||||
.condition-output {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.condition-description {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.checkbox.custom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 0.6em;
|
||||
}
|
||||
.checkbox.custom span {
|
||||
display: inline-block;
|
||||
margin-left: 0.6em;
|
||||
}
|
||||
|
||||
.t-test-data-config {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.widget-condition form {
|
||||
padding: 0.5em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.widget-condition form label {
|
||||
flex-grow: 1;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.widget-condition form input {
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
.c-cs-button[class*="--major"],
|
||||
.c-cs-button[class*='is-active'],
|
||||
.c-cs-button--menu[class*="--major"],
|
||||
.c-cs-button--menu[class*='is-active'] {
|
||||
border: solid 1.5px #0B427C;
|
||||
background-color: #4778A3;
|
||||
padding: 0.2em 0.6em;
|
||||
margin: 0.4em;
|
||||
font-weight: bold;
|
||||
color: #eee;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.c-cs__disclosure-triangle,
|
||||
.c-cs__menu-hamburger,
|
||||
.c-cs__duplicate,
|
||||
.c-cs__trash {
|
||||
$d: 8px;
|
||||
color: $colorDisclosureCtrl;
|
||||
width: $d;
|
||||
visibility: hidden;
|
||||
|
||||
&.is-enabled {
|
||||
cursor: pointer;
|
||||
visibility: visible;
|
||||
|
||||
&:hover {
|
||||
color: $colorDisclosureCtrlHov;
|
||||
}
|
||||
|
||||
&:before {
|
||||
$s: .5;
|
||||
display: block;
|
||||
font-family: symbolsfont;
|
||||
font-size: 1rem * $s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.c-cs__disclosure-triangle {
|
||||
&:before {
|
||||
content: $glyph-icon-arrow-right;
|
||||
}
|
||||
|
||||
&--expanded {
|
||||
&:before {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.c-cs__duplicate {
|
||||
margin-right: 0.3em;
|
||||
&:before {
|
||||
content: $glyph-icon-duplicate;
|
||||
}
|
||||
}
|
||||
|
||||
.c-cs__trash {
|
||||
&:before {
|
||||
content: $glyph-icon-trash;
|
||||
}
|
||||
}
|
||||
|
@ -1,154 +0,0 @@
|
||||
|
||||
|
||||
.widget-condition {
|
||||
background-color: #eee;
|
||||
margin: 0 0 0.6em;
|
||||
border-radius: 3px;
|
||||
|
||||
&--current {
|
||||
background-color: #DEECFA;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.title-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: stretch;
|
||||
padding: 0.4em 0;
|
||||
}
|
||||
|
||||
.title-bar span {
|
||||
margin-right: 0.6em;
|
||||
}
|
||||
|
||||
.title-bar span.c-c__duplicate,
|
||||
.title-bar span.c-c__trash{
|
||||
margin-right: 0;
|
||||
margin-left: 0.3em;
|
||||
}
|
||||
|
||||
.widget-condition > div {
|
||||
margin: 0 0.4em;
|
||||
}
|
||||
|
||||
.condition-name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.condition-summary .condition-description {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.condition-summary {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.condition-config {
|
||||
padding: 0.3em 0;
|
||||
}
|
||||
|
||||
.widget-condition form label {
|
||||
flex-grow: 1;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.l-widget-rule {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.l-compact-form ul li {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.widget-rule-content.expanded {
|
||||
margin: 0 3px;
|
||||
}
|
||||
|
||||
.widget-rule-content.expanded ul {
|
||||
border-top: solid 1px #ccc;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.l-compact-form ul li .controls {
|
||||
display: inline-flex;
|
||||
flex-grow: inherit;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.l-compact-form ul li > label {
|
||||
display: block;
|
||||
width: 90px;
|
||||
min-width: 90px;
|
||||
text-align: right;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.l-compact-form ul li .controls input[type="text"],
|
||||
.l-compact-form ul li .controls input[type="search"],
|
||||
.l-compact-form ul li .controls input[type="number"],
|
||||
.l-compact-form ul li .controls button,
|
||||
.l-compact-form ul li .controls select {
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
.condition-config-edit {
|
||||
padding: 3px 0;
|
||||
}
|
||||
|
||||
|
||||
.c-c__disclosure-triangle,
|
||||
.c-c__menu-hamburger,
|
||||
.c-c__duplicate,
|
||||
.c-c__trash {
|
||||
$d: 8px;
|
||||
color: $colorDisclosureCtrl;
|
||||
width: $d;
|
||||
visibility: hidden;
|
||||
|
||||
&.is-enabled {
|
||||
cursor: pointer;
|
||||
visibility: visible;
|
||||
|
||||
&:hover {
|
||||
color: $colorDisclosureCtrlHov;
|
||||
}
|
||||
|
||||
&:before {
|
||||
$s: .5;
|
||||
display: block;
|
||||
font-family: symbolsfont;
|
||||
font-size: 1rem * $s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.c-c__disclosure-triangle {
|
||||
&:before {
|
||||
content: $glyph-icon-arrow-right;
|
||||
}
|
||||
|
||||
&--expanded {
|
||||
&:before {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.c-c__menu-hamburger {
|
||||
&:before {
|
||||
content: $glyph-icon-menu-hamburger;
|
||||
}
|
||||
}
|
||||
|
||||
.c-c__duplicate {
|
||||
&:before {
|
||||
content: $glyph-icon-duplicate;
|
||||
}
|
||||
}
|
||||
|
||||
.c-c__trash {
|
||||
&:before {
|
||||
content: $glyph-icon-trash;
|
||||
}
|
||||
}
|
311
src/plugins/condition/components/conditionals.scss
Normal file
311
src/plugins/condition/components/conditionals.scss
Normal file
@ -0,0 +1,311 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
/***************************** DRAGGING */
|
||||
.is-active-dragging {
|
||||
.c-condition-h__drop-target {
|
||||
height: 3px;
|
||||
margin-bottom: $interiorMarginSm;
|
||||
}
|
||||
}
|
||||
|
||||
.c-condition-h {
|
||||
&__drop-target {
|
||||
border-radius: $controlCr;
|
||||
height: 0;
|
||||
min-height: 0;
|
||||
transition: background-color, height;
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
|
||||
&.is-drag-target {
|
||||
.c-condition > * {
|
||||
pointer-events: none; // Keeps the JS drop handler from being intercepted by internal elements
|
||||
}
|
||||
|
||||
.c-condition-h__drop-target {
|
||||
background-color: rgba($colorKey, 0.7);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.c-cs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
/************************** CONDITION SET LAYOUT */
|
||||
&__current-output {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
&__test-data-and-conditions-w {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__test-data,
|
||||
&__conditions {
|
||||
flex: 0 0 auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__test-data {
|
||||
flex: 0 0 auto;
|
||||
max-height: 50%;
|
||||
|
||||
&.is-expanded {
|
||||
margin-bottom: $interiorMargin * 4;
|
||||
}
|
||||
}
|
||||
|
||||
&__conditions {
|
||||
flex: 1 1 auto;
|
||||
|
||||
> * + * {
|
||||
margin-top: $interiorMarginSm;
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 0 1 auto;
|
||||
overflow: hidden;
|
||||
|
||||
> * {
|
||||
flex: 0 0 auto;
|
||||
overflow: hidden;
|
||||
+ * {
|
||||
margin-top: $interiorMarginSm;
|
||||
}
|
||||
}
|
||||
|
||||
.c-button {
|
||||
align-self: start;
|
||||
}
|
||||
}
|
||||
|
||||
.is-editing & {
|
||||
// Add some space to kick away from blue editing border indication
|
||||
padding: $interiorMargin;
|
||||
}
|
||||
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__conditions-h {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
padding-right: $interiorMarginSm;
|
||||
|
||||
> * + * {
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/***************************** 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;
|
||||
}
|
||||
}
|
@ -0,0 +1,185 @@
|
||||
/*****************************************************************************
|
||||
* 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>
|
||||
<li class="c-tree__item-h">
|
||||
<div
|
||||
class="c-tree__item"
|
||||
:class="{ 'is-alias': isAlias, 'is-navigated-object': navigated }"
|
||||
@click="handleItemSelected(node.object, node)"
|
||||
>
|
||||
<view-control
|
||||
v-model="expanded"
|
||||
class="c-tree__item__view-control"
|
||||
:enabled="hasChildren"
|
||||
:propagate="false"
|
||||
/>
|
||||
<div class="c-tree__item__label c-object-label">
|
||||
<div
|
||||
class="c-tree__item__type-icon c-object-label__type-icon"
|
||||
:class="typeClass"
|
||||
></div>
|
||||
<div class="c-tree__item__name c-object-label__name">{{ node.object.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul
|
||||
v-if="expanded"
|
||||
class="c-tree"
|
||||
>
|
||||
<li
|
||||
v-if="isLoading && !loaded"
|
||||
class="c-tree__item-h"
|
||||
>
|
||||
<div class="c-tree__item loading">
|
||||
<span class="c-tree__item__label">Loading...</span>
|
||||
</div>
|
||||
</li>
|
||||
<condition-set-dialog-tree-item
|
||||
v-for="child in children"
|
||||
:key="child.id"
|
||||
:node="child"
|
||||
:selected-item="selectedItem"
|
||||
:handle-item-selected="handleItemSelected"
|
||||
/>
|
||||
</ul>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import viewControl from '@/ui/components/viewControl.vue';
|
||||
|
||||
export default {
|
||||
name: 'ConditionSetDialogTreeItem',
|
||||
inject: ['openmct'],
|
||||
components: {
|
||||
viewControl
|
||||
},
|
||||
props: {
|
||||
node: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
selectedItem: {
|
||||
type: Object,
|
||||
default() {
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
handleItemSelected: {
|
||||
type: Function,
|
||||
default() {
|
||||
return (item) => {};
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hasChildren: false,
|
||||
isLoading: false,
|
||||
loaded: false,
|
||||
children: [],
|
||||
expanded: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
navigated() {
|
||||
const itemId = this.selectedItem && this.selectedItem.itemId;
|
||||
const isSelectedObject = itemId && this.openmct.objects.areIdsEqual(this.node.object.identifier, itemId);
|
||||
if (isSelectedObject && this.node.objectPath && this.node.objectPath.length > 1) {
|
||||
const isParent = this.openmct.objects.areIdsEqual(this.node.objectPath[1].identifier, this.selectedItem.parentId);
|
||||
return isSelectedObject && isParent;
|
||||
}
|
||||
return isSelectedObject;
|
||||
},
|
||||
isAlias() {
|
||||
let parent = this.node.objectPath[1];
|
||||
if (!parent) {
|
||||
return false;
|
||||
}
|
||||
let parentKeyString = this.openmct.objects.makeKeyString(parent.identifier);
|
||||
return parentKeyString !== this.node.object.location;
|
||||
},
|
||||
typeClass() {
|
||||
let type = this.openmct.types.get(this.node.object.type);
|
||||
if (!type) {
|
||||
return 'icon-object-unknown';
|
||||
}
|
||||
return type.definition.cssClass;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
expanded() {
|
||||
if (!this.hasChildren) {
|
||||
return;
|
||||
}
|
||||
if (!this.loaded && !this.isLoading) {
|
||||
this.composition = this.openmct.composition.get(this.domainObject);
|
||||
this.composition.on('add', this.addChild);
|
||||
this.composition.on('remove', this.removeChild);
|
||||
this.composition.load().then(this.finishLoading);
|
||||
this.isLoading = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.domainObject = this.node.object;
|
||||
let removeListener = this.openmct.objects.observe(this.domainObject, '*', (newObject) => {
|
||||
this.domainObject = newObject;
|
||||
});
|
||||
|
||||
this.$once('hook:destroyed', removeListener);
|
||||
if (this.openmct.composition.get(this.node.object)) {
|
||||
this.hasChildren = true;
|
||||
}
|
||||
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.expanded = false;
|
||||
},
|
||||
destroyed() {
|
||||
if (this.composition) {
|
||||
this.composition.off('add', this.addChild);
|
||||
this.composition.off('remove', this.removeChild);
|
||||
delete this.composition;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addChild(child) {
|
||||
this.children.push({
|
||||
id: this.openmct.objects.makeKeyString(child.identifier),
|
||||
object: child,
|
||||
objectPath: [child].concat(this.node.objectPath),
|
||||
navigateToParent: this.navigateToPath
|
||||
});
|
||||
},
|
||||
removeChild(identifier) {
|
||||
let removeId = this.openmct.objects.makeKeyString(identifier);
|
||||
this.children = this.children
|
||||
.filter(c => c.id !== removeId);
|
||||
},
|
||||
finishLoading() {
|
||||
this.isLoading = false;
|
||||
this.loaded = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -0,0 +1,172 @@
|
||||
/*****************************************************************************
|
||||
* 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="u-contents">
|
||||
<div class="c-overlay__top-bar">
|
||||
<div class="c-overlay__dialog-title">Select Condition Set</div>
|
||||
</div>
|
||||
<div class="c-overlay__contents-main c-selector c-tree-and-search">
|
||||
<div class="c-tree-and-search__search">
|
||||
<search ref="shell-search"
|
||||
class="c-search"
|
||||
:value="searchValue"
|
||||
@input="searchTree"
|
||||
@clear="searchTree"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- loading -->
|
||||
<div v-if="isLoading"
|
||||
class="c-tree-and-search__loading loading"
|
||||
></div>
|
||||
<!-- end loading -->
|
||||
|
||||
<div v-if="(allTreeItems.length === 0) || (searchValue && filteredTreeItems.length === 0)"
|
||||
class="c-tree-and-search__no-results"
|
||||
>
|
||||
No results found
|
||||
</div>
|
||||
|
||||
<!-- main tree -->
|
||||
<ul v-if="!isLoading"
|
||||
v-show="!searchValue"
|
||||
class="c-tree-and-search__tree c-tree"
|
||||
>
|
||||
<condition-set-dialog-tree-item
|
||||
v-for="treeItem in allTreeItems"
|
||||
:key="treeItem.id"
|
||||
:node="treeItem"
|
||||
:selected-item="selectedItem"
|
||||
:handle-item-selected="handleItemSelection"
|
||||
/>
|
||||
</ul>
|
||||
<!-- end main tree -->
|
||||
|
||||
<!-- search tree -->
|
||||
<ul v-if="searchValue"
|
||||
class="c-tree-and-search__tree c-tree"
|
||||
>
|
||||
<condition-set-dialog-tree-item
|
||||
v-for="treeItem in filteredTreeItems"
|
||||
:key="treeItem.id"
|
||||
:node="treeItem"
|
||||
:selected-item="selectedItem"
|
||||
:handle-item-selected="handleItemSelection"
|
||||
/>
|
||||
</ul>
|
||||
<!-- end search tree -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import search from '@/ui/components/search.vue';
|
||||
import ConditionSetDialogTreeItem from './ConditionSetDialogTreeItem.vue';
|
||||
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
name: 'ConditionSetSelectorDialog',
|
||||
components: {
|
||||
search,
|
||||
ConditionSetDialogTreeItem
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
expanded: false,
|
||||
searchValue: '',
|
||||
allTreeItems: [],
|
||||
filteredTreeItems: [],
|
||||
isLoading: false,
|
||||
selectedItem: undefined
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.searchService = this.openmct.$injector.get('searchService');
|
||||
this.getAllChildren();
|
||||
},
|
||||
methods: {
|
||||
getAllChildren() {
|
||||
this.isLoading = true;
|
||||
this.openmct.objects.get('ROOT')
|
||||
.then(root => {
|
||||
return this.openmct.composition.get(root).load()
|
||||
})
|
||||
.then(children => {
|
||||
this.isLoading = false;
|
||||
this.allTreeItems = children.map(c => {
|
||||
return {
|
||||
id: this.openmct.objects.makeKeyString(c.identifier),
|
||||
object: c,
|
||||
objectPath: [c],
|
||||
navigateToParent: '/browse'
|
||||
};
|
||||
});
|
||||
});
|
||||
},
|
||||
getFilteredChildren() {
|
||||
this.searchService.query(this.searchValue).then(children => {
|
||||
this.filteredTreeItems = children.hits.map(child => {
|
||||
|
||||
let context = child.object.getCapability('context'),
|
||||
object = child.object.useCapability('adapter'),
|
||||
objectPath = [],
|
||||
navigateToParent;
|
||||
|
||||
if (context) {
|
||||
objectPath = context.getPath().slice(1)
|
||||
.map(oldObject => oldObject.useCapability('adapter'))
|
||||
.reverse();
|
||||
navigateToParent = '/browse/' + objectPath.slice(1)
|
||||
.map((parent) => this.openmct.objects.makeKeyString(parent.identifier))
|
||||
.join('/');
|
||||
}
|
||||
|
||||
return {
|
||||
id: this.openmct.objects.makeKeyString(object.identifier),
|
||||
object,
|
||||
objectPath,
|
||||
navigateToParent
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
searchTree(value) {
|
||||
this.searchValue = value;
|
||||
|
||||
if (this.searchValue !== '') {
|
||||
this.getFilteredChildren();
|
||||
}
|
||||
},
|
||||
handleItemSelection(item, node) {
|
||||
if (item && item.type === 'conditionSet') {
|
||||
const parentId = (node.objectPath && node.objectPath.length > 1) ? node.objectPath[1].identifier : undefined;
|
||||
this.selectedItem = {
|
||||
itemId: item.identifier,
|
||||
parentId
|
||||
};
|
||||
this.$emit('conditionSetSelected', item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -0,0 +1,457 @@
|
||||
/*****************************************************************************
|
||||
* 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">
|
||||
<template v-if="!conditionSetDomainObject">
|
||||
<div class="c-inspect-styles__header">
|
||||
Object Style
|
||||
</div>
|
||||
<div class="c-inspect-styles__content">
|
||||
<div v-if="staticStyle"
|
||||
class="c-inspect-styles__style"
|
||||
>
|
||||
<style-editor class="c-inspect-styles__editor"
|
||||
:style-item="staticStyle"
|
||||
:is-editing="isEditing"
|
||||
@persist="updateStaticStyle"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
id="addConditionSet"
|
||||
class="c-button c-button--major c-toggle-styling-button labeled"
|
||||
@click="addConditionSet"
|
||||
>
|
||||
<span class="c-cs-button__label">Use Conditional Styling...</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="c-inspect-styles__header">
|
||||
Conditional Object Styles
|
||||
</div>
|
||||
<div class="c-inspect-styles__content c-inspect-styles__condition-set">
|
||||
<a v-if="conditionSetDomainObject"
|
||||
class="c-object-label icon-conditional"
|
||||
:href="navigateToPath"
|
||||
@click="navigateOrPreview"
|
||||
>
|
||||
<span class="c-object-label__name">{{ conditionSetDomainObject.name }}</span>
|
||||
</a>
|
||||
<template v-if="isEditing">
|
||||
<button
|
||||
id="changeConditionSet"
|
||||
class="c-button labeled"
|
||||
@click="addConditionSet"
|
||||
>
|
||||
<span class="c-button__label">Change...</span>
|
||||
</button>
|
||||
|
||||
<button class="c-click-icon icon-x"
|
||||
title="Remove conditional styles"
|
||||
@click="removeConditionSet"
|
||||
></button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div v-if="conditionsLoaded"
|
||||
class="c-inspect-styles__conditions"
|
||||
>
|
||||
<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)"
|
||||
/>
|
||||
<condition-description :show-label="true"
|
||||
:condition="getCondition(conditionStyle.conditionId)"
|
||||
/>
|
||||
<style-editor class="c-inspect-styles__editor"
|
||||
:style-item="conditionStyle"
|
||||
:is-editing="isEditing"
|
||||
@persist="updateConditionalStyle"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import StyleEditor from "./StyleEditor.vue";
|
||||
import ConditionSetSelectorDialog from "./ConditionSetSelectorDialog.vue";
|
||||
import ConditionDescription from "@/plugins/condition/components/ConditionDescription.vue";
|
||||
import ConditionError from "@/plugins/condition/components/ConditionError.vue";
|
||||
import Vue from 'vue';
|
||||
import PreviewAction from "@/ui/preview/PreviewAction.js";
|
||||
import {getApplicableStylesForItem} from "@/plugins/condition/utils/styleUtils";
|
||||
|
||||
export default {
|
||||
name: 'ConditionalStylesView',
|
||||
components: {
|
||||
ConditionDescription,
|
||||
ConditionError,
|
||||
StyleEditor
|
||||
},
|
||||
inject: [
|
||||
'openmct',
|
||||
'selection'
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
conditionalStyles: [],
|
||||
staticStyle: undefined,
|
||||
conditionSetDomainObject: undefined,
|
||||
isEditing: this.openmct.editor.isEditing(),
|
||||
conditions: undefined,
|
||||
conditionsLoaded: false,
|
||||
navigateToPath: '',
|
||||
selectedConditionId: ''
|
||||
}
|
||||
},
|
||||
destroyed() {
|
||||
this.removeListeners();
|
||||
},
|
||||
mounted() {
|
||||
this.itemId = '';
|
||||
this.getDomainObjectFromSelection();
|
||||
this.previewAction = new PreviewAction(this.openmct);
|
||||
if (this.domainObject.configuration && this.domainObject.configuration.objectStyles) {
|
||||
let objectStyles = this.itemId ? this.domainObject.configuration.objectStyles[this.itemId] : this.domainObject.configuration.objectStyles;
|
||||
this.initializeStaticStyle(objectStyles);
|
||||
if (objectStyles && objectStyles.conditionSetIdentifier) {
|
||||
this.openmct.objects.get(objectStyles.conditionSetIdentifier).then(this.initialize);
|
||||
this.conditionalStyles = objectStyles.styles;
|
||||
}
|
||||
} else {
|
||||
this.initializeStaticStyle();
|
||||
}
|
||||
this.openmct.editor.on('isEditing', this.setEditState);
|
||||
},
|
||||
methods: {
|
||||
isItemType(type, item) {
|
||||
return item && (item.type === type);
|
||||
},
|
||||
getDomainObjectFromSelection() {
|
||||
let layoutItem;
|
||||
let domainObject;
|
||||
|
||||
if (this.selection[0].length > 1) {
|
||||
//If there are more than 1 items in the this.selection[0] list, the first one could either be a sub domain object OR a layout drawing control.
|
||||
//The second item in the this.selection[0] list is the container object (usually a layout)
|
||||
layoutItem = this.selection[0][0].context.layoutItem;
|
||||
const item = this.selection[0][0].context.item;
|
||||
this.canHide = true;
|
||||
if (item &&
|
||||
(!layoutItem || (this.isItemType('subobject-view', layoutItem)))) {
|
||||
domainObject = item;
|
||||
} else {
|
||||
domainObject = this.selection[0][1].context.item;
|
||||
if (layoutItem) {
|
||||
this.itemId = layoutItem.id;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
domainObject = this.selection[0][0].context.item;
|
||||
}
|
||||
this.domainObject = domainObject;
|
||||
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.stopObservingItems) {
|
||||
this.stopObservingItems();
|
||||
}
|
||||
if (this.stopProvidingTelemetry) {
|
||||
this.stopProvidingTelemetry();
|
||||
}
|
||||
},
|
||||
initialize(conditionSetDomainObject) {
|
||||
//If there are new conditions in the conditionSet we need to set those styles to default
|
||||
this.conditionSetDomainObject = conditionSetDomainObject;
|
||||
this.enableConditionSetNav();
|
||||
this.initializeConditionalStyles();
|
||||
},
|
||||
setEditState(isEditing) {
|
||||
this.isEditing = isEditing;
|
||||
if (this.isEditing) {
|
||||
if (this.stopProvidingTelemetry) {
|
||||
this.stopProvidingTelemetry();
|
||||
}
|
||||
} else {
|
||||
this.subscribeToConditionSet();
|
||||
}
|
||||
},
|
||||
addConditionSet() {
|
||||
let conditionSetDomainObject;
|
||||
const handleItemSelection = (item) => {
|
||||
if (item) {
|
||||
conditionSetDomainObject = item;
|
||||
}
|
||||
};
|
||||
const dismissDialog = (overlay, initialize) => {
|
||||
overlay.dismiss();
|
||||
if (initialize && conditionSetDomainObject) {
|
||||
this.conditionSetDomainObject = conditionSetDomainObject;
|
||||
this.conditionalStyles = [];
|
||||
this.initializeConditionalStyles();
|
||||
}
|
||||
};
|
||||
let vm = new Vue({
|
||||
provide: {
|
||||
openmct: this.openmct
|
||||
},
|
||||
components: {ConditionSetSelectorDialog},
|
||||
data() {
|
||||
return {
|
||||
handleItemSelection
|
||||
}
|
||||
},
|
||||
template: '<condition-set-selector-dialog @conditionSetSelected="handleItemSelection"></condition-set-selector-dialog>'
|
||||
}).$mount();
|
||||
|
||||
let overlay = this.openmct.overlays.overlay({
|
||||
element: vm.$el,
|
||||
size: 'small',
|
||||
buttons: [
|
||||
{
|
||||
label: 'OK',
|
||||
emphasis: 'true',
|
||||
callback: () => dismissDialog(overlay, true)
|
||||
},
|
||||
{
|
||||
label: 'Cancel',
|
||||
callback: () => dismissDialog(overlay, false)
|
||||
}
|
||||
],
|
||||
onDestroy: () => vm.$destroy()
|
||||
});
|
||||
},
|
||||
enableConditionSetNav() {
|
||||
this.openmct.objects.getOriginalPath(this.conditionSetDomainObject.identifier).then(
|
||||
(objectPath) => {
|
||||
this.objectPath = objectPath;
|
||||
this.navigateToPath = '#/browse/' + this.objectPath
|
||||
.map(o => o && this.openmct.objects.makeKeyString(o.identifier))
|
||||
.reverse()
|
||||
.join('/');
|
||||
}
|
||||
);
|
||||
},
|
||||
navigateOrPreview(event) {
|
||||
// If editing, display condition set in Preview overlay; otherwise nav to it while browsing
|
||||
if (this.openmct.editor.isEditing()) {
|
||||
event.preventDefault();
|
||||
this.previewAction.invoke(this.objectPath);
|
||||
}
|
||||
},
|
||||
removeConditionSet() {
|
||||
this.conditionSetDomainObject = undefined;
|
||||
this.conditionalStyles = [];
|
||||
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;
|
||||
if (_.isEmpty(domainObjectStyles[this.itemId])) {
|
||||
delete domainObjectStyles[this.itemId];
|
||||
}
|
||||
} else {
|
||||
domainObjectStyles.conditionSetIdentifier = undefined;
|
||||
domainObjectStyles.selectedConditionId = undefined;
|
||||
domainObjectStyles.defaultConditionId = undefined;
|
||||
delete domainObjectStyles.conditionSetIdentifier;
|
||||
domainObjectStyles.styles = undefined;
|
||||
delete domainObjectStyles.styles;
|
||||
}
|
||||
if (_.isEmpty(domainObjectStyles)) {
|
||||
domainObjectStyles = undefined;
|
||||
}
|
||||
|
||||
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) {
|
||||
this.conditions = {};
|
||||
}
|
||||
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) {
|
||||
foundStyle.style = Object.assign((this.canHide ? { isStyleInvisible: '' } : {}), this.initialStyles, foundStyle.style);
|
||||
conditionalStyles.push(foundStyle);
|
||||
} else {
|
||||
conditionalStyles.splice(index, 0, {
|
||||
conditionId: conditionConfiguration.id,
|
||||
style: Object.assign((this.canHide ? { isStyleInvisible: '' } : {}), this.initialStyles)
|
||||
});
|
||||
}
|
||||
});
|
||||
//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.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;
|
||||
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);
|
||||
},
|
||||
updateStaticStyle(staticStyle) {
|
||||
this.staticStyle = staticStyle;
|
||||
this.persist(this.getDomainObjectConditionalStyle());
|
||||
},
|
||||
updateConditionalStyle(conditionStyle) {
|
||||
let found = this.findStyleByConditionId(conditionStyle.conditionId);
|
||||
if (found) {
|
||||
found.style = conditionStyle.style;
|
||||
this.selectedConditionId = found.conditionId;
|
||||
this.persist(this.getDomainObjectConditionalStyle());
|
||||
}
|
||||
},
|
||||
getDomainObjectConditionalStyle(defaultConditionId) {
|
||||
let objectStyle = {
|
||||
styles: this.conditionalStyles,
|
||||
staticStyle: this.staticStyle,
|
||||
selectedConditionId: this.selectedConditionId
|
||||
};
|
||||
if (defaultConditionId) {
|
||||
objectStyle.defaultConditionId = defaultConditionId;
|
||||
}
|
||||
if (this.conditionSetDomainObject) {
|
||||
objectStyle.conditionSetIdentifier = this.conditionSetDomainObject.identifier;
|
||||
}
|
||||
|
||||
let domainObjectStyles = (this.domainObject.configuration && this.domainObject.configuration.objectStyles) || {};
|
||||
|
||||
if (this.itemId) {
|
||||
domainObjectStyles[this.itemId] = objectStyle;
|
||||
} else {
|
||||
//we're deconstructing here to ensure that if an item within a domainObject already had a style we don't lose it
|
||||
domainObjectStyles = {
|
||||
...domainObjectStyles,
|
||||
...objectStyle
|
||||
}
|
||||
}
|
||||
|
||||
return domainObjectStyles;
|
||||
},
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -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>
|
215
src/plugins/condition/components/inspector/StyleEditor.vue
Normal file
215
src/plugins/condition/components/inspector/StyleEditor.vue
Normal file
@ -0,0 +1,215 @@
|
||||
/*****************************************************************************
|
||||
* 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-style">
|
||||
<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': !hasProperty(styleItem.style.color) }"
|
||||
>
|
||||
ABC
|
||||
</span>
|
||||
</span>
|
||||
<span class="c-toolbar">
|
||||
<toolbar-color-picker v-if="hasProperty(styleItem.style.border)"
|
||||
class="c-style__toolbar-button--border-color u-menu-to--center"
|
||||
:options="borderColorOption"
|
||||
@change="updateStyleValue"
|
||||
/>
|
||||
<toolbar-color-picker v-if="hasProperty(styleItem.style.backgroundColor)"
|
||||
class="c-style__toolbar-button--background-color u-menu-to--center"
|
||||
:options="backgroundColorOption"
|
||||
@change="updateStyleValue"
|
||||
/>
|
||||
<toolbar-color-picker v-if="hasProperty(styleItem.style.color)"
|
||||
class="c-style__toolbar-button--color u-menu-to--center"
|
||||
:options="colorOption"
|
||||
@change="updateStyleValue"
|
||||
/>
|
||||
<toolbar-button v-if="hasProperty(styleItem.style.imageUrl)"
|
||||
class="c-style__toolbar-button--image-url"
|
||||
:options="imageUrlOption"
|
||||
@change="updateStyleValue"
|
||||
/>
|
||||
<toolbar-toggle-button v-if="hasProperty(styleItem.style.isStyleInvisible)"
|
||||
class="c-style__toolbar-button--toggle-visible"
|
||||
:options="isStyleInvisibleOption"
|
||||
@change="updateStyleValue"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
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',
|
||||
components: {
|
||||
ToolbarButton,
|
||||
ToolbarColorPicker,
|
||||
ToolbarToggleButton
|
||||
},
|
||||
inject: [
|
||||
'openmct'
|
||||
],
|
||||
props: {
|
||||
isEditing: {
|
||||
type: Boolean
|
||||
},
|
||||
mixedStyles: {
|
||||
type: Array,
|
||||
default() {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
styleItem: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
itemStyle() {
|
||||
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.normalizeValueForSwatch(value),
|
||||
property: 'border',
|
||||
isEditing: this.isEditing,
|
||||
nonSpecific: this.mixedStyles.indexOf('border') > -1
|
||||
}
|
||||
},
|
||||
backgroundColorOption() {
|
||||
let value = this.styleItem.style.backgroundColor;
|
||||
return {
|
||||
icon: 'icon-paint-bucket',
|
||||
title: STYLE_CONSTANTS.backgroundColorTitle,
|
||||
value: this.normalizeValueForSwatch(value),
|
||||
property: 'backgroundColor',
|
||||
isEditing: this.isEditing,
|
||||
nonSpecific: this.mixedStyles.indexOf('backgroundColor') > -1
|
||||
}
|
||||
},
|
||||
colorOption() {
|
||||
let value = this.styleItem.style.color;
|
||||
return {
|
||||
icon: 'icon-font',
|
||||
title: STYLE_CONSTANTS.textColorTitle,
|
||||
value: this.normalizeValueForSwatch(value),
|
||||
property: 'color',
|
||||
isEditing: this.isEditing,
|
||||
nonSpecific: this.mixedStyles.indexOf('color') > -1
|
||||
}
|
||||
},
|
||||
imageUrlOption() {
|
||||
return {
|
||||
icon: 'icon-image',
|
||||
title: STYLE_CONSTANTS.imagePropertiesTitle,
|
||||
dialog: {
|
||||
name: "Image Properties",
|
||||
sections: [
|
||||
{
|
||||
rows: [
|
||||
{
|
||||
key: "url",
|
||||
control: "textfield",
|
||||
name: "Image URL",
|
||||
"cssClass": "l-input-lg"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
property: 'imageUrl',
|
||||
formKeys: ['url'],
|
||||
value: {url: this.styleItem.style.imageUrl},
|
||||
isEditing: this.isEditing,
|
||||
nonSpecific: this.mixedStyles.indexOf('imageUrl') > -1
|
||||
}
|
||||
},
|
||||
isStyleInvisibleOption() {
|
||||
return {
|
||||
value: this.styleItem.style.isStyleInvisible,
|
||||
property: 'isStyleInvisible',
|
||||
isEditing: this.isEditing,
|
||||
options: [
|
||||
{
|
||||
value: '',
|
||||
icon: 'icon-eye-disabled',
|
||||
title: STYLE_CONSTANTS.visibilityHidden
|
||||
},
|
||||
{
|
||||
value: STYLE_CONSTANTS.isStyleInvisible,
|
||||
icon: 'icon-eye-open',
|
||||
title: STYLE_CONSTANTS.visibilityVisible
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
hasProperty(property) {
|
||||
return property !== undefined;
|
||||
},
|
||||
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;
|
||||
}
|
||||
if (value && (value.url !== undefined)) {
|
||||
this.styleItem.style[item.property] = value.url;
|
||||
} else {
|
||||
this.styleItem.style[item.property] = value;
|
||||
}
|
||||
this.$emit('persist', this.styleItem, item.property);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -0,0 +1,149 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
/********************************************* INSPECTOR STYLES TAB */
|
||||
.c-inspect-styles {
|
||||
> * + * {
|
||||
margin-top: $interiorMargin;
|
||||
}
|
||||
|
||||
&__content,
|
||||
&__conditions,
|
||||
&__condition {
|
||||
> * + * {
|
||||
margin-top: $interiorMargin;
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__condition-set {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.c-object-label {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.c-button {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
&__style,
|
||||
&__condition {
|
||||
padding: $interiorMargin;
|
||||
}
|
||||
|
||||
&__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.5);
|
||||
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 {
|
||||
padding: 2px; // Allow a bit of room for thumb box-shadow
|
||||
|
||||
&__condition-desc {
|
||||
@include ellipsize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.c-inspect-styles__style {
|
||||
.is-editing & {
|
||||
border-bottom: 1px solid $colorInteriorBorder;
|
||||
}
|
||||
}
|
||||
|
||||
.l-shell:not(.is-editing) .c-inspect-styles {
|
||||
.c-toolbar {
|
||||
// Disabled-look toolbar when not editing
|
||||
pointer-events: none;
|
||||
cursor: inherit;
|
||||
|
||||
// Hide control buttons, like image URL
|
||||
[class*='--image-url'] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// Make buttons look disabled by knocking back icon, not swatch element
|
||||
.c-icon-button {
|
||||
&:before {
|
||||
opacity: $controlDisabledOpacity;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.c-toggle-styling-button {
|
||||
display: none;
|
||||
|
||||
.is-editing & {
|
||||
display: block;
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.is-style-invisible {
|
||||
display: none !important;
|
||||
|
||||
.is-editing & {
|
||||
display: block !important;
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
&.c-style-thumb {
|
||||
display: block !important;
|
||||
background-color: transparent !important;
|
||||
border-color: transparent !important;
|
||||
@include bgCheckerboard($size: 10px, $imp: true);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
166
src/plugins/condition/criterion/AllTelemetryCriterion.js
Normal file
166
src/plugins/condition/criterion/AllTelemetryCriterion.js
Normal file
@ -0,0 +1,166 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
import TelemetryCriterion from './TelemetryCriterion';
|
||||
import { evaluateResults } from "../utils/evaluator";
|
||||
import { getLatestTimestamp } from '../utils/time';
|
||||
|
||||
export default class AllTelemetryCriterion extends TelemetryCriterion {
|
||||
|
||||
/**
|
||||
* Subscribes/Unsubscribes to telemetry and emits the result
|
||||
* of operations performed on the telemetry data returned and a given input value.
|
||||
* @constructor
|
||||
* @param telemetryDomainObjectDefinition {id: uuid, operation: enum, input: Array, metadata: string, key: {domainObject.identifier} }
|
||||
* @param openmct
|
||||
*/
|
||||
constructor(telemetryDomainObjectDefinition, openmct) {
|
||||
super(telemetryDomainObjectDefinition, openmct);
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.telemetryObjects = { ...this.telemetryDomainObjectDefinition.telemetryObjects };
|
||||
this.telemetryDataCache = {};
|
||||
}
|
||||
|
||||
isValid() {
|
||||
return (this.telemetry === 'any' || this.telemetry === 'all') && this.metadata && this.operation;
|
||||
}
|
||||
|
||||
updateTelemetry(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) {
|
||||
if (data) {
|
||||
this.telemetryDataCache[data.id] = this.computeResult(data);
|
||||
}
|
||||
|
||||
let keys = Object.keys(telemetryObjects);
|
||||
keys.forEach((key) => {
|
||||
let telemetryObject = telemetryObjects[key];
|
||||
const id = this.openmct.objects.makeKeyString(telemetryObject.identifier);
|
||||
if (this.telemetryDataCache[id] === undefined) {
|
||||
this.telemetryDataCache[id] = false;
|
||||
}
|
||||
});
|
||||
|
||||
const datum = {
|
||||
result: evaluateResults(Object.values(this.telemetryDataCache), this.telemetry)
|
||||
};
|
||||
|
||||
if (data) {
|
||||
this.openmct.time.getAllTimeSystems().forEach(timeSystem => {
|
||||
datum[timeSystem.key] = data[timeSystem.key]
|
||||
});
|
||||
}
|
||||
return datum;
|
||||
}
|
||||
|
||||
getResult(data, telemetryObjects) {
|
||||
const validatedData = this.isValid() ? data : {};
|
||||
|
||||
if (validatedData) {
|
||||
this.telemetryDataCache[validatedData.id] = this.computeResult(validatedData);
|
||||
}
|
||||
|
||||
Object.values(telemetryObjects).forEach(telemetryObject => {
|
||||
const id = this.openmct.objects.makeKeyString(telemetryObject.identifier);
|
||||
if (this.telemetryDataCache[id] === undefined) {
|
||||
this.telemetryDataCache[id] = false;
|
||||
}
|
||||
});
|
||||
|
||||
this.result = evaluateResults(Object.values(this.telemetryDataCache), this.telemetry);
|
||||
}
|
||||
|
||||
requestLAD(telemetryObjects) {
|
||||
const options = {
|
||||
strategy: 'latest',
|
||||
size: 1
|
||||
};
|
||||
|
||||
if (!this.isValid()) {
|
||||
return this.formatData({}, telemetryObjects);
|
||||
}
|
||||
|
||||
let keys = Object.keys(Object.assign({}, telemetryObjects));
|
||||
const telemetryRequests = keys
|
||||
.map(key => this.openmct.telemetry.request(
|
||||
telemetryObjects[key],
|
||||
options
|
||||
));
|
||||
|
||||
let telemetryDataCache = {};
|
||||
return Promise.all(telemetryRequests)
|
||||
.then(telemetryRequestsResults => {
|
||||
let latestTimestamp;
|
||||
const timeSystems = this.openmct.time.getAllTimeSystems();
|
||||
const timeSystem = this.openmct.time.timeSystem();
|
||||
|
||||
telemetryRequestsResults.forEach((results, index) => {
|
||||
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: datum
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
destroy() {
|
||||
delete this.telemetryObjects;
|
||||
delete this.telemetryDataCache;
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2019, 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.
|
||||
*
|
||||
@ -20,69 +20,142 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import * as EventEmitter from 'eventemitter3';
|
||||
import EventEmitter from 'EventEmitter';
|
||||
import { OPERATIONS } from '../utils/operations';
|
||||
|
||||
export default class TelemetryCriterion extends EventEmitter {
|
||||
|
||||
/**
|
||||
* Subscribes/Unsubscribes to telemetry and emits the result
|
||||
* of operations performed on the telemetry data returned and a given input value.
|
||||
* @constructor
|
||||
* @param telemetryDomainObjectDefinition {id: uuid, operation: enum, input: Array, metadata: string, key: {domainObject.identifier} }
|
||||
* @param openmct
|
||||
*/
|
||||
constructor(telemetryDomainObjectDefinition, openmct) {
|
||||
super();
|
||||
|
||||
this.openmct = openmct;
|
||||
this.telemetryDomainObjectDefinition = telemetryDomainObjectDefinition;
|
||||
this.id = telemetryDomainObjectDefinition.id;
|
||||
this.subscription = null;
|
||||
this.telemetryMetadata = null;
|
||||
this.telemetryObjectIdAsString = null;
|
||||
openmct.objects.get(openmct.objects.makeKeyString(telemetryDomainObjectDefinition.key)).then((obj) => {
|
||||
if (openmct.telemetry.isTelemetryObject(obj)) {
|
||||
this.telemetryObject = obj;
|
||||
this.telemetryObjectIdAsString = openmct.objects.makeKeyString(this.telemetryObject.identifier);
|
||||
this.telemetryMetadata = openmct.telemetry.getMetadata(this.telemetryObject.identifier);
|
||||
this.emit('criterionUpdated', this);
|
||||
this.telemetry = telemetryDomainObjectDefinition.telemetry;
|
||||
this.operation = telemetryDomainObjectDefinition.operation;
|
||||
this.input = telemetryDomainObjectDefinition.input;
|
||||
this.metadata = telemetryDomainObjectDefinition.metadata;
|
||||
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) {
|
||||
this.openmct.time.getAllTimeSystems().forEach(timeSystem => {
|
||||
datum[timeSystem.key] = data[timeSystem.key]
|
||||
});
|
||||
}
|
||||
return datum;
|
||||
}
|
||||
|
||||
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) {
|
||||
for (let i=0, ii=OPERATIONS.length; i < ii; i++) {
|
||||
if (operation === OPERATIONS[i].name) {
|
||||
return OPERATIONS[i].operation;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleSubscription(datum) {
|
||||
//data is telemetry values, error
|
||||
//how do I get data here?
|
||||
this.emitResult(this.normalizeData(datum));
|
||||
}
|
||||
|
||||
//TODO: Revisit this logic
|
||||
normalizeData(datum) {
|
||||
return {
|
||||
[datum.key]: datum[datum.source]
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
emitResult(data, error) {
|
||||
this.emit('criterionUpdated', {
|
||||
identifier: this.id,
|
||||
data: data,
|
||||
error: error
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to the telemetry object and returns an unsubscribe function
|
||||
*/
|
||||
subscribe(openmct) {
|
||||
this.subscription = openmct.telemetry.subscribe(this.telemetryObject, (datum) => {
|
||||
this.handleSubscription(datum);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls an unsubscribe function returned by subscribe() and deletes any initialized data
|
||||
*/
|
||||
unsubscribe() {
|
||||
//unsubscribe from telemetry source
|
||||
if (typeof this.subscription === 'function') {
|
||||
this.subscription();
|
||||
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));
|
||||
}
|
||||
if (typeof comparator === 'function') {
|
||||
result = !!comparator(params);
|
||||
}
|
||||
}
|
||||
delete this.subscription;
|
||||
this.emit('criterion::Remove', this.telemetryObjectIdAsString);
|
||||
delete this.telemetryObjectIdAsString;
|
||||
return result;
|
||||
}
|
||||
|
||||
emitEvent(eventName, data) {
|
||||
this.emit(eventName, {
|
||||
id: this.id,
|
||||
data: data
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
destroy() {
|
||||
delete this.telemetryObject;
|
||||
delete this.telemetryMetadata;
|
||||
delete this.telemetryObjectIdAsString;
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2019, 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,6 +24,7 @@ import TelemetryCriterion from "./TelemetryCriterion";
|
||||
|
||||
let openmct = {},
|
||||
mockListener,
|
||||
testCriterionDefinition,
|
||||
testTelemetryObject,
|
||||
telemetryCriterion;
|
||||
|
||||
@ -35,36 +36,64 @@ describe("The telemetry criterion", function () {
|
||||
type: "test-object",
|
||||
name: "Test Object",
|
||||
telemetry: {
|
||||
values: [{
|
||||
key: "some-key",
|
||||
name: "Some attribute",
|
||||
valueMetadatas: [{
|
||||
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"
|
||||
}]
|
||||
}
|
||||
};
|
||||
openmct.objects = jasmine.createSpyObj('objects', ['makeKeyString']);
|
||||
// openmct.objects.get.and.returnValue(testTelemetryObject);
|
||||
openmct.objects = jasmine.createSpyObj('objects', ['get', 'makeKeyString']);
|
||||
openmct.objects.makeKeyString.and.returnValue(testTelemetryObject.identifier.key);
|
||||
openmct.telemetry = jasmine.createSpyObj('telemetry', ["subscribe"]);
|
||||
// openmct.telemetry.isTelemetryObject.and.returnValue(true);
|
||||
openmct.telemetry = jasmine.createSpyObj('telemetry', ['isTelemetryObject', "subscribe", "getMetadata", "getValueFormatter"]);
|
||||
openmct.telemetry.isTelemetryObject.and.returnValue(true);
|
||||
openmct.telemetry.subscribe.and.returnValue(function () {});
|
||||
openmct.telemetry.getValueFormatter.and.returnValue({
|
||||
parse: function (value) {
|
||||
return value;
|
||||
}
|
||||
});
|
||||
openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry);
|
||||
|
||||
openmct.time = jasmine.createSpyObj('timeAPI',
|
||||
['timeSystem', 'bounds', 'getAllTimeSystems']
|
||||
);
|
||||
openmct.time.timeSystem.and.returnValue({key: 'system'});
|
||||
openmct.time.bounds.and.returnValue({start: 0, end: 1});
|
||||
openmct.time.getAllTimeSystems.and.returnValue([{key: 'system'}]);
|
||||
|
||||
testCriterionDefinition = {
|
||||
id: 'test-criterion-id',
|
||||
telemetry: openmct.objects.makeKeyString(testTelemetryObject.identifier),
|
||||
operation: 'textContains',
|
||||
metadata: 'value',
|
||||
input: ['Hell'],
|
||||
telemetryObject: testTelemetryObject
|
||||
};
|
||||
|
||||
mockListener = jasmine.createSpy('listener');
|
||||
|
||||
telemetryCriterion = new TelemetryCriterion(
|
||||
testTelemetryObject,
|
||||
testCriterionDefinition,
|
||||
openmct
|
||||
);
|
||||
|
||||
telemetryCriterion.on('criterionUpdated', mockListener);
|
||||
telemetryCriterion.on('criterionResultUpdated', mockListener);
|
||||
|
||||
});
|
||||
|
||||
@ -72,41 +101,21 @@ describe("The telemetry criterion", function () {
|
||||
expect(telemetryCriterion.telemetryObjectIdAsString).toEqual(testTelemetryObject.identifier.key);
|
||||
});
|
||||
|
||||
it("subscribes to telemetry providers", function () {
|
||||
telemetryCriterion.subscribe();
|
||||
expect(telemetryCriterion.subscription).toBeDefined();
|
||||
});
|
||||
|
||||
it("normalizes telemetry data", function () {
|
||||
let result = telemetryCriterion.normalizeData({
|
||||
key: 'some-key',
|
||||
source: 'testSource',
|
||||
testSource: 'Hello'
|
||||
it("returns a result on new data from relevant telemetry providers", function () {
|
||||
telemetryCriterion.getResult({
|
||||
value: 'Hello',
|
||||
utc: 'Hi',
|
||||
id: testTelemetryObject.identifier.key
|
||||
});
|
||||
expect(result).toEqual({
|
||||
'some-key': 'Hello'
|
||||
})
|
||||
});
|
||||
|
||||
it("emits update event on new data from telemetry providers", function () {
|
||||
spyOn(telemetryCriterion, 'emitResult').and.callThrough();
|
||||
telemetryCriterion.handleSubscription({
|
||||
key: 'some-key',
|
||||
source: 'testSource',
|
||||
testSource: 'Hello'
|
||||
});
|
||||
expect(telemetryCriterion.emitResult).toHaveBeenCalled();
|
||||
expect(mockListener).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("un-subscribes from telemetry providers", function () {
|
||||
telemetryCriterion.subscribe();
|
||||
expect(telemetryCriterion.subscription).toBeDefined();
|
||||
telemetryCriterion.unsubscribe();
|
||||
expect(telemetryCriterion.subscription).toBeUndefined();
|
||||
expect(telemetryCriterion.telemetryObjectIdAsString).toBeUndefined();
|
||||
expect(telemetryCriterion.telemetryObject).toBeUndefined();
|
||||
expect(telemetryCriterion.telemetryMetadata).toBeUndefined();
|
||||
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();
|
||||
// });
|
||||
});
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2019, 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.
|
||||
*
|
||||
@ -20,37 +20,48 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import ConditionSetViewProvider from './ConditionSetViewProvider.js';
|
||||
import ConditionSetCompositionPolicy from "@/plugins/condition/ConditionSetCompositionPolicy";
|
||||
import ConditionSetCompositionPolicy from "./ConditionSetCompositionPolicy";
|
||||
import ConditionSetMetadataProvider from './ConditionSetMetadataProvider';
|
||||
import ConditionSetTelemetryProvider from './ConditionSetTelemetryProvider';
|
||||
import ConditionSetViewPolicy from './ConditionSetViewPolicy';
|
||||
import uuid from "uuid";
|
||||
|
||||
export default function ConditionPlugin() {
|
||||
|
||||
return function install(openmct) {
|
||||
openmct.types.addType('condition', {
|
||||
name: 'Condition',
|
||||
key: 'condition',
|
||||
description: 'A list of criteria which will be evaluated based on a trigger',
|
||||
creatable: false,
|
||||
initialize: function (domainObject) {
|
||||
domainObject.composition = [];
|
||||
}
|
||||
});
|
||||
|
||||
openmct.types.addType('conditionSet', {
|
||||
name: 'Condition Set',
|
||||
key: 'conditionSet',
|
||||
description: 'A set of one or more conditions based on user-specified criteria.',
|
||||
description: 'Monitor and evaluate telemetry values in real-time with a wide variety of criteria. Use to control the styling of many objects in Open MCT.',
|
||||
creatable: true,
|
||||
cssClass: 'icon-summary-widget', // TODO: replace with class for new icon
|
||||
cssClass: 'icon-conditional',
|
||||
initialize: function (domainObject) {
|
||||
domainObject.configuration = {
|
||||
conditionCollection: []
|
||||
conditionTestData: [],
|
||||
conditionCollection: [{
|
||||
isDefault: true,
|
||||
id: uuid(),
|
||||
configuration: {
|
||||
name: 'Default',
|
||||
output: 'Default',
|
||||
trigger: 'all',
|
||||
criteria: []
|
||||
},
|
||||
summary: 'Default condition'
|
||||
}]
|
||||
};
|
||||
domainObject.composition = [];
|
||||
domainObject.telemetry = {};
|
||||
}
|
||||
});
|
||||
|
||||
openmct.legacyExtension('policies', {
|
||||
category: 'view',
|
||||
implementation: ConditionSetViewPolicy
|
||||
});
|
||||
openmct.composition.addPolicy(new ConditionSetCompositionPolicy(openmct).allow);
|
||||
|
||||
openmct.telemetry.addProvider(new ConditionSetMetadataProvider(openmct));
|
||||
openmct.telemetry.addProvider(new ConditionSetTelemetryProvider(openmct));
|
||||
openmct.objectViews.addProvider(new ConditionSetViewProvider(openmct));
|
||||
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2019, 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.
|
||||
*
|
||||
@ -26,26 +26,14 @@ import ConditionPlugin from "./plugin";
|
||||
let openmct = createOpenMct();
|
||||
openmct.install(new ConditionPlugin());
|
||||
|
||||
let conditionDefinition;
|
||||
let conditionSetDefinition;
|
||||
let mockDomainObject;
|
||||
let mockDomainObject2;
|
||||
let mockConditionSetDomainObject;
|
||||
let element;
|
||||
let child;
|
||||
|
||||
describe('the plugin', function () {
|
||||
|
||||
beforeAll((done) => {
|
||||
conditionDefinition = openmct.types.get('condition').definition;
|
||||
mockDomainObject = {
|
||||
identifier: {
|
||||
key: 'testConditionKey',
|
||||
namespace: ''
|
||||
},
|
||||
type: 'condition'
|
||||
};
|
||||
|
||||
conditionDefinition.initialize(mockDomainObject);
|
||||
|
||||
conditionSetDefinition = openmct.types.get('conditionSet').definition;
|
||||
const appHolder = document.createElement('div');
|
||||
@ -56,7 +44,7 @@ describe('the plugin', function () {
|
||||
child = document.createElement('div');
|
||||
element.appendChild(child);
|
||||
|
||||
mockDomainObject2 = {
|
||||
mockConditionSetDomainObject = {
|
||||
identifier: {
|
||||
key: 'testConditionSetKey',
|
||||
namespace: ''
|
||||
@ -64,22 +52,12 @@ describe('the plugin', function () {
|
||||
type: 'conditionSet'
|
||||
};
|
||||
|
||||
conditionSetDefinition.initialize(mockDomainObject2);
|
||||
conditionSetDefinition.initialize(mockConditionSetDomainObject);
|
||||
|
||||
openmct.on('start', done);
|
||||
openmct.start(appHolder);
|
||||
});
|
||||
|
||||
let mockConditionObject = {
|
||||
name: 'Condition',
|
||||
key: 'condition',
|
||||
creatable: false
|
||||
};
|
||||
|
||||
it('defines a condition object type with the correct key', () => {
|
||||
expect(conditionDefinition.key).toEqual(mockConditionObject.key);
|
||||
});
|
||||
|
||||
let mockConditionSetObject = {
|
||||
name: 'Condition Set',
|
||||
key: 'conditionSet',
|
||||
@ -90,18 +68,6 @@ describe('the plugin', function () {
|
||||
expect(conditionSetDefinition.key).toEqual(mockConditionSetObject.key);
|
||||
});
|
||||
|
||||
describe('the condition object', () => {
|
||||
|
||||
it('is not creatable', () => {
|
||||
expect(conditionDefinition.creatable).toEqual(mockConditionObject.creatable);
|
||||
});
|
||||
|
||||
it('initializes with an empty composition list', () => {
|
||||
expect(mockDomainObject.composition instanceof Array).toBeTrue();
|
||||
expect(mockDomainObject.composition.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('the conditionSet object', () => {
|
||||
|
||||
it('is creatable', () => {
|
||||
@ -109,14 +75,17 @@ describe('the plugin', function () {
|
||||
});
|
||||
|
||||
it('initializes with an empty composition list', () => {
|
||||
expect(mockDomainObject2.composition instanceof Array).toBeTrue();
|
||||
expect(mockDomainObject2.composition.length).toEqual(0);
|
||||
expect(mockConditionSetDomainObject.composition instanceof Array).toBeTrue();
|
||||
expect(mockConditionSetDomainObject.composition.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('provides a view', () => {
|
||||
const testViewObject = {
|
||||
id:"test-object",
|
||||
type: "conditionSet"
|
||||
type: "conditionSet",
|
||||
configuration: {
|
||||
conditionCollection: []
|
||||
}
|
||||
};
|
||||
|
||||
const applicableViews = openmct.objectViews.get(testViewObject);
|
||||
|
@ -1,4 +1,54 @@
|
||||
/*****************************************************************************
|
||||
* 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 const TRIGGER = {
|
||||
ANY: 'any',
|
||||
ALL: 'all'
|
||||
ALL: 'all',
|
||||
NOT: 'not',
|
||||
XOR: 'xor'
|
||||
};
|
||||
|
||||
export const TRIGGER_LABEL = {
|
||||
'any': 'when any criteria are met',
|
||||
'all': 'when all criteria are met',
|
||||
'not': 'when no criteria are met',
|
||||
'xor': 'when only one criteria is met'
|
||||
};
|
||||
|
||||
export const STYLE_CONSTANTS = {
|
||||
isStyleInvisible: 'is-style-invisible',
|
||||
borderColorTitle: 'Set border color',
|
||||
textColorTitle: 'Set text color',
|
||||
backgroundColorTitle: 'Set background color',
|
||||
imagePropertiesTitle: 'Edit image properties',
|
||||
visibilityHidden: 'Hidden',
|
||||
visibilityVisible: 'Visible'
|
||||
};
|
||||
|
||||
export const ERROR = {
|
||||
'TELEMETRY_NOT_FOUND': {
|
||||
errorText: 'Telemetry not found for criterion'
|
||||
},
|
||||
'CONDITION_NOT_FOUND': {
|
||||
errorText: 'Condition not found'
|
||||
}
|
||||
};
|
||||
|
@ -1,7 +1,68 @@
|
||||
export const computeConditionForAny = (args) => {
|
||||
return false;
|
||||
};
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
import { TRIGGER } from "./constants";
|
||||
|
||||
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 true;
|
||||
}
|
||||
|
||||
function matchAny(results) {
|
||||
for (const result of results) {
|
||||
if (result) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export const computeConditionForAll = (args) => {
|
||||
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;
|
||||
}
|
||||
|
204
src/plugins/condition/utils/evaluatorSpec.js
Normal file
204
src/plugins/condition/utils/evaluatorSpec.js
Normal file
@ -0,0 +1,204 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
import { evaluateResults } from './evaluator';
|
||||
import { TRIGGER } from './constants';
|
||||
|
||||
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 = [];
|
||||
|
||||
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])
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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])
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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])
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
// });
|
||||
});
|
@ -1,98 +1,144 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
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',
|
||||
operation: function (input) {
|
||||
return input[0] === input[1];
|
||||
return Number(input[0]) === Number(input[1]);
|
||||
},
|
||||
text: 'is equal to',
|
||||
appliesTo: ['number'],
|
||||
inputCount: 1,
|
||||
getDescription: function (values) {
|
||||
return ' == ' + values[0];
|
||||
return ' is ' + joinValues(values, 1);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'notEqualTo',
|
||||
operation: function (input) {
|
||||
return input[0] !== input[1];
|
||||
return Number(input[0]) !== Number(input[1]);
|
||||
},
|
||||
text: 'is not equal to',
|
||||
appliesTo: ['number'],
|
||||
inputCount: 1,
|
||||
getDescription: function (values) {
|
||||
return ' != ' + values[0];
|
||||
return ' is not ' + joinValues(values, 1);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'greaterThan',
|
||||
operation: function (input) {
|
||||
return input[0] > input[1];
|
||||
return Number(input[0]) > Number(input[1]);
|
||||
},
|
||||
text: 'is greater than',
|
||||
appliesTo: ['number'],
|
||||
inputCount: 1,
|
||||
getDescription: function (values) {
|
||||
return ' > ' + values[0];
|
||||
return ' > ' + joinValues(values, 1);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lessThan',
|
||||
operation: function (input) {
|
||||
return input[0] < input[1];
|
||||
return Number(input[0]) < Number(input[1]);
|
||||
},
|
||||
text: 'is less than',
|
||||
appliesTo: ['number'],
|
||||
inputCount: 1,
|
||||
getDescription: function (values) {
|
||||
return ' < ' + values[0];
|
||||
return ' < ' + joinValues(values, 1);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'greaterThanOrEq',
|
||||
operation: function (input) {
|
||||
return input[0] >= input[1];
|
||||
return Number(input[0]) >= Number(input[1]);
|
||||
},
|
||||
text: 'is greater than or equal to',
|
||||
appliesTo: ['number'],
|
||||
inputCount: 1,
|
||||
getDescription: function (values) {
|
||||
return ' >= ' + values[0];
|
||||
return ' >= ' + joinValues(values, 1);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'lessThanOrEq',
|
||||
operation: function (input) {
|
||||
return input[0] <= input[1];
|
||||
return Number(input[0]) <= Number(input[1]);
|
||||
},
|
||||
text: 'is less than or equal to',
|
||||
appliesTo: ['number'],
|
||||
inputCount: 1,
|
||||
getDescription: function (values) {
|
||||
return ' <= ' + values[0];
|
||||
return ' <= ' + joinValues(values, 1);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'between',
|
||||
operation: function (input) {
|
||||
return input[0] > input[1] && input[0] < input[2];
|
||||
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);
|
||||
},
|
||||
text: 'is between',
|
||||
appliesTo: ['number'],
|
||||
inputCount: 2,
|
||||
getDescription: function (values) {
|
||||
return ' between ' + values[0] + ' and ' + values[1];
|
||||
return ' is between ' + values[0] + ' and ' + values[1];
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'notBetween',
|
||||
operation: function (input) {
|
||||
return input[0] < input[1] || input[0] > input[2];
|
||||
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);
|
||||
},
|
||||
text: 'is not between',
|
||||
appliesTo: ['number'],
|
||||
inputCount: 2,
|
||||
getDescription: function (values) {
|
||||
return ' not between ' + values[0] + ' and ' + values[1];
|
||||
return ' is not between ' + values[0] + ' and ' + values[1];
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -104,7 +150,7 @@ export const OPERATIONS = [
|
||||
appliesTo: ['string'],
|
||||
inputCount: 1,
|
||||
getDescription: function (values) {
|
||||
return ' contains ' + values[0];
|
||||
return ' contains ' + joinValues(values, 1);
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -116,7 +162,7 @@ export const OPERATIONS = [
|
||||
appliesTo: ['string'],
|
||||
inputCount: 1,
|
||||
getDescription: function (values) {
|
||||
return ' does not contain ' + values[0];
|
||||
return ' does not contain ' + joinValues(values, 1);
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -128,7 +174,7 @@ export const OPERATIONS = [
|
||||
appliesTo: ['string'],
|
||||
inputCount: 1,
|
||||
getDescription: function (values) {
|
||||
return ' starts with ' + values[0];
|
||||
return ' starts with ' + joinValues(values, 1);
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -140,7 +186,7 @@ export const OPERATIONS = [
|
||||
appliesTo: ['string'],
|
||||
inputCount: 1,
|
||||
getDescription: function (values) {
|
||||
return ' ends with ' + values[0];
|
||||
return ' ends with ' + joinValues(values, 1);
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -152,7 +198,7 @@ export const OPERATIONS = [
|
||||
appliesTo: ['string'],
|
||||
inputCount: 1,
|
||||
getDescription: function (values) {
|
||||
return ' is exactly ' + values[0];
|
||||
return ' is exactly ' + joinValues(values, 1);
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -182,25 +228,67 @@ 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 ' == ' + values[0];
|
||||
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 ' != ' + values[0];
|
||||
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) => lhsValue === _.trim(value.toString()));
|
||||
}
|
||||
return false;
|
||||
},
|
||||
text: 'is one of',
|
||||
appliesTo: ["string", "number"],
|
||||
inputCount: 1,
|
||||
getDescription: function (values) {
|
||||
return ' is one of ' + values[0];
|
||||
}
|
||||
},
|
||||
{
|
||||
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) => lhsValue === _.trim(value.toString()));
|
||||
return !found;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
text: 'is not one of',
|
||||
appliesTo: ["string", "number"],
|
||||
inputCount: 1,
|
||||
getDescription: function (values) {
|
||||
return ' is not one of ' + values[0];
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
export const INPUT_TYPES = {
|
||||
'string': 'text',
|
||||
'number': 'number'
|
||||
};
|
||||
|
142
src/plugins/condition/utils/operationsSpec.js
Normal file
142
src/plugins/condition/utils/operationsSpec.js
Normal file
@ -0,0 +1,142 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
import { OPERATIONS } from "./operations";
|
||||
let isOneOfOperation = OPERATIONS.find((operation) => operation.name === 'valueIs');
|
||||
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('operations', function () {
|
||||
|
||||
it('should evaluate isOneOf to true for number inputs', () => {
|
||||
const inputs = [45, "5,6,45,8"];
|
||||
expect(!!isOneOfOperation.operation(inputs)).toBeTrue();
|
||||
});
|
||||
|
||||
it('should evaluate isOneOf to true for string inputs', () => {
|
||||
const inputs = ["45", " 45, 645, 4,8 "];
|
||||
expect(!!isOneOfOperation.operation(inputs)).toBeTrue();
|
||||
});
|
||||
|
||||
it('should evaluate isNotOneOf to true for number inputs', () => {
|
||||
const inputs = [45, "5,6,4,8"];
|
||||
expect(!!isNotOneOfOperation.operation(inputs)).toBeTrue();
|
||||
});
|
||||
|
||||
it('should evaluate isNotOneOf to true for string inputs', () => {
|
||||
const inputs = ["45", " 5,645, 4,8 "];
|
||||
expect(!!isNotOneOfOperation.operation(inputs)).toBeTrue();
|
||||
});
|
||||
|
||||
it('should evaluate isOneOf to false for number inputs', () => {
|
||||
const inputs = [4, "5, 6, 7, 8"];
|
||||
expect(!!isOneOfOperation.operation(inputs)).toBeFalse();
|
||||
});
|
||||
|
||||
it('should evaluate isOneOf to false for string inputs', () => {
|
||||
const inputs = ["4", "5,645 ,7,8"];
|
||||
expect(!!isOneOfOperation.operation(inputs)).toBeFalse();
|
||||
});
|
||||
|
||||
it('should evaluate isNotOneOf to false for number inputs', () => {
|
||||
const inputs = [4, "5,4, 7,8"];
|
||||
expect(!!isNotOneOfOperation.operation(inputs)).toBeFalse();
|
||||
});
|
||||
|
||||
it('should evaluate isNotOneOf to false for string inputs', () => {
|
||||
const inputs = ["4", "5,46,4,8"];
|
||||
expect(!!isNotOneOfOperation.operation(inputs)).toBeFalse();
|
||||
});
|
||||
|
||||
it('should evaluate isBetween to true', () => {
|
||||
const inputs = ["4", "3", "89"];
|
||||
expect(!!isBetween.operation(inputs)).toBeTrue();
|
||||
});
|
||||
|
||||
it('should evaluate isNotBetween to true', () => {
|
||||
const inputs = ["45", "100", "89"];
|
||||
expect(!!isNotBetween.operation(inputs)).toBeTrue();
|
||||
});
|
||||
|
||||
it('should evaluate isBetween to false', () => {
|
||||
const inputs = ["4", "100", "89"];
|
||||
expect(!!isBetween.operation(inputs)).toBeFalse();
|
||||
});
|
||||
|
||||
it('should evaluate isNotBetween to false', () => {
|
||||
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();
|
||||
});
|
||||
});
|
172
src/plugins/condition/utils/styleUtils.js
Normal file
172
src/plugins/condition/utils/styleUtils.js
Normal file
@ -0,0 +1,172 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
const NONE_VALUE = '__no_value';
|
||||
|
||||
const styleProps = {
|
||||
backgroundColor: {
|
||||
svgProperty: 'fill',
|
||||
noneValue: NONE_VALUE,
|
||||
applicableForType: type => {
|
||||
return !type ? true : (type === 'text-view' ||
|
||||
type === 'telemetry-view' ||
|
||||
type === 'box-view' ||
|
||||
type === 'subobject-view');
|
||||
}
|
||||
},
|
||||
border: {
|
||||
svgProperty: 'stroke',
|
||||
noneValue: NONE_VALUE,
|
||||
applicableForType: type => {
|
||||
return !type ? true : (type === 'text-view' ||
|
||||
type === 'telemetry-view' ||
|
||||
type === 'box-view' ||
|
||||
type === 'image-view' ||
|
||||
type === 'line-view'||
|
||||
type === 'subobject-view');
|
||||
}
|
||||
},
|
||||
color: {
|
||||
svgProperty: 'color',
|
||||
noneValue: NONE_VALUE,
|
||||
applicableForType: type => {
|
||||
return !type ? true : (type === 'text-view' ||
|
||||
type === 'telemetry-view'||
|
||||
type === 'subobject-view');
|
||||
}
|
||||
},
|
||||
imageUrl: {
|
||||
svgProperty: 'url',
|
||||
noneValue: '',
|
||||
applicableForType: type => {
|
||||
return !type ? false : type === 'image-view';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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) {
|
||||
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 getApplicableStylesForItem = (domainObject, item) => {
|
||||
const type = item && item.type;
|
||||
const id = item && item.id;
|
||||
let style = {};
|
||||
|
||||
let staticStyle = getStaticStyleForItem(domainObject, id);
|
||||
|
||||
const properties = Object.keys(styleProps);
|
||||
properties.forEach(property => {
|
||||
const styleProp = styleProps[property];
|
||||
if (styleProp.applicableForType(type)) {
|
||||
let defaultValue;
|
||||
if (staticStyle) {
|
||||
defaultValue = staticStyle[property];
|
||||
} else if (item) {
|
||||
defaultValue = item[styleProp.svgProperty];
|
||||
}
|
||||
style[property] = defaultValue === undefined ? styleProp.noneValue : defaultValue;
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
};
|
52
src/plugins/condition/utils/time.js
Normal file
52
src/plugins/condition/utils/time.js
Normal file
@ -0,0 +1,52 @@
|
||||
/*****************************************************************************
|
||||
* 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 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 => {
|
||||
latest[timeSystem.key] = timestamp[timeSystem.key];
|
||||
});
|
||||
|
||||
return latest;
|
||||
}
|
64
src/plugins/conditionWidget/ConditionWidgetViewProvider.js
Normal file
64
src/plugins/conditionWidget/ConditionWidgetViewProvider.js
Normal file
@ -0,0 +1,64 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
import ConditionWidgetComponent from './components/ConditionWidget.vue';
|
||||
import Vue from 'vue';
|
||||
|
||||
export default function ConditionWidget(openmct) {
|
||||
return {
|
||||
key: 'conditionWidget',
|
||||
name: 'Condition Widget',
|
||||
cssClass: 'icon-condition-widget',
|
||||
canView: function (domainObject) {
|
||||
return domainObject.type === 'conditionWidget';
|
||||
},
|
||||
canEdit: function (domainObject) {
|
||||
return domainObject.type === 'conditionWidget';
|
||||
},
|
||||
view: function (domainObject) {
|
||||
let component;
|
||||
|
||||
return {
|
||||
show: function (element) {
|
||||
component = new Vue({
|
||||
el: element,
|
||||
components: {
|
||||
ConditionWidgetComponent: ConditionWidgetComponent
|
||||
},
|
||||
provide: {
|
||||
openmct,
|
||||
domainObject
|
||||
},
|
||||
template: '<condition-widget-component></condition-widget-component>'
|
||||
});
|
||||
},
|
||||
destroy: function (element) {
|
||||
component.$destroy();
|
||||
component = undefined;
|
||||
}
|
||||
};
|
||||
},
|
||||
priority: function () {
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
}
|
61
src/plugins/conditionWidget/components/ConditionWidget.vue
Normal file
61
src/plugins/conditionWidget/components/ConditionWidget.vue
Normal file
@ -0,0 +1,61 @@
|
||||
/*****************************************************************************
|
||||
* 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>
|
||||
<component :is="urlDefined ? 'a' : 'span'"
|
||||
class="c-condition-widget"
|
||||
:href="urlDefined ? internalDomainObject.url : null"
|
||||
>
|
||||
<div class="c-condition-widget__label">
|
||||
{{ internalDomainObject.label }}
|
||||
</div>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject'],
|
||||
data: function () {
|
||||
return {
|
||||
internalDomainObject: this.domainObject
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
urlDefined() {
|
||||
return this.internalDomainObject.url && this.internalDomainObject.url.length > 0;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.unlisten = this.openmct.objects.observe(this.internalDomainObject, '*', this.updateInternalDomainObject);
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.unlisten) {
|
||||
this.unlisten();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateInternalDomainObject(domainObject) {
|
||||
this.internalDomainObject = domainObject;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
60
src/plugins/conditionWidget/components/condition-widget.scss
Normal file
60
src/plugins/conditionWidget/components/condition-widget.scss
Normal file
@ -0,0 +1,60 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
.c-condition-widget {
|
||||
$shdwSize: 3px;
|
||||
@include userSelectNone();
|
||||
background-color: rgba($colorBodyFg, 0.1); // Give a little presence if the user hasn't defined a fill color
|
||||
border-radius: $basicCr;
|
||||
border: 1px solid transparent;
|
||||
display: inline-block;
|
||||
padding: $interiorMarginLg $interiorMarginLg * 2;
|
||||
}
|
||||
|
||||
a.c-condition-widget {
|
||||
// Widget is conditionally made into a <a> when URL property has been defined
|
||||
cursor: pointer !important;
|
||||
pointer-events: inherit;
|
||||
}
|
||||
|
||||
// Make Condition Widget expand when in a hidden frame Layout context
|
||||
// For both static and Flexible Layouts
|
||||
.c-so-view--no-frame > .c-so-view__object-view > .c-condition-widget {
|
||||
@include abs();
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
// Add some margin when a Condition Widget is in a Flexible Layout
|
||||
.c-fl .c-so-view--no-frame .c-condition-widget {
|
||||
@include abs(1px);
|
||||
}
|
||||
|
||||
// When the widget is in the main view, center it in the space
|
||||
.l-shell__main-container > .c-condition-widget {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
58
src/plugins/conditionWidget/plugin.js
Normal file
58
src/plugins/conditionWidget/plugin.js
Normal file
@ -0,0 +1,58 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
import ConditionWidgetViewProvider from './ConditionWidgetViewProvider.js';
|
||||
|
||||
export default function plugin() {
|
||||
return function install(openmct) {
|
||||
openmct.objectViews.addProvider(new ConditionWidgetViewProvider(openmct));
|
||||
|
||||
openmct.types.addType('conditionWidget', {
|
||||
name: "Condition Widget",
|
||||
description: "A button that can be used on its own, or dynamically styled with a Condition Set.",
|
||||
creatable: true,
|
||||
cssClass: 'icon-condition-widget',
|
||||
initialize(domainObject) {
|
||||
domainObject.label = 'Condition Widget';
|
||||
},
|
||||
form: [
|
||||
{
|
||||
"key": "label",
|
||||
"name": "Label",
|
||||
"control": "textfield",
|
||||
property: [
|
||||
"label"
|
||||
],
|
||||
"required": true,
|
||||
"cssClass": "l-input"
|
||||
},
|
||||
{
|
||||
"key": "url",
|
||||
"name": "URL",
|
||||
"control": "textfield",
|
||||
"required": false,
|
||||
"cssClass": "l-input-lg"
|
||||
}
|
||||
]
|
||||
});
|
||||
};
|
||||
}
|
@ -347,78 +347,6 @@ define(['lodash'], function (_) {
|
||||
};
|
||||
}
|
||||
|
||||
function getFillMenu(selectedParent, selection) {
|
||||
return {
|
||||
control: "color-picker",
|
||||
domainObject: selectedParent,
|
||||
applicableSelectedItems: selection.filter(selectionPath => {
|
||||
let type = selectionPath[0].context.layoutItem.type;
|
||||
return type === 'text-view' ||
|
||||
type === 'telemetry-view' ||
|
||||
type === 'box-view';
|
||||
}),
|
||||
property: function (selectionPath) {
|
||||
return getPath(selectionPath) + ".fill";
|
||||
},
|
||||
icon: "icon-paint-bucket",
|
||||
title: "Set fill color"
|
||||
};
|
||||
}
|
||||
|
||||
function getStrokeMenu(selectedParent, selection) {
|
||||
return {
|
||||
control: "color-picker",
|
||||
domainObject: selectedParent,
|
||||
applicableSelectedItems: selection.filter(selectionPath => {
|
||||
let type = selectionPath[0].context.layoutItem.type;
|
||||
return type === 'text-view' ||
|
||||
type === 'telemetry-view' ||
|
||||
type === 'box-view' ||
|
||||
type === 'image-view' ||
|
||||
type === 'line-view';
|
||||
}),
|
||||
property: function (selectionPath) {
|
||||
return getPath(selectionPath) + ".stroke";
|
||||
},
|
||||
icon: "icon-line-horz",
|
||||
title: "Set border color"
|
||||
};
|
||||
}
|
||||
|
||||
function getTextColorMenu(selectedParent, selection) {
|
||||
return {
|
||||
control: "color-picker",
|
||||
domainObject: selectedParent,
|
||||
applicableSelectedItems: selection.filter(selectionPath => {
|
||||
let type = selectionPath[0].context.layoutItem.type;
|
||||
return type === 'text-view' || type === 'telemetry-view';
|
||||
}),
|
||||
property: function (selectionPath) {
|
||||
return getPath(selectionPath) + ".color";
|
||||
},
|
||||
icon: "icon-font",
|
||||
mandatory: true,
|
||||
title: "Set text color",
|
||||
preventNone: true
|
||||
};
|
||||
}
|
||||
|
||||
function getURLButton(selectedParent, selection) {
|
||||
return {
|
||||
control: "button",
|
||||
domainObject: selectedParent,
|
||||
applicableSelectedItems: selection.filter(selectionPath => {
|
||||
return selectionPath[0].context.layoutItem.type === 'image-view';
|
||||
}),
|
||||
property: function (selectionPath) {
|
||||
return getPath(selectionPath);
|
||||
},
|
||||
icon: "icon-image",
|
||||
title: "Edit image properties",
|
||||
dialog: DIALOG_FORM.image
|
||||
};
|
||||
}
|
||||
|
||||
function getTextButton(selectedParent, selection) {
|
||||
return {
|
||||
control: "button",
|
||||
@ -429,7 +357,7 @@ define(['lodash'], function (_) {
|
||||
property: function (selectionPath) {
|
||||
return getPath(selectionPath);
|
||||
},
|
||||
icon: "icon-gear",
|
||||
icon: "icon-font",
|
||||
title: "Edit text properties",
|
||||
dialog: DIALOG_FORM.text
|
||||
};
|
||||
@ -505,14 +433,14 @@ define(['lodash'], function (_) {
|
||||
|
||||
let toolbar = {
|
||||
'add-menu': [],
|
||||
'text': [],
|
||||
'url': [],
|
||||
'toggle-frame': [],
|
||||
'display-mode': [],
|
||||
'telemetry-value': [],
|
||||
'style': [],
|
||||
'text-style': [],
|
||||
'position': [],
|
||||
'text': [],
|
||||
'url': [],
|
||||
'remove': []
|
||||
};
|
||||
|
||||
@ -520,6 +448,10 @@ define(['lodash'], function (_) {
|
||||
let selectedParent = selectionPath[1].context.item;
|
||||
let layoutItem = selectionPath[0].context.layoutItem;
|
||||
|
||||
if (!layoutItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (layoutItem.type === 'subobject-view') {
|
||||
if (toolbar['add-menu'].length === 0 && selectionPath[0].context.item.type === 'layout') {
|
||||
toolbar['add-menu'] = [getAddButton(selectedObjects, selectionPath)];
|
||||
@ -546,15 +478,8 @@ define(['lodash'], function (_) {
|
||||
if (toolbar['telemetry-value'].length === 0) {
|
||||
toolbar['telemetry-value'] = [getTelemetryValueMenu(selectionPath, selectedObjects)];
|
||||
}
|
||||
if (toolbar.style.length < 2) {
|
||||
toolbar.style = [
|
||||
getFillMenu(selectedParent, selectedObjects),
|
||||
getStrokeMenu(selectedParent, selectedObjects)
|
||||
];
|
||||
}
|
||||
if (toolbar['text-style'].length === 0) {
|
||||
toolbar['text-style'] = [
|
||||
getTextColorMenu(selectedParent, selectedObjects),
|
||||
getTextSizeMenu(selectedParent, selectedObjects)
|
||||
];
|
||||
}
|
||||
@ -571,15 +496,8 @@ define(['lodash'], function (_) {
|
||||
toolbar.remove = [getRemoveButton(selectedParent, selectionPath, selectedObjects)];
|
||||
}
|
||||
} else if (layoutItem.type === 'text-view') {
|
||||
if (toolbar.style.length < 2) {
|
||||
toolbar.style = [
|
||||
getFillMenu(selectedParent, selectedObjects),
|
||||
getStrokeMenu(selectedParent, selectedObjects)
|
||||
];
|
||||
}
|
||||
if (toolbar['text-style'].length === 0) {
|
||||
toolbar['text-style'] = [
|
||||
getTextColorMenu(selectedParent, selectedObjects),
|
||||
getTextSizeMenu(selectedParent, selectedObjects)
|
||||
];
|
||||
}
|
||||
@ -599,12 +517,6 @@ define(['lodash'], function (_) {
|
||||
toolbar.remove = [getRemoveButton(selectedParent, selectionPath, selectedObjects)];
|
||||
}
|
||||
} else if (layoutItem.type === 'box-view') {
|
||||
if (toolbar.style.length < 2) {
|
||||
toolbar.style = [
|
||||
getFillMenu(selectedParent, selectedObjects),
|
||||
getStrokeMenu(selectedParent, selectedObjects)
|
||||
];
|
||||
}
|
||||
if (toolbar.position.length === 0) {
|
||||
toolbar.position = [
|
||||
getStackOrder(selectedParent, selectionPath),
|
||||
@ -618,11 +530,6 @@ define(['lodash'], function (_) {
|
||||
toolbar.remove = [getRemoveButton(selectedParent, selectionPath, selectedObjects)];
|
||||
}
|
||||
} else if (layoutItem.type === 'image-view') {
|
||||
if (toolbar.style.length === 0) {
|
||||
toolbar.style = [
|
||||
getStrokeMenu(selectedParent, selectedObjects)
|
||||
];
|
||||
}
|
||||
if (toolbar.position.length === 0) {
|
||||
toolbar.position = [
|
||||
getStackOrder(selectedParent, selectionPath),
|
||||
@ -632,18 +539,10 @@ define(['lodash'], function (_) {
|
||||
getWidthInput(selectedParent, selectedObjects)
|
||||
];
|
||||
}
|
||||
if (toolbar.url.length === 0) {
|
||||
toolbar.url = [getURLButton(selectedParent, selectedObjects)];
|
||||
}
|
||||
if (toolbar.remove.length === 0) {
|
||||
toolbar.remove = [getRemoveButton(selectedParent, selectionPath, selectedObjects)];
|
||||
}
|
||||
} else if (layoutItem.type === 'line-view') {
|
||||
if (toolbar.style.length === 0) {
|
||||
toolbar.style = [
|
||||
getStrokeMenu(selectedParent, selectedObjects)
|
||||
];
|
||||
}
|
||||
if (toolbar.position.length === 0) {
|
||||
toolbar.position = [
|
||||
getStackOrder(selectedParent, selectionPath),
|
||||
|
@ -23,20 +23,20 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="isEditing"
|
||||
class="c-properties"
|
||||
class="c-inspect-properties"
|
||||
>
|
||||
<div class="c-properties__header">
|
||||
<div class="c-inspect-properties__header">
|
||||
Alphanumeric Format
|
||||
</div>
|
||||
<ul class="c-properties__section">
|
||||
<li class="c-properties__row">
|
||||
<ul class="c-inspect-properties__section">
|
||||
<li class="c-inspect-properties__row">
|
||||
<div
|
||||
class="c-properties__label"
|
||||
class="c-inspect-properties__label"
|
||||
title="Printf formatting for the selected telemetry"
|
||||
>
|
||||
<label for="telemetryPrintfFormat">Format</label>
|
||||
</div>
|
||||
<div class="c-properties__value">
|
||||
<div class="c-inspect-properties__value">
|
||||
<input
|
||||
id="telemetryPrintfFormat"
|
||||
type="text"
|
||||
@ -85,7 +85,13 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
let format = selection[0][0].context.layoutItem.format;
|
||||
let layoutItem = selection[0][0].context.layoutItem;
|
||||
|
||||
if (!layoutItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
let format = layoutItem.format;
|
||||
this.nonMixedFormat = selection.every(selectionPath => {
|
||||
return selectionPath[0].context.layoutItem.format === format;
|
||||
});
|
||||
|
@ -29,6 +29,7 @@
|
||||
>
|
||||
<div
|
||||
class="c-box-view"
|
||||
:class="[styleClass]"
|
||||
:style="style"
|
||||
></div>
|
||||
</layout-frame>
|
||||
@ -36,12 +37,13 @@
|
||||
|
||||
<script>
|
||||
import LayoutFrame from './LayoutFrame.vue'
|
||||
import conditionalStylesMixin from '../mixins/objectStyles-mixin';
|
||||
|
||||
export default {
|
||||
makeDefinition() {
|
||||
return {
|
||||
fill: '#717171',
|
||||
stroke: 'transparent',
|
||||
stroke: '',
|
||||
x: 1,
|
||||
y: 1,
|
||||
width: 10,
|
||||
@ -52,6 +54,7 @@ export default {
|
||||
components: {
|
||||
LayoutFrame
|
||||
},
|
||||
mixins: [conditionalStylesMixin],
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
@ -71,10 +74,14 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
style() {
|
||||
return {
|
||||
backgroundColor: this.item.fill,
|
||||
border: '1px solid ' + this.item.stroke
|
||||
};
|
||||
if (this.itemStyle) {
|
||||
return this.itemStyle;
|
||||
} else {
|
||||
return {
|
||||
backgroundColor: this.item.fill,
|
||||
border: this.item.stroke ? '1px solid ' + this.item.stroke : ''
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
@ -29,6 +29,7 @@
|
||||
>
|
||||
<div
|
||||
class="c-image-view"
|
||||
:class="[styleClass]"
|
||||
:style="style"
|
||||
></div>
|
||||
</layout-frame>
|
||||
@ -36,6 +37,7 @@
|
||||
|
||||
<script>
|
||||
import LayoutFrame from './LayoutFrame.vue'
|
||||
import conditionalStylesMixin from "../mixins/objectStyles-mixin";
|
||||
|
||||
export default {
|
||||
makeDefinition(openmct, gridSize, element) {
|
||||
@ -52,6 +54,7 @@ export default {
|
||||
components: {
|
||||
LayoutFrame
|
||||
},
|
||||
mixins: [conditionalStylesMixin],
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
@ -71,10 +74,18 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
style() {
|
||||
return {
|
||||
backgroundImage: 'url(' + this.item.url + ')',
|
||||
border: '1px solid ' + this.item.stroke
|
||||
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,
|
||||
border
|
||||
};
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
@ -23,6 +23,7 @@
|
||||
<template>
|
||||
<div
|
||||
class="l-layout__frame c-frame no-frame"
|
||||
:class="[styleClass]"
|
||||
:style="style"
|
||||
>
|
||||
<svg
|
||||
@ -31,7 +32,7 @@
|
||||
>
|
||||
<line
|
||||
v-bind="linePosition"
|
||||
:stroke="item.stroke"
|
||||
:stroke="stroke"
|
||||
stroke-width="2"
|
||||
/>
|
||||
</svg>
|
||||
@ -60,6 +61,8 @@
|
||||
|
||||
<script>
|
||||
|
||||
import conditionalStylesMixin from "../mixins/objectStyles-mixin";
|
||||
|
||||
const START_HANDLE_QUADRANTS = {
|
||||
1: 'c-frame-edit__handle--sw',
|
||||
2: 'c-frame-edit__handle--se',
|
||||
@ -85,6 +88,7 @@ export default {
|
||||
};
|
||||
},
|
||||
inject: ['openmct'],
|
||||
mixins: [conditionalStylesMixin],
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
@ -122,6 +126,16 @@ export default {
|
||||
}
|
||||
return {x, y, x2, y2};
|
||||
},
|
||||
stroke() {
|
||||
if (this.itemStyle) {
|
||||
if (this.itemStyle.border) {
|
||||
return this.itemStyle.border.replace('1px solid ', '');
|
||||
}
|
||||
return '';
|
||||
} else {
|
||||
return this.item.stroke;
|
||||
}
|
||||
},
|
||||
style() {
|
||||
let {x, y, x2, y2} = this.position;
|
||||
let width = Math.max(this.gridSize[0] * Math.abs(x - x2), 1);
|
||||
|
@ -45,7 +45,7 @@ import LayoutFrame from './LayoutFrame.vue'
|
||||
const MINIMUM_FRAME_SIZE = [320, 180],
|
||||
DEFAULT_DIMENSIONS = [10, 10],
|
||||
DEFAULT_POSITION = [1, 1],
|
||||
DEFAULT_HIDDEN_FRAME_TYPES = ['hyperlink', 'summary-widget'];
|
||||
DEFAULT_HIDDEN_FRAME_TYPES = ['hyperlink', 'summary-widget', 'conditionWidget'];
|
||||
|
||||
function getDefaultDimensions(gridSize) {
|
||||
return MINIMUM_FRAME_SIZE.map((min, index) => {
|
||||
|
@ -30,6 +30,7 @@
|
||||
<div
|
||||
v-if="domainObject"
|
||||
class="c-telemetry-view"
|
||||
:class="styleClass"
|
||||
:style="styleObject"
|
||||
@contextmenu.prevent="showContextMenu"
|
||||
>
|
||||
@ -59,6 +60,7 @@
|
||||
<script>
|
||||
import LayoutFrame from './LayoutFrame.vue'
|
||||
import printj from 'printj'
|
||||
import conditionalStylesMixin from "../mixins/objectStyles-mixin";
|
||||
|
||||
const DEFAULT_TELEMETRY_DIMENSIONS = [10, 5],
|
||||
DEFAULT_POSITION = [1, 1],
|
||||
@ -77,8 +79,8 @@ export default {
|
||||
height: DEFAULT_TELEMETRY_DIMENSIONS[1],
|
||||
displayMode: 'all',
|
||||
value: metadata.getDefaultDisplayValue(),
|
||||
stroke: "transparent",
|
||||
fill: "transparent",
|
||||
stroke: "",
|
||||
fill: "",
|
||||
color: "",
|
||||
size: "13px"
|
||||
};
|
||||
@ -87,6 +89,7 @@ export default {
|
||||
components: {
|
||||
LayoutFrame
|
||||
},
|
||||
mixins: [conditionalStylesMixin],
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
@ -122,12 +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
|
||||
}
|
||||
}, this.itemStyle);
|
||||
|
||||
},
|
||||
fieldName() {
|
||||
return this.valueMetadata && this.valueMetadata.name;
|
||||
|
@ -29,21 +29,23 @@
|
||||
>
|
||||
<div
|
||||
class="c-text-view"
|
||||
:class="[styleClass]"
|
||||
:style="style"
|
||||
>
|
||||
{{ item.text }}
|
||||
<div class="c-text-view__text">{{ item.text }}</div>
|
||||
</div>
|
||||
</layout-frame>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import LayoutFrame from './LayoutFrame.vue'
|
||||
import conditionalStylesMixin from "../mixins/objectStyles-mixin";
|
||||
|
||||
export default {
|
||||
makeDefinition(openmct, gridSize, element) {
|
||||
return {
|
||||
fill: 'transparent',
|
||||
stroke: 'transparent',
|
||||
fill: '',
|
||||
stroke: '',
|
||||
size: '13px',
|
||||
color: '',
|
||||
x: 1,
|
||||
@ -57,6 +59,7 @@ export default {
|
||||
components: {
|
||||
LayoutFrame
|
||||
},
|
||||
mixins: [conditionalStylesMixin],
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
@ -76,12 +79,9 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
style() {
|
||||
return {
|
||||
backgroundColor: this.item.fill,
|
||||
borderColor: this.item.stroke,
|
||||
color: this.item.color,
|
||||
return Object.assign({
|
||||
fontSize: this.item.size
|
||||
};
|
||||
}, this.itemStyle);
|
||||
}
|
||||
},
|
||||
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();
|
||||
|
78
src/plugins/displayLayout/mixins/objectStyles-mixin.js
Normal file
78
src/plugins/displayLayout/mixins/objectStyles-mixin.js
Normal file
@ -0,0 +1,78 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
import StyleRuleManager from "@/plugins/condition/StyleRuleManager";
|
||||
import {getStylesWithoutNoneValue} from "@/plugins/condition/utils/styleUtils";
|
||||
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
data() {
|
||||
return {
|
||||
itemStyle: undefined,
|
||||
styleClass: ''
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.parentDomainObject = this.$parent.domainObject;
|
||||
this.itemId = this.item.id;
|
||||
this.objectStyle = this.getObjectStyleForItem(this.parentDomainObject.configuration.objectStyles);
|
||||
this.initObjectStyles();
|
||||
},
|
||||
destroyed() {
|
||||
if (this.stopListeningObjectStyles) {
|
||||
this.stopListeningObjectStyles();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getObjectStyleForItem(objectStyle) {
|
||||
if (objectStyle) {
|
||||
return objectStyle[this.itemId] ? Object.assign({}, objectStyle[this.itemId]) : undefined;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
initObjectStyles() {
|
||||
if (!this.styleRuleManager) {
|
||||
this.styleRuleManager = new StyleRuleManager(this.objectStyle, this.openmct, this.updateStyle.bind(this), true);
|
||||
} else {
|
||||
this.styleRuleManager.updateObjectStyleConfig(this.objectStyle);
|
||||
}
|
||||
|
||||
if (this.stopListeningObjectStyles) {
|
||||
this.stopListeningObjectStyles();
|
||||
}
|
||||
|
||||
this.stopListeningObjectStyles = this.openmct.objects.observe(this.parentDomainObject, 'configuration.objectStyles', (newObjectStyle) => {
|
||||
//Updating object styles in the inspector view will trigger this so that the changes are reflected immediately
|
||||
let newItemObjectStyle = this.getObjectStyleForItem(newObjectStyle);
|
||||
if (this.objectStyle !== newItemObjectStyle) {
|
||||
this.objectStyle = newItemObjectStyle;
|
||||
this.styleRuleManager.updateObjectStyleConfig(this.objectStyle);
|
||||
}
|
||||
});
|
||||
},
|
||||
updateStyle(style) {
|
||||
this.itemStyle = getStylesWithoutNoneValue(style);
|
||||
this.styleClass = this.itemStyle && this.itemStyle.isStyleInvisible;
|
||||
}
|
||||
}
|
||||
};
|
@ -1,17 +1,17 @@
|
||||
<template>
|
||||
<div class="c-properties__section c-filter-settings">
|
||||
<div class="c-inspect-properties__section c-filter-settings">
|
||||
<li
|
||||
v-for="(filter, index) in filterField.filters"
|
||||
:key="index"
|
||||
class="c-properties__row c-filter-settings__setting"
|
||||
class="c-inspect-properties__row c-filter-settings__setting"
|
||||
>
|
||||
<div
|
||||
class="c-properties__label label"
|
||||
class="c-inspect-properties__label label"
|
||||
:disabled="useGlobal"
|
||||
>
|
||||
{{ filterField.name }} =
|
||||
</div>
|
||||
<div class="c-properties__value value">
|
||||
<div class="c-inspect-properties__value value">
|
||||
<!-- EDITING -->
|
||||
<!-- String input, editing -->
|
||||
<template v-if="!filter.possibleValues && isEditing">
|
||||
|
@ -26,17 +26,17 @@
|
||||
</div>
|
||||
|
||||
<div v-if="expanded">
|
||||
<ul class="c-properties">
|
||||
<ul class="c-inspect-properties">
|
||||
<div
|
||||
v-if="!isEditing && persistedFilters.useGlobal"
|
||||
class="c-properties__label span-all"
|
||||
class="c-inspect-properties__label span-all"
|
||||
>
|
||||
Uses global filter
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="isEditing"
|
||||
class="c-properties__label span-all"
|
||||
class="c-inspect-properties__label span-all"
|
||||
>
|
||||
<toggle-switch
|
||||
:id="keyString"
|
||||
|
@ -23,7 +23,7 @@
|
||||
</div>
|
||||
<ul
|
||||
v-if="expanded"
|
||||
class="c-properties"
|
||||
class="c-inspect-properties"
|
||||
>
|
||||
<filter-field
|
||||
v-for="metadatum in globalMetadata"
|
||||
|
@ -7,7 +7,7 @@
|
||||
}
|
||||
.c-filter-tree {
|
||||
// Filters UI uses a tree-based structure
|
||||
.c-properties {
|
||||
.c-inspect-properties {
|
||||
// Add extra margin to account for filter-indicator
|
||||
margin-left: 38px;
|
||||
}
|
||||
|
@ -313,8 +313,4 @@
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.c-object-view {
|
||||
display: contents;
|
||||
}
|
||||
}
|
||||
|
@ -232,18 +232,16 @@ export default {
|
||||
this.newFrameLocation = [containerIndex, insertFrameIndex];
|
||||
},
|
||||
addFrame(domainObject) {
|
||||
if (this.newFrameLocation.length) {
|
||||
let containerIndex = this.newFrameLocation[0],
|
||||
frameIndex = this.newFrameLocation[1],
|
||||
frame = new Frame(domainObject.identifier),
|
||||
container = this.containers[containerIndex];
|
||||
let containerIndex = this.newFrameLocation.length ? this.newFrameLocation[0] : 0;
|
||||
let container = this.containers[containerIndex];
|
||||
let frameIndex = this.newFrameLocation.length ? this.newFrameLocation[1] : container.frames.length;
|
||||
let frame = new Frame(domainObject.identifier);
|
||||
|
||||
container.frames.splice(frameIndex + 1, 0, frame);
|
||||
sizeItems(container.frames, frame);
|
||||
container.frames.splice(frameIndex + 1, 0, frame);
|
||||
sizeItems(container.frames, frame);
|
||||
|
||||
this.newFrameLocation = [];
|
||||
this.persist(containerIndex);
|
||||
}
|
||||
this.newFrameLocation = [];
|
||||
this.persist(containerIndex);
|
||||
},
|
||||
deleteFrame(frameId) {
|
||||
let container = this.containers
|
||||
|
@ -26,6 +26,7 @@ export default function ImageryViewProvider(openmct) {
|
||||
return {
|
||||
show: function (element) {
|
||||
component = new Vue({
|
||||
el: element,
|
||||
components: {
|
||||
ImageryViewLayout
|
||||
},
|
||||
@ -33,7 +34,6 @@ export default function ImageryViewProvider(openmct) {
|
||||
openmct,
|
||||
domainObject
|
||||
},
|
||||
el: element,
|
||||
template: '<imagery-view-layout ref="ImageryLayout"></imagery-view-layout>'
|
||||
});
|
||||
},
|
||||
|
@ -1,90 +1,68 @@
|
||||
<template>
|
||||
<multipane class="c-imagery-layout"
|
||||
type="vertical"
|
||||
>
|
||||
<pane :style="{'min-height': `300px`}">
|
||||
<div class="main-image-wrapper c-imagery has-local-controls">
|
||||
<div class="h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover l-flex-row c-imagery__lc">
|
||||
<span class="holder flex-elem grows c-imagery__lc__sliders">
|
||||
<input v-model="filters.brightness"
|
||||
class="icon-brightness"
|
||||
type="range"
|
||||
min="0"
|
||||
max="500"
|
||||
>
|
||||
<input v-model="filters.contrast"
|
||||
class="icon-contrast"
|
||||
type="range"
|
||||
min="0"
|
||||
max="500"
|
||||
>
|
||||
</span>
|
||||
<span class="holder flex-elem t-reset-btn-holder c-imagery__lc__reset-btn">
|
||||
<a class="s-icon-button icon-reset t-btn-reset"
|
||||
@click="filters={brightness: 100, contrast: 100}"
|
||||
></a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="main-image s-image-main"
|
||||
:class="{'paused unnsynced': paused(),'stale':false }"
|
||||
:style="{'background-image': `url(${getImageUrl()})`,
|
||||
'filter': `brightness(${filters.brightness}%) contrast(${filters.contrast}%)`}"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="l-image-controller flex-elem l-flex-row">
|
||||
<div class="l-datetime-w flex-elem grows">
|
||||
<a class="c-button show-thumbs sm hidden icon-thumbs-strip"></a>
|
||||
<span class="l-time">{{ getTime() }}</span>
|
||||
</div>
|
||||
<div class="h-local-controls flex-elem">
|
||||
<a class="c-button icon-pause pause-play"
|
||||
:class="{'is-paused': paused()}"
|
||||
@click="paused(!paused())"
|
||||
></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</pane>
|
||||
|
||||
<pane class="c-inspector__elements"
|
||||
handle="before"
|
||||
:style="{'min-height': `100px`}"
|
||||
>
|
||||
<div class="c-elements-pool">
|
||||
<div ref="thumbsWrapper"
|
||||
class="thumbs-layout"
|
||||
@scroll="handleScroll"
|
||||
>
|
||||
<div v-for="(imageData, index) in imageHistory"
|
||||
:key="index"
|
||||
class="l-image-thumb-item"
|
||||
:class="{selected: imageData.selected}"
|
||||
@click="setSelectedImage(imageData)"
|
||||
<div class="c-imagery">
|
||||
<div class="c-imagery__main-image-wrapper has-local-controls">
|
||||
<div class="h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover l-flex-row c-imagery__lc">
|
||||
<span class="holder flex-elem grows c-imagery__lc__sliders">
|
||||
<input v-model="filters.brightness"
|
||||
class="icon-brightness"
|
||||
type="range"
|
||||
min="0"
|
||||
max="500"
|
||||
>
|
||||
<img class="l-thumb"
|
||||
:src="getImageUrl(imageData)"
|
||||
>
|
||||
<div class="l-time">{{ getTime(imageData) }}</div>
|
||||
</div>
|
||||
<input v-model="filters.contrast"
|
||||
class="icon-contrast"
|
||||
type="range"
|
||||
min="0"
|
||||
max="500"
|
||||
>
|
||||
</span>
|
||||
<span class="holder flex-elem t-reset-btn-holder c-imagery__lc__reset-btn">
|
||||
<a class="s-icon-button icon-reset t-btn-reset"
|
||||
@click="filters={brightness: 100, contrast: 100}"
|
||||
></a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="main-image s-image-main c-imagery__main-image"
|
||||
:class="{'paused unnsynced': paused(),'stale':false }"
|
||||
:style="{'background-image': `url(${getImageUrl()})`,
|
||||
'filter': `brightness(${filters.brightness}%) contrast(${filters.contrast}%)`}"
|
||||
>
|
||||
</div>
|
||||
<div class="c-imagery__control-bar">
|
||||
<div class="c-imagery__timestamp">{{ getTime() }}</div>
|
||||
<div class="h-local-controls flex-elem">
|
||||
<a class="c-button icon-pause pause-play"
|
||||
:class="{'is-paused': paused()}"
|
||||
@click="paused(!paused())"
|
||||
></a>
|
||||
</div>
|
||||
</div>
|
||||
</pane>
|
||||
</multipane>
|
||||
</div>
|
||||
<div ref="thumbsWrapper"
|
||||
class="c-imagery__thumbs-wrapper"
|
||||
:class="{'is-paused': paused()}"
|
||||
@scroll="handleScroll"
|
||||
>
|
||||
<div v-for="(imageData, index) in imageHistory"
|
||||
:key="index"
|
||||
class="c-imagery__thumb c-thumb"
|
||||
:class="{selected: imageData.selected}"
|
||||
@click="setSelectedImage(imageData)"
|
||||
>
|
||||
<img class="c-thumb__image"
|
||||
:src="getImageUrl(imageData)"
|
||||
>
|
||||
<div class="c-thumb__timestamp">{{ getTime(imageData) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import multipane from '@/ui/layout/multipane.vue';
|
||||
import pane from '@/ui/layout/pane.vue';
|
||||
import _ from 'lodash';
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject'],
|
||||
components: {
|
||||
multipane,
|
||||
pane
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
autoScroll: true,
|
||||
@ -109,7 +87,7 @@ export default {
|
||||
this.subscribe(this.domainObject);
|
||||
},
|
||||
updated() {
|
||||
this.scrollToBottom();
|
||||
this.scrollToRight();
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.stopListening();
|
||||
@ -152,6 +130,10 @@ export default {
|
||||
if (arguments.length > 0 && state !== this.isPaused) {
|
||||
this.unselectAllImages();
|
||||
this.isPaused = state;
|
||||
if (state === true) {
|
||||
// If we are pausing, select the latest image in imageHistory
|
||||
this.setSelectedImage(this.imageHistory[this.imageHistory.length - 1]);
|
||||
}
|
||||
|
||||
if (this.nextDatum) {
|
||||
this.updateValues(this.nextDatum);
|
||||
@ -180,24 +162,31 @@ export default {
|
||||
this.updateValues(values[values.length - 1]);
|
||||
});
|
||||
},
|
||||
scrollToBottom() {
|
||||
scrollToRight() {
|
||||
if (this.isPaused || !this.$refs.thumbsWrapper || !this.autoScroll) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scrollHeight = this.$refs.thumbsWrapper.scrollHeight || 0;
|
||||
if (!scrollHeight) {
|
||||
const scrollWidth = this.$refs.thumbsWrapper.scrollWidth || 0;
|
||||
if (!scrollWidth) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => this.$refs.thumbsWrapper.scrollTop = scrollHeight, 0);
|
||||
setTimeout(() => this.$refs.thumbsWrapper.scrollLeft = scrollWidth, 0);
|
||||
},
|
||||
setSelectedImage(image) {
|
||||
this.imageUrl = this.getImageUrl(image);
|
||||
this.time = this.getTime(image);
|
||||
this.paused(true);
|
||||
this.unselectAllImages();
|
||||
image.selected = true;
|
||||
// If we are paused and the current image IS selected, unpause
|
||||
// Otherwise, set current image and pause
|
||||
if (this.isPaused && image.selected) {
|
||||
this.paused(false);
|
||||
this.unselectAllImages();
|
||||
} else {
|
||||
this.imageUrl = this.getImageUrl(image);
|
||||
this.time = this.getTime(image);
|
||||
this.paused(true);
|
||||
this.unselectAllImages();
|
||||
image.selected = true;
|
||||
}
|
||||
},
|
||||
stopListening() {
|
||||
if (this.unsubscribe) {
|
||||
|
@ -1,16 +1,20 @@
|
||||
.c-imagery-layout {
|
||||
.c-imagery {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
|
||||
.main-image-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding-bottom: 5px;
|
||||
> * + * {
|
||||
margin-top: $interiorMargin;
|
||||
}
|
||||
|
||||
.main-image {
|
||||
&__main-image-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
&__main-image {
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
@ -21,12 +25,137 @@
|
||||
}
|
||||
}
|
||||
|
||||
.l-image-controller {
|
||||
&__control-bar {
|
||||
padding: 5px 0 0 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.thumbs-layout {
|
||||
margin-top: 5px;
|
||||
overflow: auto;
|
||||
&__timestamp {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
&__thumbs-wrapper {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 135px;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
|
||||
&.is-paused {
|
||||
background: rgba($colorPausedBg, 0.4);
|
||||
}
|
||||
|
||||
.c-thumb:last-child {
|
||||
// Hilite the lastest thumb
|
||||
background: $colorBodyFg;
|
||||
color: $colorBodyBg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*************************************** THUMBS */
|
||||
.c-thumb {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 4px;
|
||||
width: $imageThumbsD;
|
||||
|
||||
&:hover {
|
||||
background: $colorThumbHoverBg;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: $colorPausedBg !important;
|
||||
color: $colorPausedFg !important;
|
||||
}
|
||||
|
||||
&__image {
|
||||
background-color: rgba($colorBodyFg, 0.2);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__timestamp {
|
||||
flex: 0 0 auto;
|
||||
padding: 2px 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.l-layout,
|
||||
.c-fl {
|
||||
.c-imagery__thumbs-wrapper {
|
||||
// When Imagery is in a layout, hide the thumbs area
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.s-image-main {
|
||||
background-color: $colorPlotBg;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
/*************************************** IMAGERY LOCAL CONTROLS*/
|
||||
.c-imagery {
|
||||
.h-local-controls--overlay-content {
|
||||
position: absolute;
|
||||
right: $interiorMargin; top: $interiorMargin;
|
||||
z-index: 2;
|
||||
background: $colorLocalControlOvrBg;
|
||||
border-radius: $basicCr;
|
||||
max-width: 200px;
|
||||
min-width: 100px;
|
||||
width: 35%;
|
||||
align-items: center;
|
||||
padding: $interiorMargin $interiorMarginLg;
|
||||
|
||||
input[type="range"] {
|
||||
display: block;
|
||||
width: 100%;
|
||||
&:not(:first-child) {
|
||||
margin-top: $interiorMarginLg;
|
||||
}
|
||||
|
||||
&:before {
|
||||
margin-right: $interiorMarginSm;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__lc {
|
||||
&__reset-btn {
|
||||
$bc: $scrollbarTrackColorBg;
|
||||
&:before,
|
||||
&:after {
|
||||
border-right: 1px solid $bc;
|
||||
content:'';
|
||||
display: block;
|
||||
width: 5px;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
&:before {
|
||||
border-top: 1px solid $bc;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
&:after {
|
||||
border-bottom: 1px solid $bc;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*************************************** BUTTONS */
|
||||
.c-button.pause-play {
|
||||
// Pause icon set by default in markup
|
||||
&.is-paused {
|
||||
background: $colorPausedBg !important;
|
||||
color: $colorPausedFg;
|
||||
|
||||
&:before {
|
||||
content: $glyph-icon-play;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
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>
|
251
src/plugins/notebook/components/notebook-embed.vue
Normal file
251
src/plugins/notebook/components/notebook-embed.vue
Normal file
@ -0,0 +1,251 @@
|
||||
<template>
|
||||
<div class="c-snapshot c-ne__embed">
|
||||
<div v-if="embed.snapshot"
|
||||
class="c-ne__embed__snap-thumb"
|
||||
@click="openSnapshot()"
|
||||
>
|
||||
<img :src="embed.snapshot.src">
|
||||
</div>
|
||||
<div class="c-ne__embed__info">
|
||||
<div class="c-ne__embed__name">
|
||||
<a class="c-ne__embed__link"
|
||||
:class="embed.cssClass"
|
||||
@click="changeLocation"
|
||||
>{{ embed.name }}</a>
|
||||
<PopupMenu :popup-menu-items="popupMenuItems" />
|
||||
</div>
|
||||
<div v-if="embed.snapshot"
|
||||
class="c-ne__embed__time"
|
||||
>
|
||||
{{ formatTime(embed.createdOn, 'YYYY-MM-DD HH:mm:ss') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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 Vue from 'vue';
|
||||
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
components: {
|
||||
PopupMenu
|
||||
},
|
||||
props: {
|
||||
embed: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
removeActionString: {
|
||||
type: String,
|
||||
default() {
|
||||
return 'Remove This Embed';
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
popupMenuItems: []
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
},
|
||||
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;
|
||||
|
||||
let save = false;
|
||||
let painterroInstance = {};
|
||||
const annotateVue = new Vue({
|
||||
template: '<div id="snap-annotation"></div>'
|
||||
});
|
||||
|
||||
let annotateOverlay = self.openmct.overlays.overlay({
|
||||
element: annotateVue.$mount().$el,
|
||||
size: 'large',
|
||||
dismissable: false,
|
||||
buttons: [
|
||||
{
|
||||
label: 'Cancel',
|
||||
callback: function () {
|
||||
save = false;
|
||||
painterroInstance.save();
|
||||
annotateOverlay.dismiss();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Save',
|
||||
callback: function () {
|
||||
|
||||
save = true;
|
||||
painterroInstance.save();
|
||||
annotateOverlay.dismiss();
|
||||
}
|
||||
}
|
||||
],
|
||||
onDestroy: function () {
|
||||
annotateVue.$destroy(true);
|
||||
}
|
||||
});
|
||||
|
||||
painterroInstance = Painterro({
|
||||
id: 'snap-annotation',
|
||||
activeColor: '#ff0000',
|
||||
activeColorAlpha: 1.0,
|
||||
activeFillColor: '#fff',
|
||||
activeFillColorAlpha: 0.0,
|
||||
backgroundFillColor: '#000',
|
||||
backgroundFillColorAlpha: 0.0,
|
||||
defaultFontSize: 16,
|
||||
defaultLineWidth: 2,
|
||||
defaultTool: 'ellipse',
|
||||
hiddenTools: ['save', 'open', 'close', 'eraser', 'pixelize', 'rotate', 'settings', 'resize'],
|
||||
translation: {
|
||||
name: 'en',
|
||||
strings: {
|
||||
lineColor: 'Line',
|
||||
fillColor: 'Fill',
|
||||
lineWidth: 'Size',
|
||||
textColor: 'Color',
|
||||
fontSize: 'Size',
|
||||
fontStyle: 'Style'
|
||||
}
|
||||
},
|
||||
saveHandler: function (image, done) {
|
||||
if (save) {
|
||||
const url = image.asBlob();
|
||||
const reader = new window.FileReader();
|
||||
reader.readAsDataURL(url);
|
||||
reader.onloadend = function () {
|
||||
const snapshot = reader.result;
|
||||
const snapshotObject = {
|
||||
src: snapshot,
|
||||
type: url.type,
|
||||
size: url.size,
|
||||
modified: Date.now()
|
||||
};
|
||||
|
||||
self.embed.snapshot = snapshotObject;
|
||||
self.updateEmbed(self.embed);
|
||||
};
|
||||
} else {
|
||||
console.log('You cancelled the annotation!!!');
|
||||
}
|
||||
|
||||
done(true);
|
||||
}
|
||||
}).show(this.embed.snapshot.src);
|
||||
},
|
||||
changeLocation() {
|
||||
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();
|
||||
|
||||
window.location.href = link;
|
||||
|
||||
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({
|
||||
data: () => {
|
||||
return {
|
||||
embed: self.embed
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
formatTime: self.formatTime,
|
||||
annotateSnapshot: self.annotateSnapshot
|
||||
},
|
||||
template: SnapshotTemplate
|
||||
});
|
||||
|
||||
const snapshotOverlay = this.openmct.overlays.overlay({
|
||||
element: snapshot.$mount().$el,
|
||||
onDestroy: () => { snapshot.$destroy(true) },
|
||||
size: 'large',
|
||||
dismissable: true,
|
||||
buttons: [
|
||||
{
|
||||
label: 'Done',
|
||||
emphasis: true,
|
||||
callback: () => {
|
||||
snapshotOverlay.dismiss();
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
},
|
||||
previewEmbed() {
|
||||
const self = this;
|
||||
const previewAction = new PreviewAction(self.openmct);
|
||||
previewAction.invoke(JSON.parse(self.embed.objectPath));
|
||||
},
|
||||
removeEmbed(success) {
|
||||
if (!success) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit('removeEmbed', this.embed.id);
|
||||
},
|
||||
updateEmbed(embed) {
|
||||
this.$emit('updateEmbed', embed);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
316
src/plugins/notebook/components/notebook-entry.vue
Normal file
316
src/plugins/notebook/components/notebook-entry.vue
Normal file
@ -0,0 +1,316 @@
|
||||
<template>
|
||||
<div class="c-notebook__entry c-ne has-local-controls"
|
||||
@dragover="dragover"
|
||||
@drop.capture="dropCapture"
|
||||
@drop.prevent="dropOnEntry(entry.id, $event)"
|
||||
>
|
||||
<div class="c-ne__time-and-content">
|
||||
<div class="c-ne__time">
|
||||
<span>{{ formatTime(entry.createdOn, 'YYYY-MM-DD') }}</span>
|
||||
<span>{{ formatTime(entry.createdOn, 'HH:mm:ss') }}</span>
|
||||
</div>
|
||||
<div class="c-ne__content">
|
||||
<div :id="entry.id"
|
||||
class="c-ne__text"
|
||||
:class="{'c-input-inline' : !readOnly }"
|
||||
:contenteditable="!readOnly"
|
||||
:style="!entry.text.length ? defaultEntryStyle : ''"
|
||||
@blur="textBlur($event, entry.id)"
|
||||
@focus="textFocus($event, entry.id)"
|
||||
>{{ entry.text.length ? entry.text : defaultText }}</div>
|
||||
<div class="c-snapshots c-ne__embeds">
|
||||
<NotebookEmbed v-for="embed in entry.embeds"
|
||||
:key="embed.id"
|
||||
:embed="embed"
|
||||
:entry="entry"
|
||||
@removeEmbed="removeEmbed"
|
||||
@updateEmbed="updateEmbed"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!readOnly"
|
||||
class="c-ne__local-controls--hidden"
|
||||
>
|
||||
<button class="c-icon-button c-icon-button--major icon-trash"
|
||||
title="Delete this entry"
|
||||
@click="deleteEntry"
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="readOnly"
|
||||
class="c-ne__section-and-page"
|
||||
>
|
||||
<a class="c-click-link"
|
||||
@click="navigateToSection()"
|
||||
>
|
||||
{{ result.section.name }}
|
||||
</a>
|
||||
<span class="icon-arrow-right"></span>
|
||||
<a class="c-click-link"
|
||||
@click="navigateToPage()"
|
||||
>
|
||||
{{ result.page.name }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import NotebookEmbed from './notebook-embed.vue';
|
||||
import { createNewEmbed, getEntryPosById, getNotebookEntries } from '../utils/notebook-entries';
|
||||
import Moment from 'moment';
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'snapshotContainer'],
|
||||
components: {
|
||||
NotebookEmbed
|
||||
},
|
||||
props: {
|
||||
domainObject: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
entry: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
result: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
selectedPage: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
selectedSection: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
readOnly: {
|
||||
type: Boolean,
|
||||
default() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentEntryValue: '',
|
||||
defaultEntryStyle: {
|
||||
fontStyle: 'italic',
|
||||
color: '#6e6e6e'
|
||||
},
|
||||
defaultText: 'add description'
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
entry() {
|
||||
},
|
||||
readOnly(readOnly) {
|
||||
},
|
||||
selectedSection(selectedSection) {
|
||||
},
|
||||
selectedPage(selectedSection) {
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.updateEntries = this.updateEntries.bind(this);
|
||||
},
|
||||
beforeDestory() {
|
||||
},
|
||||
methods: {
|
||||
deleteEntry() {
|
||||
const self = this;
|
||||
if (!self.domainObject || !self.selectedSection || !self.selectedPage || !self.entry.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entryPosById = this.entryPosById(this.entry.id);
|
||||
if (entryPosById === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dialog = this.openmct.overlays.dialog({
|
||||
iconClass: 'alert',
|
||||
message: 'This action will permanently delete this entry. Do you wish to continue?',
|
||||
buttons: [
|
||||
{
|
||||
label: "Ok",
|
||||
emphasis: true,
|
||||
callback: () => {
|
||||
const entries = getNotebookEntries(self.domainObject, self.selectedSection, self.selectedPage);
|
||||
entries.splice(entryPosById, 1);
|
||||
this.updateEntries(entries);
|
||||
dialog.dismiss();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Cancel",
|
||||
callback: () => {
|
||||
dialog.dismiss();
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
},
|
||||
dragover() {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = "copy";
|
||||
},
|
||||
dropCapture(event) {
|
||||
const isEditing = this.openmct.editor.isEditing();
|
||||
if (isEditing) {
|
||||
this.openmct.editor.cancel();
|
||||
}
|
||||
},
|
||||
dropOnEntry(entryId, $event) {
|
||||
event.stopImmediatePropagation();
|
||||
|
||||
if (!this.domainObject || !this.selectedSection || !this.selectedPage) {
|
||||
return;
|
||||
}
|
||||
|
||||
const snapshotId = $event.dataTransfer.getData('snapshot/id');
|
||||
if (snapshotId.length) {
|
||||
this.moveSnapshot(snapshotId);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const data = $event.dataTransfer.getData('openmct/domain-object-path');
|
||||
const objectPath = JSON.parse(data);
|
||||
const entryPos = this.entryPosById(entryId);
|
||||
const bounds = this.openmct.time.bounds();
|
||||
const snapshotMeta = {
|
||||
bounds,
|
||||
link: null,
|
||||
objectPath,
|
||||
openmct: this.openmct
|
||||
}
|
||||
const newEmbed = createNewEmbed(snapshotMeta);
|
||||
const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
|
||||
const currentEntryEmbeds = entries[entryPos].embeds;
|
||||
currentEntryEmbeds.push(newEmbed);
|
||||
this.updateEntries(entries);
|
||||
},
|
||||
entryPosById(entryId) {
|
||||
return getEntryPosById(entryId, this.domainObject, this.selectedSection, this.selectedPage);
|
||||
},
|
||||
findPositionInArray(array, id) {
|
||||
let position = -1;
|
||||
array.some((item, index) => {
|
||||
const found = item.id === id;
|
||||
if (found) {
|
||||
position = index;
|
||||
}
|
||||
|
||||
return found;
|
||||
});
|
||||
|
||||
return position;
|
||||
},
|
||||
formatTime(unixTime, timeFormat) {
|
||||
return Moment.utc(unixTime).format(timeFormat);
|
||||
},
|
||||
moveSnapshot(snapshotId) {
|
||||
const snapshot = this.snapshotContainer.getSnapshot(snapshotId);
|
||||
this.entry.embeds.push(snapshot);
|
||||
this.updateEntry(this.entry);
|
||||
this.snapshotContainer.removeSnapshot(snapshotId);
|
||||
},
|
||||
navigateToPage() {
|
||||
this.$emit('changeSectionPage', {
|
||||
sectionId: this.result.section.id,
|
||||
pageId: this.result.page.id
|
||||
});
|
||||
},
|
||||
navigateToSection() {
|
||||
this.$emit('changeSectionPage', {
|
||||
sectionId: this.result.section.id,
|
||||
pageId: null
|
||||
});
|
||||
},
|
||||
removeEmbed(id) {
|
||||
const embedPosition = this.findPositionInArray(this.entry.embeds, id);
|
||||
this.entry.embeds.splice(embedPosition, 1);
|
||||
this.updateEntry(this.entry);
|
||||
},
|
||||
selectTextInsideElement(element) {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(element);
|
||||
var selection = window.getSelection();
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
},
|
||||
textBlur($event, entryId) {
|
||||
if (!this.domainObject || !this.selectedSection || !this.selectedPage) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = $event.target;
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entryPos = this.entryPosById(entryId);
|
||||
const value = target.textContent.trim();
|
||||
if (this.currentEntryValue !== value) {
|
||||
const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
|
||||
entries[entryPos].text = value;
|
||||
|
||||
this.updateEntries(entries);
|
||||
}
|
||||
},
|
||||
textFocus($event) {
|
||||
if (this.readOnly || !this.domainObject || !this.selectedSection || !this.selectedPage) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = $event.target
|
||||
this.currentEntryValue = target ? target.innerText : '';
|
||||
|
||||
if (!this.entry.text.length) {
|
||||
this.selectTextInsideElement(target);
|
||||
}
|
||||
},
|
||||
updateEmbed(newEmbed) {
|
||||
let embed = this.entry.embeds.find(e => e.id === newEmbed.id);
|
||||
|
||||
if (!embed) {
|
||||
return;
|
||||
}
|
||||
|
||||
embed = newEmbed;
|
||||
this.updateEntry(this.entry);
|
||||
},
|
||||
updateEntry(newEntry) {
|
||||
if (!this.domainObject || !this.selectedSection || !this.selectedPage) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
|
||||
entries.forEach(entry => {
|
||||
if (entry.id === newEntry.id) {
|
||||
entry = newEntry;
|
||||
}
|
||||
});
|
||||
|
||||
this.updateEntries(entries);
|
||||
},
|
||||
updateEntries(entries) {
|
||||
this.$emit('updateEntries', entries);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
136
src/plugins/notebook/components/notebook-menu-switcher.vue
Normal file
136
src/plugins/notebook/components/notebook-menu-switcher.vue
Normal file
@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left">
|
||||
<button
|
||||
class="c-button--menu icon-notebook"
|
||||
title="Take a Notebook Snapshot"
|
||||
@click="setNotebookTypes"
|
||||
@click.stop="toggleMenu"
|
||||
>
|
||||
<span class="c-button__label"></span>
|
||||
</button>
|
||||
<div
|
||||
v-show="showMenu"
|
||||
class="c-menu"
|
||||
>
|
||||
<ul>
|
||||
<li
|
||||
v-for="(type, index) in notebookTypes"
|
||||
:key="index"
|
||||
:class="type.cssClass"
|
||||
:title="type.name"
|
||||
@click="snapshot(type)"
|
||||
>
|
||||
{{ type.name }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Snapshot from '../snapshot';
|
||||
import { clearDefaultNotebook, getDefaultNotebook } from '../utils/notebook-storage';
|
||||
import { NOTEBOOK_DEFAULT, NOTEBOOK_SNAPSHOT } from '../notebook-constants';
|
||||
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
props: {
|
||||
domainObject: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
ignoreLink: {
|
||||
type: Boolean,
|
||||
default() {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
objectPath: {
|
||||
type: Array,
|
||||
default() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
notebookSnapshot: null,
|
||||
notebookTypes: [],
|
||||
showMenu: false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.notebookSnapshot = new Snapshot(this.openmct);
|
||||
|
||||
document.addEventListener('click', this.hideMenu);
|
||||
},
|
||||
destroyed() {
|
||||
document.removeEventListener('click', this.hideMenu);
|
||||
},
|
||||
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);
|
||||
|
||||
if (!domainObject.location) {
|
||||
clearDefaultNotebook();
|
||||
} else {
|
||||
defaultPath = `${domainObject.name} - ${defaultNotebook.section.name} - ${defaultNotebook.page.name}`;
|
||||
}
|
||||
}
|
||||
|
||||
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',
|
||||
type: NOTEBOOK_SNAPSHOT
|
||||
});
|
||||
|
||||
this.notebookTypes = notebookTypes;
|
||||
},
|
||||
toggleMenu() {
|
||||
this.showMenu = !this.showMenu;
|
||||
},
|
||||
hideMenu() {
|
||||
this.showMenu = false;
|
||||
},
|
||||
snapshot(notebook) {
|
||||
this.hideMenu();
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
129
src/plugins/notebook/components/notebook-snapshot-container.vue
Normal file
129
src/plugins/notebook/components/notebook-snapshot-container.vue
Normal file
@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<div class="c-snapshots-h">
|
||||
<div class="l-browse-bar">
|
||||
<div class="l-browse-bar__start">
|
||||
<div class="l-browse-bar__object-name--w icon-notebook">
|
||||
<div class="l-browse-bar__object-name">
|
||||
Notebook Snapshots
|
||||
<span v-if="snapshots.length"
|
||||
class="l-browse-bar__object-details"
|
||||
> {{ snapshots.length }} of {{ getNotebookSnapshotMaxCount() }}
|
||||
</span>
|
||||
</div>
|
||||
<PopupMenu v-if="snapshots.length > 0"
|
||||
:popup-menu-items="popupMenuItems"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="l-browse-bar__end">
|
||||
<button class="c-click-icon c-click-icon--major icon-x"
|
||||
@click="close"
|
||||
></button>
|
||||
</div>
|
||||
</div><!-- closes l-browse-bar -->
|
||||
<div class="c-snapshots">
|
||||
<span v-for="snapshot in snapshots"
|
||||
:key="snapshot.id"
|
||||
draggable="true"
|
||||
@dragstart="startEmbedDrag(snapshot, $event)"
|
||||
>
|
||||
<NotebookEmbed ref="notebookEmbed"
|
||||
:key="snapshot.id"
|
||||
:embed="snapshot"
|
||||
:remove-action-string="'Delete Snapshot'"
|
||||
@updateEmbed="updateSnapshot"
|
||||
@removeEmbed="removeSnapshot"
|
||||
/>
|
||||
</span>
|
||||
<div v-if="!snapshots.length > 0"
|
||||
class="hint"
|
||||
>
|
||||
There are no Notebook Snapshots currently.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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';
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'snapshotContainer'],
|
||||
components: {
|
||||
NotebookEmbed,
|
||||
PopupMenu
|
||||
},
|
||||
props: {
|
||||
toggleSnapshot: {
|
||||
type: Function,
|
||||
default() {
|
||||
return () => {};
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
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;
|
||||
},
|
||||
getRemoveDialog() {
|
||||
const options = {
|
||||
name: this.removeActionString,
|
||||
callback: this.removeAllSnapshots.bind(this)
|
||||
}
|
||||
const removeDialog = new RemoveDialog(this.openmct, options);
|
||||
removeDialog.show();
|
||||
},
|
||||
removeAllSnapshots(success) {
|
||||
if (!success) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.snapshotContainer.removeAllSnapshots();
|
||||
},
|
||||
removeSnapshot(id) {
|
||||
this.snapshotContainer.removeSnapshot(id);
|
||||
},
|
||||
snapshotsUpdated() {
|
||||
this.snapshots = this.snapshotContainer.getSnapshots();
|
||||
},
|
||||
startEmbedDrag(snapshot, event) {
|
||||
event.dataTransfer.setData('text/plain', snapshot.id);
|
||||
event.dataTransfer.setData('snapshot/id', snapshot.id);
|
||||
},
|
||||
updateSnapshot(snapshot) {
|
||||
this.snapshotContainer.updateSnapshot(snapshot);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<div class="c-indicator c-indicator--clickable icon-notebook"
|
||||
:class="[
|
||||
{ 's-status-off': snapshotCount === 0 },
|
||||
{ 's-status-on': snapshotCount > 0 },
|
||||
{ 's-status-caution': snapshotCount === snapshotMaxCount },
|
||||
{ 'has-new-snapshot': flashIndicator }
|
||||
]"
|
||||
>
|
||||
<span class="label c-indicator__label">
|
||||
{{ indicatorTitle }}
|
||||
<button @click="toggleSnapshot">
|
||||
{{ expanded ? 'Hide' : 'Show' }}
|
||||
</button>
|
||||
</span>
|
||||
<span class="c-indicator__count">{{ snapshotCount }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SnapshotContainerComponent from './notebook-snapshot-container.vue';
|
||||
import { EVENT_SNAPSHOTS_UPDATED } from '../notebook-constants';
|
||||
import { NOTEBOOK_SNAPSHOT_MAX_COUNT } from '../snapshot-container';
|
||||
import Vue from 'vue';
|
||||
|
||||
export default {
|
||||
inject: ['openmct','snapshotContainer'],
|
||||
data() {
|
||||
return {
|
||||
expanded: false,
|
||||
indicatorTitle: '',
|
||||
snapshotCount: 0,
|
||||
snapshotMaxCount: NOTEBOOK_SNAPSHOT_MAX_COUNT,
|
||||
flashIndicator: false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.snapshotContainer.on(EVENT_SNAPSHOTS_UPDATED, this.snapshotsUpdated);
|
||||
this.updateSnapshotIndicatorTitle();
|
||||
},
|
||||
methods: {
|
||||
notifyNewSnapshot() {
|
||||
this.flashIndicator = true;
|
||||
setTimeout(this.removeNotify, 15000);
|
||||
},
|
||||
removeNotify() {
|
||||
this.flashIndicator = false;
|
||||
},
|
||||
snapshotsUpdated() {
|
||||
if (this.snapshotContainer.getSnapshots().length > this.snapshotCount) {
|
||||
this.notifyNewSnapshot();
|
||||
}
|
||||
this.updateSnapshotIndicatorTitle();
|
||||
},
|
||||
toggleSnapshot() {
|
||||
this.expanded = !this.expanded;
|
||||
|
||||
const drawerElement = document.querySelector('.l-shell__drawer');
|
||||
drawerElement.classList.toggle('is-expanded');
|
||||
|
||||
this.updateSnapshotContainer();
|
||||
},
|
||||
updateSnapshotContainer() {
|
||||
const { openmct, snapshotContainer } = this;
|
||||
const toggleSnapshot = this.toggleSnapshot.bind(this);
|
||||
const drawerElement = document.querySelector('.l-shell__drawer');
|
||||
drawerElement.innerHTML = '<div></div>';
|
||||
const divElement = document.querySelector('.l-shell__drawer div');
|
||||
|
||||
this.component = new Vue({
|
||||
provide: {
|
||||
openmct,
|
||||
snapshotContainer
|
||||
},
|
||||
el: divElement,
|
||||
components: {
|
||||
SnapshotContainerComponent
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
toggleSnapshot
|
||||
};
|
||||
},
|
||||
template: '<SnapshotContainerComponent :toggleSnapshot="toggleSnapshot"></SnapshotContainerComponent>'
|
||||
}).$mount();
|
||||
},
|
||||
updateSnapshotIndicatorTitle() {
|
||||
const snapshotCount = this.snapshotContainer.getSnapshots().length;
|
||||
this.snapshotCount = snapshotCount;
|
||||
const snapshotTitleSuffix = snapshotCount === 1
|
||||
? 'Snapshot'
|
||||
: 'Snapshots';
|
||||
this.indicatorTitle = `${snapshotCount} ${snapshotTitleSuffix}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user