Compare commits
	
		
			399 Commits
		
	
	
		
			hide-show-
			...
			vista/hide
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | a237ab5ac5 | ||
|   | 7dd81beb03 | ||
|   | 1842d3923c | ||
|   | 26838635b6 | ||
|   | 11f2c35bb2 | ||
|   | 766f48c1ba | ||
|   | da7b93f9b3 | ||
|   | 99c095a69f | ||
|   | f885e83505 | ||
|   | 928bc4c68a | ||
|   | d5539c7ae4 | ||
|   | c86a104fb6 | ||
|   | 46fedc1a30 | ||
|   | 3b6ef9b44b | ||
|   | 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 | ||
|   | 7d2256d70f | ||
|   | 5814d2a35e | ||
|   | 6ab84c0bc3 | ||
|   | 67f493f012 | ||
|   | 0686e6d38f | ||
|   | 7fc825949c | ||
|   | 2a9ccdcffd | ||
|   | 6db78af69f | ||
|   | 038489256c | ||
|   | 22161fce7f | ||
|   | 386fc75047 | ||
|   | fa6dd84945 | ||
|   | 682601477c | ||
|   | 0a9d23d86f | ||
|   | 379e37c881 | ||
|   | 37ef269dce | ||
|   | 38deef6e72 | ||
|   | b6220288ac | ||
|   | 2e82edb306 | ||
|   | 8f0e773ac1 | ||
|   | 223a0feada | ||
|   | 0fd0da8331 | ||
|   | fa21911287 | ||
|   | 7db4ac8ff6 | ||
|   | 03829af2ad | ||
|   | 322cd94be7 | ||
|   | 5d31806fb7 | ||
|   | 384f0efcb3 | 
							
								
								
									
										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 | ||||
|                 }; | ||||
|             } | ||||
|         }); | ||||
|   | ||||
| @@ -43,7 +43,7 @@ | ||||
|             openmct.legacyRegistry.enable.bind(openmct.legacyRegistry) | ||||
|         ); | ||||
|  | ||||
|         openmct.install(openmct.plugins.Snow()); | ||||
|         openmct.install(openmct.plugins.Espresso()); | ||||
|         openmct.install(openmct.plugins.MyItems()); | ||||
|         openmct.install(openmct.plugins.LocalStorage()); | ||||
|         openmct.install(openmct.plugins.Generator()); | ||||
|   | ||||
| @@ -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 = { | ||||
|   | ||||
| @@ -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 = { | ||||
|   | ||||
| @@ -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. | ||||
|  * | ||||
| @@ -265,6 +265,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 () { | ||||
|   | ||||
| @@ -20,19 +20,17 @@ | ||||
|  * 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 {computeCondition} from "@/plugins/condition/utils/evaluator"; | ||||
| import TelemetryCriterion from "./criterion/TelemetryCriterion"; | ||||
| import { evaluateResults } from './utils/evaluator'; | ||||
| import { getLatestTimestamp } from './utils/time'; | ||||
| import AllTelemetryCriterion from "./criterion/AllTelemetryCriterion"; | ||||
|  | ||||
| /* | ||||
| * conditionConfiguration = { | ||||
| *   identifier: { | ||||
| *       key: '', | ||||
| *       namespace: '' | ||||
| *   }, | ||||
| *   trigger: 'any'/'all', | ||||
| *   id: uuid, | ||||
| *   trigger: 'any'/'all'/'not','xor', | ||||
| *   criteria: [ | ||||
| *       { | ||||
| *           telemetry: '', | ||||
| @@ -48,44 +46,67 @@ export default class ConditionClass extends EventEmitter { | ||||
|     /** | ||||
|      * Manages criteria and emits the result of - true or false - based on criteria evaluated. | ||||
|      * @constructor | ||||
|      * @param conditionConfiguration: {identifier: {domainObject.identifier},trigger: enum, criteria: Array of {id: uuid, operation: enum, input: Array, metaDataKey: string, key: {domainObject.identifier} } | ||||
|      * @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) { | ||||
|     constructor(conditionConfiguration, openmct, conditionManager) { | ||||
|         super(); | ||||
|  | ||||
|         this.openmct = openmct; | ||||
|         this.id = this.openmct.objects.makeKeyString(conditionConfiguration.identifier); | ||||
|         this.conditionManager = conditionManager; | ||||
|         this.id = conditionConfiguration.id; | ||||
|         this.criteria = []; | ||||
|         this.criteriaResults = {}; | ||||
|         this.result = undefined; | ||||
|         this.timeSystems = this.openmct.time.getAllTimeSystems(); | ||||
|         if (conditionConfiguration.configuration.criteria) { | ||||
|             this.createCriteria(conditionConfiguration.configuration.criteria); | ||||
|         } | ||||
|         this.trigger = conditionConfiguration.configuration.trigger; | ||||
|         this.result = null; | ||||
|         this.openmct.objects.get(this.id).then(obj => this.observeForChanges(obj)); | ||||
|     } | ||||
|  | ||||
|     observeForChanges(conditionDO) { | ||||
|         this.stopObservingForChanges = this.openmct.objects.observe(conditionDO, '*', this.update.bind(this)); | ||||
|     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); | ||||
|     } | ||||
|  | ||||
|     update(newDomainObject) { | ||||
|         this.updateTrigger(newDomainObject.configuration.trigger); | ||||
|         this.updateCriteria(newDomainObject.configuration.criteria); | ||||
|     isAnyOrAllTelemetry(criterion) { | ||||
|         return (criterion.telemetry && (criterion.telemetry === 'all' || criterion.telemetry === 'any')); | ||||
|     } | ||||
|  | ||||
|     isTelemetryUsed(id) { | ||||
|         return this.criteria.some(criterion => { | ||||
|             return this.isAnyOrAllTelemetry(criterion) || criterion.telemetryObjectIdAsString === id; | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     update(conditionConfiguration) { | ||||
|         this.updateTrigger(conditionConfiguration.configuration.trigger); | ||||
|         this.updateCriteria(conditionConfiguration.configuration.criteria); | ||||
|     } | ||||
|  | ||||
|     updateTrigger(trigger) { | ||||
|         if (this.trigger !== trigger) { | ||||
|             this.trigger = trigger; | ||||
|             this.handleConditionUpdated(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     generateCriterion(criterionConfiguration) { | ||||
|         return { | ||||
|             id: uuid(), | ||||
|             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 || '' | ||||
| @@ -103,14 +124,24 @@ export default class ConditionClass extends EventEmitter { | ||||
|         this.createCriteria(criterionConfigurations); | ||||
|     } | ||||
|  | ||||
|     updateTelemetry() { | ||||
|         this.criteria.forEach((criterion) => { | ||||
|             criterion.updateTelemetry(this.conditionManager.telemetryObjects); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      *  adds criterion to the condition. | ||||
|      */ | ||||
|     addCriterion(criterionConfiguration) { | ||||
|         let criterion; | ||||
|         let criterionConfigurationWithId = this.generateCriterion(criterionConfiguration || null); | ||||
|         let criterion = new TelemetryCriterion(criterionConfigurationWithId, this.openmct); | ||||
|         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)); | ||||
|         criterion.on('criterionResultUpdated', (obj) => this.handleCriterionResult(obj)); | ||||
|         if (!this.criteria) { | ||||
|             this.criteria = []; | ||||
|         } | ||||
| @@ -139,22 +170,11 @@ export default class ConditionClass extends EventEmitter { | ||||
|             const newCriterionConfiguration = this.generateCriterion(criterionConfiguration); | ||||
|             let newCriterion = new TelemetryCriterion(newCriterionConfiguration, this.openmct); | ||||
|             newCriterion.on('criterionUpdated', (obj) => this.handleCriterionUpdated(obj)); | ||||
|             newCriterion.on('criterionResultUpdated', (obj) => this.handleCriterionResult(obj)); | ||||
|  | ||||
|             let criterion = found.item; | ||||
|             criterion.unsubscribe(); | ||||
|             criterion.off('criterionUpdated', (obj) => this.handleCriterionUpdated(obj)); | ||||
|             criterion.off('criterionResultUpdated', (obj) => this.handleCriterionResult(obj)); | ||||
|             this.criteria.splice(found.index, 1, newCriterion); | ||||
|             if (this.criteriaResults[criterion.id] !== undefined) { | ||||
|                 delete this.criteriaResults[criterion.id]; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     removeCriterion(id) { | ||||
|         if (this.destroyCriterion(id)) { | ||||
|             this.handleConditionUpdated(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -162,14 +182,12 @@ export default class ConditionClass extends EventEmitter { | ||||
|         let found = this.findCriterion(id); | ||||
|         if (found) { | ||||
|             let criterion = found.item; | ||||
|             criterion.destroy(); | ||||
|             criterion.off('criterionUpdated', (result) => { | ||||
|                 this.handleCriterionUpdated(id, result); | ||||
|             criterion.off('criterionUpdated', (obj) => { | ||||
|                 this.handleCriterionUpdated(obj); | ||||
|             }); | ||||
|             criterion.destroy(); | ||||
|             this.criteria.splice(found.index, 1); | ||||
|             if (this.criteriaResults[criterion.id] !== undefined) { | ||||
|                 delete this.criteriaResults[criterion.id]; | ||||
|             } | ||||
|  | ||||
|             return true; | ||||
|         } | ||||
|         return false; | ||||
| @@ -179,36 +197,38 @@ export default class ConditionClass extends EventEmitter { | ||||
|         let found = this.findCriterion(criterion.id); | ||||
|         if (found) { | ||||
|             this.criteria[found.index] = criterion.data; | ||||
|             this.subscribe(); | ||||
|             this.emitEvent('conditionUpdated', { | ||||
|                 trigger: this.trigger, | ||||
|                 criteria: this.criteria | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     requestLADConditionResult() { | ||||
|         let latestTimestamp; | ||||
|         let criteriaResults = {}; | ||||
|         const criteriaRequests = this.criteria | ||||
|             .map(criterion => criterion.requestLAD({telemetryObjects: 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) } | ||||
|                     ) | ||||
|                 }; | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     handleCriterionResult(eventData) { | ||||
|         let id = eventData.id; | ||||
|         let result = eventData.data.result; | ||||
|         let found = this.findCriterion(id); | ||||
|         if (found) { | ||||
|             this.criteriaResults[id] = result; | ||||
|         } | ||||
|         this.handleConditionUpdated(); | ||||
|     } | ||||
|  | ||||
|     subscribe() { | ||||
|         this.criteria.forEach((criterion) => { | ||||
|             if (criterion.isValid()) { | ||||
|                 criterion.subscribe(); | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     handleConditionUpdated() { | ||||
|         // trigger an updated event so that consumers can react accordingly | ||||
|         this.evaluate(); | ||||
|         this.emitEvent('conditionResultUpdated', {result: this.result}); | ||||
|     } | ||||
|  | ||||
|     getCriteria() { | ||||
| @@ -224,21 +244,7 @@ export default class ConditionClass extends EventEmitter { | ||||
|         return success; | ||||
|     } | ||||
|  | ||||
|     evaluate() { | ||||
|         this.result = computeCondition(this.criteriaResults, this.trigger === TRIGGER.ALL); | ||||
|     } | ||||
|  | ||||
|     emitEvent(eventName, data) { | ||||
|         this.emit(eventName, { | ||||
|             id: this.id, | ||||
|             data: data | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     destroy() { | ||||
|         if (typeof this.stopObservingForChanges === 'function') { | ||||
|             this.stopObservingForChanges(); | ||||
|         } | ||||
|         this.destroyCriteria(); | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										336
									
								
								src/plugins/condition/ConditionManager.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										336
									
								
								src/plugins/condition/ConditionManager.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,336 @@ | ||||
| /***************************************************************************** | ||||
|  * 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, id) | ||||
|         ); | ||||
|         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.updateConditionTelemetry(); | ||||
|     } | ||||
|  | ||||
|     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()); | ||||
|     } | ||||
|  | ||||
|     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() | ||||
|                         ); | ||||
|                     }); | ||||
|                     const currentCondition = this.getCurrentConditionLAD(conditionResults); | ||||
|  | ||||
|                     return Object.assign( | ||||
|                         { | ||||
|                             output: currentCondition.configuration.output, | ||||
|                             id: this.conditionSetDomainObject.identifier, | ||||
|                             conditionId: currentCondition.id | ||||
|                         }, | ||||
|                         latestTimestamp | ||||
|                     ); | ||||
|                 }); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     isTelemetryUsed(id) { | ||||
|         for(const condition of this.conditionClassCollection) { | ||||
|             if (condition.isTelemetryUsed(id)) { | ||||
|                 return true; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     telemetryReceived(id, datum) { | ||||
|         if (!this.isTelemetryUsed(id)) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         const normalizedDatum = this.createNormalizedDatum(datum, id); | ||||
|         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, id) { | ||||
|         const normalizedDatum = Object.values(this.telemetryObjects[id].telemetryMetaData).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); | ||||
|     }); | ||||
|  | ||||
| }); | ||||
| @@ -28,7 +28,6 @@ describe('ConditionSetCompositionPolicy', () => { | ||||
|     let testTelemetryObject; | ||||
|     let openmct = {}; | ||||
|     let parentDomainObject; | ||||
|     let composition; | ||||
|  | ||||
|     beforeAll(function () { | ||||
|         testTelemetryObject = { | ||||
| @@ -57,7 +56,6 @@ describe('ConditionSetCompositionPolicy', () => { | ||||
|         openmct.telemetry = jasmine.createSpyObj('telemetry', ['isTelemetryObject']); | ||||
|         policy = new ConditionSetCompositionPolicy(openmct); | ||||
|         parentDomainObject = {}; | ||||
|         composition = {}; | ||||
|     }); | ||||
|  | ||||
|     it('returns true for object types that are not conditionSets', function () { | ||||
|   | ||||
							
								
								
									
										68
									
								
								src/plugins/condition/ConditionSetMetadataProvider.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								src/plugins/condition/ConditionSetMetadataProvider.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,68 @@ | ||||
| /***************************************************************************** | ||||
|  * 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([ | ||||
|                 { | ||||
|                     name: 'Output', | ||||
|                     key: 'output', | ||||
|                     format: 'enum', | ||||
|                     enumerations: enumerations, | ||||
|                     hints: { | ||||
|                         range: 1 | ||||
|                     } | ||||
|                 } | ||||
|             ]) | ||||
|         }; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										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 ? [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; | ||||
| @@ -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-conditional'; // TODO: replace with class for new icon | ||||
|         this.cssClass = 'icon-conditional'; | ||||
|     } | ||||
|  | ||||
|     canView(domainObject) { | ||||
| @@ -70,4 +73,12 @@ export default class ConditionSetViewProvider { | ||||
|             } | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     priority(domainObject) { | ||||
|         if (domainObject.type === 'conditionSet') { | ||||
|             return Number.MAX_VALUE; | ||||
|         } else { | ||||
|             return DEFAULT_VIEW_PRIORITY; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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,6 +61,9 @@ describe("The condition", function () { | ||||
|                 }] | ||||
|             } | ||||
|         }; | ||||
|         conditionManager.telemetryObjects = { | ||||
|             "test-object": testTelemetryObject | ||||
|         }; | ||||
|         openmct.objects = jasmine.createSpyObj('objects', ['get', 'makeKeyString']); | ||||
|         openmct.objects.get.and.returnValue(new Promise(function (resolve, reject) { | ||||
|             resolve(testTelemetryObject); | ||||
| @@ -63,13 +73,23 @@ describe("The condition", function () { | ||||
|         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 = { | ||||
|             id: '123-456', | ||||
|             configuration: { | ||||
|                 name: 'mock condition', | ||||
|                 output: 'mock output', | ||||
|                 trigger: TRIGGER.ANY, | ||||
|                 criteria: [ | ||||
|                     { | ||||
|                         id: '1234-5678-9999-0000', | ||||
|                         operation: 'equalTo', | ||||
|                         input: false, | ||||
|                         input: ['0'], | ||||
|                         metadata: 'value', | ||||
|                         telemetry: testTelemetryObject.identifier | ||||
|                     } | ||||
| @@ -79,14 +99,12 @@ describe("The condition", function () { | ||||
|  | ||||
|         conditionObj = new Condition( | ||||
|             testConditionDefinition, | ||||
|             openmct | ||||
|             openmct, | ||||
|             conditionManager | ||||
|         ); | ||||
|  | ||||
|         conditionObj.on('conditionUpdated', mockListener); | ||||
|  | ||||
|     }); | ||||
|  | ||||
|     it("generates criteria with an id", function () { | ||||
|     it("generates criteria with the correct properties", function () { | ||||
|         const testCriterion = testConditionDefinition.configuration.criteria[0]; | ||||
|         let criterion = conditionObj.generateCriterion(testCriterion); | ||||
|         expect(criterion.id).toBeDefined(); | ||||
|   | ||||
| @@ -0,0 +1,128 @@ | ||||
| /***************************************************************************** | ||||
|  * 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) { | ||||
|         super(); | ||||
|         this.openmct = openmct; | ||||
|         this.callback = callback; | ||||
|         if (styleConfiguration) { | ||||
|             this.initialize(styleConfiguration); | ||||
|             if (styleConfiguration.conditionSetIdentifier) { | ||||
|                 this.subscribeToConditionSet(); | ||||
|             } else { | ||||
|                 this.applyStaticStyle(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     initialize(styleConfiguration) { | ||||
|         this.conditionSetIdentifier = styleConfiguration.conditionSetIdentifier; | ||||
|         this.staticStyle = styleConfiguration.staticStyle; | ||||
|         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, output => this.handleConditionSetResultUpdated(output)); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     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); | ||||
|             //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)); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     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; | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -1,159 +1,205 @@ | ||||
| /***************************************************************************** | ||||
| * 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="isEditing"> | ||||
|     <div v-if="domainObject" | ||||
|          class="c-c-editui__conditions c-c-container__container c-c__drag-wrapper" | ||||
|          :class="['widget-condition', { 'widget-condition--current': currentConditionIdentifier && (currentConditionIdentifier.key === conditionIdentifier.key) }]" | ||||
|          :data-condition-index="conditionIndex" | ||||
|          :draggable="!domainObject.isDefault" | ||||
|          @dragstart="dragStart" | ||||
|          @dragover.stop | ||||
| <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="c-condition-h__drop-target"></div> | ||||
|     <div v-if="isEditing" | ||||
|          class="c-condition c-condition--edit" | ||||
|     > | ||||
|         <div class="title-bar"> | ||||
|             <span | ||||
|                 class="c-c__menu-hamburger" | ||||
|                 :class="{ 'is-enabled': !domainObject.isDefault }" | ||||
|         <!-- 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="is-enabled flex-elem" | ||||
|                 :class="['c-c__disclosure-triangle', { 'c-c__disclosure-triangle--expanded': expanded }]" | ||||
|                 @click="expanded = !expanded" | ||||
|  | ||||
|             <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> | ||||
|             <div class="condition-summary"> | ||||
|                 <span class="condition-name">{{ domainObject.configuration.name }}</span> | ||||
|                 <span class="condition-description">{{ domainObject.configuration.name }}</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> | ||||
|             <span v-if="!domainObject.isDefault" | ||||
|                   class="is-enabled c-c__duplicate" | ||||
|                   @click="cloneCondition" | ||||
|             ></span> | ||||
|             <span v-if="!domainObject.isDefault" | ||||
|                   class="is-enabled c-c__trash" | ||||
|                   @click="removeCondition" | ||||
|             ></span> | ||||
|         </div> | ||||
|         <div v-if="expanded" | ||||
|              class="condition-config-edit widget-condition-content c-sw-editui__conditions-wrapper holder widget-conditions-wrapper flex-elem expanded" | ||||
|              class="c-condition__definition c-cdef" | ||||
|         > | ||||
|             <div id="conditionArea" | ||||
|                  class="c-c-editui__condition widget-conditions" | ||||
|             <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" | ||||
|             > | ||||
|                 <div class="c-c-condition"> | ||||
|                     <div class="c-c-condition__ui l-compact-form l-widget-condition has-local-controls"> | ||||
|                         <div> | ||||
|                             <ul class="t-widget-condition-config"> | ||||
|                                 <li> | ||||
|                                     <label>Condition Name</label> | ||||
|                                     <span class="controls"> | ||||
|                                         <input v-model="domainObject.configuration.name" | ||||
|                                                class="t-condition-input__name" | ||||
|                                                type="text" | ||||
|                                         > | ||||
|                                     </span> | ||||
|                                 </li> | ||||
|                                 <li> | ||||
|                                     <label>Output</label> | ||||
|                                     <span class="controls"> | ||||
|                                         <select v-model="selectedOutputKey" | ||||
|                                                 @change="checkInputValue" | ||||
|                                         > | ||||
|                                             <option value="">- Select Output -</option> | ||||
|                                             <option v-for="option in outputOptions" | ||||
|                                                     :key="option" | ||||
|                                                     :value="option" | ||||
|                                             > | ||||
|                                                 {{ initCap(option) }} | ||||
|                                             </option> | ||||
|                                         </select> | ||||
|                                         <input v-if="selectedOutputKey === outputOptions[2]" | ||||
|                                                v-model="domainObject.configuration.output" | ||||
|                                                class="t-condition-name-input" | ||||
|                                                type="text" | ||||
|                                                @blur="persist" | ||||
|                                         > | ||||
|                                     </span> | ||||
|                                 </li> | ||||
|                             </ul> | ||||
|                             <div v-if="!domainObject.isDefault" | ||||
|                                  class="widget-condition-content expanded" | ||||
|                             > | ||||
|                                 <ul class="t-widget-condition-config"> | ||||
|                                     <li class="has-local-controls t-condition"> | ||||
|                                         <label>Match when</label> | ||||
|                                         <span class="controls"> | ||||
|                                             <select v-model="trigger"> | ||||
|                                                 <option value="all">all criteria are met</option> | ||||
|                                                 <option value="any">any criteria are met</option> | ||||
|                                             </select> | ||||
|                                         </span> | ||||
|                                     </li> | ||||
|                                 </ul> | ||||
|                                 <ul v-if="telemetry.length" | ||||
|                                     class="t-widget-condition-config" | ||||
|                                 > | ||||
|                                     <Criterion v-for="(criterion, index) in domainObject.configuration.criteria" | ||||
|                                                :key="index" | ||||
|                                                :telemetry="telemetry" | ||||
|                                                :criterion="criterion" | ||||
|                                                :index="index" | ||||
|                                                :trigger="trigger" | ||||
|                                                @persist="persist" | ||||
|                                     /> | ||||
|                                 </ul> | ||||
|                                 <div class="holder c-c-button-wrapper align-left"> | ||||
|                                     <span class="c-c-label-spacer"></span> | ||||
|                                     <button | ||||
|                                         class="c-c-button c-c-button--minor add-criteria-button" | ||||
|                                         @click="addCriteria" | ||||
|                                     > | ||||
|                                         <span class="c-c-button__label">Add Criteria</span> | ||||
|                                     </button> | ||||
|                                 </div> | ||||
|                             </div> | ||||
|                 <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> | ||||
| <div v-else> | ||||
|     <div v-if="domainObject" | ||||
|          id="conditionArea" | ||||
|          class="c-cs-ui__conditions" | ||||
|          :class="['widget-condition', { 'widget-condition--current': currentConditionIdentifier && (currentConditionIdentifier.key === conditionIdentifier.key) }]" | ||||
|     <div v-else | ||||
|          class="c-condition c-condition--browse" | ||||
|     > | ||||
|         <div class="title-bar"> | ||||
|             <span class="condition-name"> | ||||
|                 {{ domainObject.configuration.name }} | ||||
|         <!-- Browse view --> | ||||
|         <div class="c-condition__header"> | ||||
|             <span class="c-condition__name"> | ||||
|                 {{ condition.configuration.name }} | ||||
|             </span> | ||||
|             <span class="condition-output"> | ||||
|                 Output: {{ domainObject.configuration.output }} | ||||
|             <span class="c-condition__output"> | ||||
|                 Output: {{ condition.configuration.output }} | ||||
|             </span> | ||||
|         </div> | ||||
|         <div class="condition-config"> | ||||
|             <span class="condition-description"> | ||||
|                 {{ domainObject.configuration.description }} | ||||
|             </span> | ||||
|         <div class="c-condition__summary"> | ||||
|             <condition-description :show-label="false" | ||||
|                                    :condition="condition" | ||||
|             /> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import ConditionClass from "@/plugins/condition/Condition"; | ||||
| import Criterion from '../../condition/components/Criterion.vue'; | ||||
| 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 | ||||
|         Criterion, | ||||
|         ConditionDescription | ||||
|     }, | ||||
|     props: { | ||||
|         conditionIdentifier: { | ||||
|             type: Object, | ||||
|             required: true | ||||
|         }, | ||||
|         currentConditionIdentifier: { | ||||
|         condition: { | ||||
|             type: Object, | ||||
|             required: true | ||||
|         }, | ||||
| @@ -169,104 +215,157 @@ export default { | ||||
|             type: Array, | ||||
|             required: true, | ||||
|             default: () => [] | ||||
|         }, | ||||
|         isDragging: { | ||||
|             type: Boolean, | ||||
|             default: false | ||||
|         }, | ||||
|         moveIndex: { | ||||
|             type: Number, | ||||
|             default: 0 | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             domainObject: this.domainObject, | ||||
|             currentCriteria: this.currentCriteria, | ||||
|             expanded: true, | ||||
|             trigger: 'all', | ||||
|             selectedOutputKey: '', | ||||
|             stringOutputField: false, | ||||
|             outputOptions: ['false', 'true', 'string'] | ||||
|             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.openmct.objects.get(this.conditionIdentifier).then((domainObject => { | ||||
|             this.domainObject = domainObject; | ||||
|             this.initialize(); | ||||
|         })); | ||||
|         this.setOutputSelection(); | ||||
|     }, | ||||
|     methods: { | ||||
|         initialize() { | ||||
|             this.setOutput(); | ||||
|             if (!this.domainObject.isDefault) { | ||||
|                 this.conditionClass = new ConditionClass(this.domainObject, this.openmct); | ||||
|                 this.conditionClass.on('conditionResultUpdated', this.handleConditionResult.bind(this)); | ||||
|         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.domainObject.configuration.criteria.push(criteriaObject); | ||||
|             this.condition.configuration.criteria.push(criteriaObject); | ||||
|         }, | ||||
|         dragStart(e) { | ||||
|             this.$emit('setMoveIndex', Number(e.target.getAttribute('data-condition-index'))); | ||||
|             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); | ||||
|         }, | ||||
|         destroy() { | ||||
|             if (this.conditionClass) { | ||||
|                 this.conditionClass.off('conditionResultUpdated', this.handleConditionResult.bind(this)); | ||||
|                 if (typeof this.conditionClass.destroy === 'function') { | ||||
|                     this.conditionClass.destroy(); | ||||
|                 } | ||||
|                 delete this.conditionClass; | ||||
|         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); | ||||
|             } | ||||
|         }, | ||||
|         handleConditionResult(args) { | ||||
|             this.$emit('conditionResultUpdated', { | ||||
|                 id: this.conditionIdentifier, | ||||
|                 result: args.data.result | ||||
|             }) | ||||
|         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.conditionIdentifier); | ||||
|             this.$emit('removeCondition', this.conditionIndex); | ||||
|         }, | ||||
|         cloneCondition(ev) { | ||||
|             this.$emit('cloneCondition', { | ||||
|                 identifier: this.conditionIdentifier, | ||||
|                 index: Number(ev.target.closest('.widget-condition').getAttribute('data-condition-index')) | ||||
|                 condition: this.condition, | ||||
|                 index: this.conditionIndex | ||||
|             }); | ||||
|         }, | ||||
|         setOutput() { | ||||
|             let conditionOutput = this.domainObject.configuration.output; | ||||
|             if (conditionOutput) { | ||||
|                 if (conditionOutput !== 'false' && conditionOutput !== 'true') { | ||||
|                     this.selectedOutputKey = 'string'; | ||||
|                 } else { | ||||
|                     this.selectedOutputKey = conditionOutput; | ||||
|                 } | ||||
|             } | ||||
|         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.openmct.objects.mutate(this.domainObject, 'configuration', this.domainObject.configuration); | ||||
|             this.$emit('updateCondition', { | ||||
|                 condition: this.condition, | ||||
|                 index: this.conditionIndex | ||||
|             }); | ||||
|         }, | ||||
|         checkInputValue() { | ||||
|             if (this.selectedOutputKey === 'string') { | ||||
|                 this.domainObject.configuration.output = ''; | ||||
|             } else { | ||||
|                 this.domainObject.configuration.output = this.selectedOutputKey; | ||||
|             } | ||||
|         }, | ||||
|         updateCurrentCondition() { | ||||
|             this.$emit('updateCurrentCondition', this.currentConditionIdentifier); | ||||
|         }, | ||||
|         hasTelemetry(identifier) { | ||||
|             // TODO: check parent domainObject.composition.hasTelemetry | ||||
|             return this.currentCriteria && identifier; | ||||
|         }, | ||||
|         initCap: function (string) { | ||||
|             return string.charAt(0).toUpperCase() + string.slice(1) | ||||
|         initCap(str) { | ||||
|             return str.charAt(0).toUpperCase() + str.slice(1) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| </script> | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -22,68 +22,62 @@ | ||||
|  | ||||
| <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="c-c condition-collection"> | ||||
|             <ul class="c-c__container-holder"> | ||||
|                 <li v-for="(conditionIdentifier, index) in conditionCollection" | ||||
|                     :key="conditionIdentifier.key" | ||||
|                 > | ||||
|                     <div v-if="isEditing" | ||||
|                          class="c-c__drag-ghost" | ||||
|                          @drop.prevent="dropCondition" | ||||
|                          @dragenter="dragEnter" | ||||
|                          @dragleave="dragLeave" | ||||
|                          @dragover.prevent | ||||
|                     ></div> | ||||
|                     <Condition :condition-identifier="conditionIdentifier" | ||||
|                                :current-condition-identifier="currentConditionIdentifier" | ||||
|                                :condition-index="index" | ||||
|                                :telemetry="telemetryObjs" | ||||
|                                :is-editing="isEditing" | ||||
|                                @updateCurrentCondition="updateCurrentCondition" | ||||
|                                @removeCondition="removeCondition" | ||||
|                                @cloneCondition="cloneCondition" | ||||
|                                @conditionResultUpdated="handleConditionResult" | ||||
|                                @setMoveIndex="setMoveIndex" | ||||
|                     /> | ||||
|                 </li> | ||||
|             </ul> | ||||
|  | ||||
|         <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 uuid from 'uuid'; | ||||
|  | ||||
| import Condition from './Condition.vue'; | ||||
| import ConditionManager from '../ConditionManager'; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct', 'domainObject'], | ||||
| @@ -91,46 +85,84 @@ export default { | ||||
|         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: [], | ||||
|             conditionResults: {}, | ||||
|             conditions: [], | ||||
|             currentConditionIdentifier: this.currentConditionIdentifier || {}, | ||||
|             telemetryObjs: [], | ||||
|             moveIndex: Number, | ||||
|             isDragging: false | ||||
|             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.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.instantiate = this.openmct.$injector.get('instantiate'); | ||||
|         this.composition = this.openmct.composition.get(this.domainObject); | ||||
|         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); | ||||
|         } else { | ||||
|             this.updateCurrentCondition(this.conditionCollection[0]); | ||||
|         } | ||||
|         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: { | ||||
|         handleConditionSetResultUpdated(data) { | ||||
|             this.$emit('conditionSetResultUpdated', data) | ||||
|         }, | ||||
|         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(e) { | ||||
|             let targetIndex = Array.from(document.querySelectorAll('.c-c__drag-ghost')).indexOf(e.target); | ||||
|             if (targetIndex > this.moveIndex) { targetIndex-- } // for 'downward' move | ||||
|         dropCondition(targetIndex) { | ||||
|             const oldIndexArr = Object.keys(this.conditionCollection); | ||||
|             const move = function (arr, old_index, new_index) { | ||||
|                 while (old_index < 0) { | ||||
| @@ -156,39 +188,13 @@ export default { | ||||
|             } | ||||
|  | ||||
|             this.reorder(reorderPlan); | ||||
|  | ||||
|             e.target.classList.remove("dragging"); | ||||
|         }, | ||||
|         dragComplete() { | ||||
|             this.isDragging = false; | ||||
|         }, | ||||
|         dragEnter(e) { | ||||
|             if (!this.isDragging) { return } | ||||
|             let targetIndex = Array.from(document.querySelectorAll('.c-c__drag-ghost')).indexOf(e.target); | ||||
|             if (targetIndex > this.moveIndex) { targetIndex-- } // for 'downward' move | ||||
|             if (this.moveIndex === targetIndex) { return } | ||||
|             e.target.classList.add("dragging"); | ||||
|         }, | ||||
|         dragLeave(e) { | ||||
|             e.target.classList.remove("dragging"); | ||||
|         }, | ||||
|         handleConditionResult(args) { | ||||
|             let idAsString = this.openmct.objects.makeKeyString(args.id); | ||||
|             this.conditionResults[idAsString] = args.result; | ||||
|             this.updateCurrentConditionId(); | ||||
|         }, | ||||
|         updateCurrentConditionId() { | ||||
|             let currentConditionIdentifier = this.conditionCollection[this.conditionCollection.length-1]; | ||||
|             for (let i = 0; i < this.conditionCollection.length - 1; i++) { | ||||
|                 let conditionIdAsString = this.openmct.objects.makeKeyString(this.conditionCollection[i]); | ||||
|                 if (this.conditionResults[conditionIdAsString]) { | ||||
|                     //first condition to be true wins | ||||
|                     currentConditionIdentifier = this.conditionCollection[i]; | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
|             this.$emit('currentConditionUpdated', currentConditionIdentifier); | ||||
|         }, | ||||
|         addTelemetryObject(domainObject) { | ||||
|             this.telemetryObjs.push(domainObject); | ||||
|             this.$emit('telemetryUpdated', this.telemetryObjs); | ||||
|         }, | ||||
|         removeTelemetryObject(identifier) { | ||||
|             let index = _.findIndex(this.telemetryObjs, (obj) => { | ||||
| @@ -200,99 +206,23 @@ export default { | ||||
|                 this.telemetryObjs.splice(index, 1); | ||||
|             } | ||||
|         }, | ||||
|         removeTelemetry(telemetryDomainObjectIdentifier) { | ||||
|             let index = _.findIndex(this.telemetryObjs, (obj) => { | ||||
|                 let objId = this.openmct.objects.makeKeyString(obj.identifier); | ||||
|                 let id = this.openmct.objects.makeKeyString(telemetryDomainObjectIdentifier); | ||||
|                 return objId === id; | ||||
|             }); | ||||
|             if (index > -1) { | ||||
|                 this.telemetryObjs.splice(index, 1); | ||||
|             } | ||||
|         addCondition() { | ||||
|             this.conditionManager.addCondition(); | ||||
|         }, | ||||
|         /* | ||||
|             Adds a condition to list via programatic creation of default for initial list, manual | ||||
|             creation via Add Condition button, or duplication via button in title bar of condition. | ||||
|             Params: | ||||
|             event: always null, | ||||
|             isDefault (boolean): true if conditionList is empty | ||||
|             isClone (boolean): true if duplicating a condition | ||||
|             definition (string): definition property of condition being duplicated with new name | ||||
|             index (number): index of condition being duplicated | ||||
|         */ | ||||
|         addCondition(event, isDefault, isClone, configuration, index) { | ||||
|             let conditionDomainObject = this.createConditionDomainObject(!!isDefault, isClone, configuration); | ||||
|             //persist the condition domain object so that we can do an openmct.objects.get on it and only persist the identifier in the conditionCollection of conditionSet | ||||
|             this.openmct.objects.mutate(conditionDomainObject, 'created', new Date()); | ||||
|             if (!isClone) { | ||||
|                 this.conditionCollection.unshift(conditionDomainObject.identifier); | ||||
|             } else { | ||||
|                 this.conditionCollection.splice(index + 1, 0, conditionDomainObject.identifier); | ||||
|             } | ||||
|             this.persist(); | ||||
|         updateCondition(data) { | ||||
|             this.conditionManager.updateCondition(data.condition, data.index); | ||||
|         }, | ||||
|         updateCurrentCondition(identifier) { | ||||
|             this.currentConditionIdentifier = identifier; | ||||
|         }, | ||||
|         createConditionDomainObject(isDefault, isClone, configuration) { | ||||
|             const configurationTemplate = { | ||||
|                 name: isDefault ? 'Default' : (isClone ? 'Copy of ' : '') + 'Unnamed Condition', | ||||
|                 output: 'false', | ||||
|                 trigger: 'any', | ||||
|                 criteria: isDefault ? [] : [{ | ||||
|                     telemetry: '', | ||||
|                     operation: '', | ||||
|                     input: [], | ||||
|                     metadata: '', | ||||
|                     key: '' | ||||
|                 }] | ||||
|             }; | ||||
|             let conditionObj = { | ||||
|                 isDefault: isDefault, | ||||
|                 type: 'condition', | ||||
|                 name: isDefault ? 'Default' : (isClone ? 'Copy of ' : '') + 'Unnamed Condition', | ||||
|                 identifier: { | ||||
|                     namespace: this.domainObject.identifier.namespace, | ||||
|                     key: uuid() | ||||
|                 }, | ||||
|                 configuration: isClone ? configuration: configurationTemplate, | ||||
|                 summary: 'summary description', | ||||
|                 created: new Date() | ||||
|             }; | ||||
|             let conditionDomainObjectKeyString = this.openmct.objects.makeKeyString(conditionObj.identifier); | ||||
|             let newDomainObject = this.instantiate(conditionObj, conditionDomainObjectKeyString); | ||||
|  | ||||
|             return newDomainObject.useCapability('adapter'); | ||||
|         }, | ||||
|         updateCondition(updatedCondition) { | ||||
|             let index = _.findIndex(this.conditions, (condition) => condition.id === updatedCondition.id); | ||||
|             this.conditions[index] = updatedCondition; | ||||
|         }, | ||||
|         removeCondition(identifier) { | ||||
|             let index = _.findIndex(this.conditionCollection, (condition) => { | ||||
|                 let conditionId = this.openmct.objects.makeKeyString(condition); | ||||
|                 let id = this.openmct.objects.makeKeyString(identifier); | ||||
|                 return conditionId === id; | ||||
|             }); | ||||
|             this.conditionCollection.splice(index, 1); | ||||
|             this.persist(); | ||||
|             this.updateCurrentConditionId(); | ||||
|         removeCondition(index) { | ||||
|             this.conditionManager.removeCondition(index); | ||||
|         }, | ||||
|         reorder(reorderPlan) { | ||||
|             let oldConditions = Array.from(this.conditionCollection); | ||||
|             reorderPlan.forEach((reorderEvent) => { | ||||
|                 this.$set(this.conditionCollection, reorderEvent.newIndex, oldConditions[reorderEvent.oldIndex]); | ||||
|             }); | ||||
|             this.persist(); | ||||
|             this.conditionManager.reorderConditions(reorderPlan); | ||||
|         }, | ||||
|         cloneCondition(condition) { | ||||
|             this.openmct.objects.get(condition.identifier).then((obj) => { | ||||
|                 obj.configuration.name = 'Copy of ' + obj.configuration.name; | ||||
|                 this.addCondition(null, false, true, obj.configuration, condition.index); | ||||
|             }); | ||||
|         cloneCondition(data) { | ||||
|             this.conditionManager.cloneCondition(data.condition, data.index); | ||||
|         }, | ||||
|         persist() { | ||||
|             this.openmct.objects.mutate(this.domainObject, 'configuration.conditionCollection', this.conditionCollection); | ||||
|         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> | ||||
							
								
								
									
										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> | ||||
| @@ -21,26 +21,45 @@ | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| <template> | ||||
| <div class="c-cs-edit w-condition-set"> | ||||
|     <div class="c-sw-edit__ui holder"> | ||||
|         <CurrentOutput :condition="currentCondition" /> | ||||
|         <TestData :is-editing="isEditing" /> | ||||
|         <ConditionCollection :is-editing="isEditing" | ||||
|                              @currentConditionUpdated="updateCurrentCondition" | ||||
| <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 | ||||
|     }, | ||||
| @@ -49,24 +68,31 @@ export default { | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             currentCondition: this.currentCondition | ||||
|             currentConditionOutput: '', | ||||
|             defaultConditionOutput: '', | ||||
|             telemetryObjs: [], | ||||
|             testData: {} | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         let conditionCollection = this.domainObject.configuration.conditionCollection; | ||||
|         this.currentConditionIdentifier = conditionCollection.length ? this.updateCurrentCondition(conditionCollection[0]) : null; | ||||
|         this.conditionSetIdentifier = this.openmct.objects.makeKeyString(this.domainObject.identifier); | ||||
|         this.testData = { | ||||
|             applied: false, | ||||
|             conditionTestInputs: this.domainObject.configuration.conditionTestData || [] | ||||
|         }; | ||||
|     }, | ||||
|     methods: { | ||||
|         setCurrentCondition() { | ||||
|             if (this.currentConditionIdentifier) { | ||||
|                 this.openmct.objects.get(this.currentConditionIdentifier).then((obj) => { | ||||
|                     this.currentCondition = obj; | ||||
|                 }); | ||||
|             } | ||||
|         updateCurrentOutput(currentConditionResult) { | ||||
|             this.currentConditionOutput = currentConditionResult.output; | ||||
|         }, | ||||
|         updateCurrentCondition(conditionIdentifier) { | ||||
|             this.currentConditionIdentifier = conditionIdentifier; | ||||
|             this.setCurrentCondition(); | ||||
|         updateDefaultOutput(output) { | ||||
|             this.currentConditionOutput = output; | ||||
|         }, | ||||
|         updateTelemetry(telemetryObjs) { | ||||
|             this.telemetryObjs = telemetryObjs; | ||||
|         }, | ||||
|         updateTestData(testData) { | ||||
|             this.testData = testData; | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -1,12 +1,38 @@ | ||||
| /***************************************************************************** | ||||
| * 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="has-local-controls t-condition"> | ||||
|     <label>{{ setRowLabel }}</label> | ||||
|     <span class="t-configuration"> | ||||
|         <span class="controls"> | ||||
|             <select v-model="criterion.telemetry" | ||||
| <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" | ||||
| @@ -16,9 +42,10 @@ | ||||
|             </select> | ||||
|         </span> | ||||
|         <span v-if="criterion.telemetry" | ||||
|               class="controls" | ||||
|               class="c-cdef__control" | ||||
|         > | ||||
|             <select v-model="criterion.metadata" | ||||
|             <select ref="metadataSelect" | ||||
|                     v-model="criterion.metadata" | ||||
|                     @change="updateOperations" | ||||
|             > | ||||
|                 <option value="">- Select Field -</option> | ||||
| @@ -31,10 +58,10 @@ | ||||
|             </select> | ||||
|         </span> | ||||
|         <span v-if="criterion.telemetry && criterion.metadata" | ||||
|               class="controls" | ||||
|               class="c-cdef__control" | ||||
|         > | ||||
|             <select v-model="criterion.operation" | ||||
|                     @change="updateOperationInputVisibility" | ||||
|                     @change="updateInputVisibilityAndValues" | ||||
|             > | ||||
|                 <option value="">- Select Comparison -</option> | ||||
|                 <option v-for="option in filteredOps" | ||||
| @@ -44,20 +71,43 @@ | ||||
|                     {{ option.text }} | ||||
|                 </option> | ||||
|             </select> | ||||
|             <input v-if="isInputOperation" | ||||
|                    v-model="criterion.input" | ||||
|                    class="t-condition-name-input" | ||||
|                    type="text" | ||||
|                    @blur="persist" | ||||
|             > | ||||
|             <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> | ||||
| </li> | ||||
|  | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import { OPERATIONS } from '../utils/operations'; | ||||
| import { INPUT_TYPES } from '../utils/operations'; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct'], | ||||
| @@ -82,82 +132,160 @@ export default { | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             telemetryMetadata: {}, | ||||
|             telemetryMetadataOptions: {}, | ||||
|             telemetryMetadataOptions: [], | ||||
|             operations: OPERATIONS, | ||||
|             filteredOps: [], | ||||
|             isInputOperation: false, | ||||
|             rowLabel: '' | ||||
|             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.criterion.metadata = ''; | ||||
|                 this.criterion.operation = ''; | ||||
|                 this.criterion.input = []; | ||||
|                 this.clearDependentFields(ev.target); | ||||
|                 this.persist(); | ||||
|             } | ||||
|             if (this.criterion.telemetry) { | ||||
|                 this.openmct.objects.get(this.criterion.telemetry).then((telemetryObject) => { | ||||
|                     this.telemetryMetadata = this.openmct.telemetry.getMetadata(telemetryObject); | ||||
|                     this.telemetryMetadataOptions = this.telemetryMetadata.values(); | ||||
|                 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(); | ||||
|                     this.updateOperationInputVisibility(); | ||||
|                 }); | ||||
|             } else { | ||||
|                 this.criterion.metadata = ''; | ||||
|             } | ||||
|             this.persist(); | ||||
|         }, | ||||
|         updateOperations(ev) { | ||||
|             if (ev) { | ||||
|                 this.criterion.operation = ''; | ||||
|                 this.criterion.input = []; | ||||
|         addMetaDataOptions(options) { | ||||
|             if (!this.telemetryMetadataOptions) { | ||||
|                 this.telemetryMetadataOptions = options; | ||||
|             } | ||||
|             let operationFormat = 'string'; | ||||
|             this.telemetryMetadata.valueMetadatas.forEach((value, index) => { | ||||
|                 if (value.key === this.criterion.metadata) { | ||||
|                     let valueMetadata = this.telemetryMetadataOptions[index]; | ||||
|                     if (valueMetadata.formatString) { | ||||
|                         operationFormat = 'number'; | ||||
|                     } else if (valueMetadata.format) { | ||||
|                         if (valueMetadata.format === 'utc') { | ||||
|                             operationFormat = 'number'; | ||||
|                         } else if (valueMetadata.format === 'enum') { | ||||
|                             operationFormat = 'enum'; | ||||
|                         } | ||||
|                     } | ||||
|             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); | ||||
|                 } | ||||
|             }); | ||||
|             this.filteredOps = [...this.operations.filter(op => op.appliesTo.indexOf(operationFormat) !== -1)]; | ||||
|         }, | ||||
|         updateOperationInputVisibility(ev) { | ||||
|         updateOperations(ev) { | ||||
|             this.updateOperationFormat(); | ||||
|             if (ev) { | ||||
|                 this.criterion.input = []; | ||||
|                 this.clearDependentFields(ev.target); | ||||
|                 this.persist(); | ||||
|             } | ||||
|             if (this.criterion.operation === '') { | ||||
|                 this.isInputOperation = false; | ||||
|             } else { | ||||
|                 for (let i = 0; i < this.filteredOps.length; i++) { | ||||
|                     if (this.criterion.operation === this.filteredOps[i].name) { | ||||
|                         this.isInputOperation = this.filteredOps[i].inputCount > 0; | ||||
|                         if (!this.isInputOperation) {this.criterion.input = ''} | ||||
|                     } | ||||
|         }, | ||||
|         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; | ||||
|                 } | ||||
|             } | ||||
|             this.persist(); | ||||
|             if (!this.inputCount) { | ||||
|                 this.criterion.input = []; | ||||
|             } | ||||
|         }, | ||||
|         updateMetadataSelection() { | ||||
|             this.updateOperationInputVisibility(); | ||||
|         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); | ||||
|   | ||||
| @@ -1,69 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * 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="current-output"> | ||||
|     <div v-if="condition" | ||||
|          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 && condition" | ||||
|          class="c-cs__ui_content" | ||||
|     > | ||||
|         <div> | ||||
|             <span class="current-output">{{ condition.configuration.output }}</span> | ||||
|         </div> | ||||
|     </div> | ||||
| </section> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| export default { | ||||
|     inject: ['openmct', 'domainObject'], | ||||
|     props: { | ||||
|         isEditing: Boolean, | ||||
|         condition: { | ||||
|             default: () => {return null;}, | ||||
|             type: Object | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             expanded: true | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|     }, | ||||
|     updated() { | ||||
|  | ||||
|     }, | ||||
|     methods: { | ||||
|  | ||||
|     } | ||||
| } | ||||
| </script> | ||||
|   | ||||
| @@ -23,54 +23,98 @@ | ||||
| <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="c-toggle-switch"> | ||||
|             <input | ||||
|                 type="checkbox" | ||||
|                 :checked="isApplied" | ||||
|                 @change="applyTestData" | ||||
|             > | ||||
|             <span class="c-toggle-switch__slider"></span> | ||||
|             <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> | ||||
| @@ -79,17 +123,113 @@ | ||||
| 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, | ||||
|             isApplied: 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(ev) { | ||||
|             this.$emit('change', ev.target.checked); | ||||
|         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 | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,147 +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__label { | ||||
|     color: #333; | ||||
| } | ||||
|  | ||||
| .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 1px #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,199 +0,0 @@ | ||||
| .widget-condition { | ||||
|     background-color: #eee; | ||||
|     margin: 0 0 0.33em; | ||||
|     border-radius: 3px; | ||||
|  | ||||
|     &--current { | ||||
|         background-color: #DEECFA; | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| .c-c-editui__conditions.widget-condition { | ||||
|     margin: 0; | ||||
| } | ||||
|  | ||||
| .c-c-button-wrapper { | ||||
|     border-top: solid 1px #ccc; | ||||
|     padding: 2px; | ||||
| } | ||||
|  | ||||
| .c-c-label-spacer { | ||||
|     display: inline-block; | ||||
|     width: 90px; | ||||
| } | ||||
|  | ||||
| .c-c-button[class*="--minor"], | ||||
| .c-c-button[class*='is-active'], | ||||
| .c-c-button--menu[class*="--minor"], | ||||
| .c-c-button--menu[class*='is-active'] { | ||||
|     border: solid 1px #666; | ||||
|     background-color: #fff; | ||||
|     padding: 0.1em 0.4em; | ||||
|     margin: 0.4em; | ||||
|     font-weight: normal; | ||||
|     color: #666; | ||||
|     border-radius: 6px; | ||||
| } | ||||
|  | ||||
|  | ||||
|  | ||||
| .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-condition { | ||||
|     padding: 0; | ||||
| } | ||||
|  | ||||
| .l-compact-form ul li { | ||||
|     padding: 0; | ||||
| } | ||||
|  | ||||
| .widget-condition-content.expanded { | ||||
|     margin: 0 3px; | ||||
| } | ||||
|  | ||||
| .widget-condition-content.expanded ul li { | ||||
|     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 { | ||||
|     &:active { | ||||
|         cursor: grabbing; | ||||
|         cursor: -moz-grabbing; | ||||
|         cursor: -webkit-grabbing; | ||||
|     } | ||||
|  | ||||
|     &:before { | ||||
|         content: $glyph-icon-menu-hamburger; | ||||
|     } | ||||
| } | ||||
|  | ||||
| .c-c__duplicate { | ||||
|     &:before { | ||||
|         content: $glyph-icon-duplicate; | ||||
|     } | ||||
| } | ||||
|  | ||||
| .c-c__trash { | ||||
|     &:before { | ||||
|         content: $glyph-icon-trash; | ||||
|     } | ||||
| } | ||||
|  | ||||
| .c-c__drag-ghost { | ||||
|     width: 100%; | ||||
|     min-height: 0.33em; | ||||
|     &.dragging { | ||||
|         min-height: 2em; | ||||
|         border: solid 1px blue; | ||||
|         background-color: lightblue; | ||||
|         border-radius: 2px; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										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,401 @@ | ||||
| /***************************************************************************** | ||||
| * 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" | ||||
|             > | ||||
|                 <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: '' | ||||
|         } | ||||
|     }, | ||||
|     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(); | ||||
|             } | ||||
|         }, | ||||
|         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; | ||||
|         }, | ||||
|         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; | ||||
|                 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; | ||||
|                 delete domainObjectStyles.conditionSetIdentifier; | ||||
|                 domainObjectStyles.styles = undefined; | ||||
|                 delete domainObjectStyles.styles; | ||||
|             } | ||||
|             if (_.isEmpty(domainObjectStyles)) { | ||||
|                 domainObjectStyles = undefined; | ||||
|             } | ||||
|  | ||||
|             this.persist(domainObjectStyles); | ||||
|         }, | ||||
|         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); | ||||
|             keys.forEach((key) => { | ||||
|                 if ((key !== 'styles') && | ||||
|                     (key !== 'staticStyle') && | ||||
|                     (key !== 'conditionSetIdentifier')) { | ||||
|                     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) => { | ||||
|                 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()); | ||||
|         }, | ||||
|         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.persist(this.getDomainObjectConditionalStyle()); | ||||
|             } | ||||
|         }, | ||||
|         getDomainObjectConditionalStyle() { | ||||
|             let objectStyle = { | ||||
|                 styles: this.conditionalStyles, | ||||
|                 staticStyle: this.staticStyle | ||||
|             }; | ||||
|             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] : {}; | ||||
|         }, | ||||
|         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,124 @@ | ||||
| /***************************************************************************** | ||||
|  * 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(); | ||||
|     } | ||||
|  | ||||
|     .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; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										152
									
								
								src/plugins/condition/criterion/AllTelemetryCriterion.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										152
									
								
								src/plugins/condition/criterion/AllTelemetryCriterion.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,152 @@ | ||||
| /***************************************************************************** | ||||
|  * 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"; | ||||
|  | ||||
| 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(options) { | ||||
|         options = Object.assign({}, | ||||
|             options, | ||||
|             { | ||||
|                 strategy: 'latest', | ||||
|                 size: 1 | ||||
|             } | ||||
|         ); | ||||
|  | ||||
|         if (!this.isValid()) { | ||||
|             return this.formatData({}, options.telemetryObjects); | ||||
|         } | ||||
|  | ||||
|         let keys = Object.keys(Object.assign({}, options.telemetryObjects)); | ||||
|         const telemetryRequests = keys | ||||
|             .map(key => this.openmct.telemetry.request( | ||||
|                 options.telemetryObjects[key], | ||||
|                 options | ||||
|             )); | ||||
|  | ||||
|         return Promise.all(telemetryRequests) | ||||
|             .then(telemetryRequestsResults => { | ||||
|                 let latestDatum; | ||||
|                 telemetryRequestsResults.forEach((results, index) => { | ||||
|                     latestDatum = results.length ? results[results.length - 1] : {}; | ||||
|                     if (index < telemetryRequestsResults.length-1) { | ||||
|                         if (latestDatum) { | ||||
|                             this.telemetryDataCache[latestDatum.id] = this.computeResult(latestDatum); | ||||
|                         } | ||||
|                     } | ||||
|                 }); | ||||
|                 return { | ||||
|                     id: this.id, | ||||
|                     data: this.formatData(latestDatum, options.telemetryObjects) | ||||
|                 }; | ||||
|             }); | ||||
|     } | ||||
|  | ||||
|     destroy() { | ||||
|         delete this.telemetryObjects; | ||||
|         delete this.telemetryDataCache; | ||||
|     } | ||||
| } | ||||
| @@ -20,8 +20,8 @@ | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import * as EventEmitter from 'eventemitter3'; | ||||
| import {OPERATIONS} from '../utils/operations'; | ||||
| import EventEmitter from 'EventEmitter'; | ||||
| import { OPERATIONS } from '../utils/operations'; | ||||
|  | ||||
| export default class TelemetryCriterion extends EventEmitter { | ||||
|  | ||||
| @@ -36,30 +36,75 @@ export default class TelemetryCriterion extends EventEmitter { | ||||
|         super(); | ||||
|  | ||||
|         this.openmct = openmct; | ||||
|         this.objectAPI = this.openmct.objects; | ||||
|         this.telemetryAPI = this.openmct.telemetry; | ||||
|         this.telemetryDomainObjectDefinition = telemetryDomainObjectDefinition; | ||||
|         this.id = telemetryDomainObjectDefinition.id; | ||||
|         this.telemetry = telemetryDomainObjectDefinition.telemetry; | ||||
|         this.operation = telemetryDomainObjectDefinition.operation; | ||||
|         this.input = telemetryDomainObjectDefinition.input; | ||||
|         this.metadata = telemetryDomainObjectDefinition.metadata; | ||||
|         this.subscription = null; | ||||
|         this.telemetryObjectIdAsString = null; | ||||
|         this.objectAPI.get(this.objectAPI.makeKeyString(this.telemetry)).then((obj) => this.initialize(obj)); | ||||
|     } | ||||
|         this.result = undefined; | ||||
|  | ||||
|     initialize(obj) { | ||||
|         this.telemetryObject = obj; | ||||
|         this.telemetryObjectIdAsString = this.objectAPI.makeKeyString(this.telemetryObject.identifier); | ||||
|         this.initialize(); | ||||
|         this.emitEvent('criterionUpdated', this); | ||||
|     } | ||||
|  | ||||
|     handleSubscription(data) { | ||||
|         let result = this.computeResult(data); | ||||
|         this.emitEvent('criterionResultUpdated', { | ||||
|             result: result, | ||||
|             error: null | ||||
|         }) | ||||
|     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]; | ||||
|     } | ||||
|  | ||||
|     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(options) { | ||||
|         options = Object.assign({}, | ||||
|             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] : {}; | ||||
|             return { | ||||
|                 id: this.id, | ||||
|                 data: this.formatData(latestDatum) | ||||
|             }; | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     findOperation(operation) { | ||||
| @@ -72,17 +117,17 @@ export default class TelemetryCriterion extends EventEmitter { | ||||
|     } | ||||
|  | ||||
|     computeResult(data) { | ||||
|         let comparator = this.findOperation(this.operation); | ||||
|         let params = []; | ||||
|         let result = false; | ||||
|         params.push(data[this.metadata]); | ||||
|         if (this.input instanceof Array && this.input.length) { | ||||
|             params.push(this.input[0]); | ||||
|         } else if (this.input) { | ||||
|             params.push(this.input); | ||||
|         } | ||||
|         if (typeof comparator === 'function') { | ||||
|             result = comparator(params); | ||||
|         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); | ||||
|             } | ||||
|         } | ||||
|         return result; | ||||
|     } | ||||
| @@ -94,35 +139,9 @@ export default class TelemetryCriterion extends EventEmitter { | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     isValid() { | ||||
|         return this.telemetryObject && this.metadata && this.operation; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      *  Subscribes to the telemetry object and returns an unsubscribe function | ||||
|      */ | ||||
|     subscribe() { | ||||
|         this.unsubscribe(); | ||||
|         this.subscription = this.telemetryAPI.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(); | ||||
|         } | ||||
|         delete this.subscription; | ||||
|     } | ||||
|  | ||||
|     destroy() { | ||||
|         this.unsubscribe(); | ||||
|         this.emitEvent('criterionRemoved'); | ||||
|         delete this.telemetryObjectIdAsString; | ||||
|         delete this.telemetryObject; | ||||
|         delete this.telemetryObjectIdAsString; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -24,7 +24,6 @@ import TelemetryCriterion from "./TelemetryCriterion"; | ||||
|  | ||||
| let openmct = {}, | ||||
|     mockListener, | ||||
|     mockListener2, | ||||
|     testCriterionDefinition, | ||||
|     testTelemetryObject, | ||||
|     telemetryCriterion; | ||||
| @@ -37,40 +36,57 @@ 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', ['get', 'makeKeyString']); | ||||
|         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 = jasmine.createSpyObj('telemetry', ['isTelemetryObject', "subscribe", "getMetadata", "getValueFormatter"]); | ||||
|         openmct.telemetry.isTelemetryObject.and.returnValue(true); | ||||
|         openmct.telemetry.subscribe.and.returnValue(function () {}); | ||||
|         openmct.telemetry.getMetadata.and.returnValue(testTelemetryObject.telemetry.values); | ||||
|         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) | ||||
|             telemetry: openmct.objects.makeKeyString(testTelemetryObject.identifier), | ||||
|             operation: 'textContains', | ||||
|             metadata: 'value', | ||||
|             input: ['Hell'], | ||||
|             telemetryObject: testTelemetryObject | ||||
|         }; | ||||
|  | ||||
|         mockListener = jasmine.createSpy('listener'); | ||||
|         mockListener2 = jasmine.createSpy('updatedListener', (data) => { | ||||
|             console.log(data); | ||||
|         }); | ||||
|  | ||||
|         telemetryCriterion = new TelemetryCriterion( | ||||
|             testCriterionDefinition, | ||||
| @@ -78,39 +94,28 @@ describe("The telemetry criterion", function () { | ||||
|         ); | ||||
|  | ||||
|         telemetryCriterion.on('criterionResultUpdated', mockListener); | ||||
|         telemetryCriterion.on('criterionUpdated', mockListener2); | ||||
|  | ||||
|     }); | ||||
|  | ||||
|     it("initializes with a telemetry objectId as string", function () { | ||||
|         telemetryCriterion.initialize(testTelemetryObject); | ||||
|         expect(telemetryCriterion.telemetryObjectIdAsString).toEqual(testTelemetryObject.identifier.key); | ||||
|         expect(mockListener2).toHaveBeenCalled(); | ||||
|     }); | ||||
|  | ||||
|     it("subscribes to telemetry providers", function () { | ||||
|         telemetryCriterion.subscribe(); | ||||
|         expect(telemetryCriterion.subscription).toBeDefined(); | ||||
|     }); | ||||
|  | ||||
|     it("emits update event on new data from telemetry providers", function () { | ||||
|         spyOn(telemetryCriterion, 'emitEvent').and.callThrough(); | ||||
|         telemetryCriterion.handleSubscription({ | ||||
|             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(telemetryCriterion.emitEvent).toHaveBeenCalled(); | ||||
|         expect(mockListener).toHaveBeenCalled(); | ||||
|     }); | ||||
|  | ||||
|     it("un-subscribes from telemetry providers", function () { | ||||
|         telemetryCriterion.subscribe(); | ||||
|         expect(telemetryCriterion.subscription).toBeDefined(); | ||||
|         telemetryCriterion.destroy(); | ||||
|         expect(telemetryCriterion.subscription).toBeUndefined(); | ||||
|         expect(telemetryCriterion.telemetryObjectIdAsString).toBeUndefined(); | ||||
|         expect(telemetryCriterion.telemetryObject).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(); | ||||
|     // }); | ||||
| }); | ||||
|   | ||||
| @@ -21,36 +21,47 @@ | ||||
|  *****************************************************************************/ | ||||
| import ConditionSetViewProvider from './ConditionSetViewProvider.js'; | ||||
| 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-conditional',  // 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)); | ||||
|  | ||||
|     } | ||||
|   | ||||
| @@ -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,16 +1,68 @@ | ||||
| export const computeCondition = (resultMap, allMustBeTrue) => { | ||||
|     let result = false; | ||||
|     for (let key in resultMap) { | ||||
|         if (resultMap.hasOwnProperty(key)) { | ||||
|             result = resultMap[key]; | ||||
|             if (allMustBeTrue && !result) { | ||||
|                 //If we want all conditions to be true, then even one negative result should break. | ||||
|                 break; | ||||
|             } else if (!allMustBeTrue && result) { | ||||
|                 //If we want at least one condition to be true, then even one positive result should break. | ||||
|                 break; | ||||
|             } | ||||
| /***************************************************************************** | ||||
|  * 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 result; | ||||
| }; | ||||
|  | ||||
|     return true; | ||||
| } | ||||
|  | ||||
| function matchAny(results) { | ||||
|     for (const result of results) { | ||||
|         if (result) { | ||||
|             return true; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     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,140 @@ | ||||
| /***************************************************************************** | ||||
|  * 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; | ||||
| }; | ||||
|  | ||||
| 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 ' + values.join(', '); | ||||
|         } | ||||
|     }, | ||||
|     { | ||||
|         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 ' + values.join(', '); | ||||
|         } | ||||
|     }, | ||||
|     { | ||||
|         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 ' > ' + values.join(', '); | ||||
|         } | ||||
|     }, | ||||
|     { | ||||
|         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 ' < ' + values.join(', '); | ||||
|         } | ||||
|     }, | ||||
|     { | ||||
|         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 ' >= ' + values.join(', '); | ||||
|         } | ||||
|     }, | ||||
|     { | ||||
|         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 ' <= ' + values.join(', '); | ||||
|         } | ||||
|     }, | ||||
|     { | ||||
|         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 +146,7 @@ export const OPERATIONS = [ | ||||
|         appliesTo: ['string'], | ||||
|         inputCount: 1, | ||||
|         getDescription: function (values) { | ||||
|             return ' contains ' + values[0]; | ||||
|             return ' contains ' + values.join(', '); | ||||
|         } | ||||
|     }, | ||||
|     { | ||||
| @@ -116,7 +158,7 @@ export const OPERATIONS = [ | ||||
|         appliesTo: ['string'], | ||||
|         inputCount: 1, | ||||
|         getDescription: function (values) { | ||||
|             return ' does not contain ' + values[0]; | ||||
|             return ' does not contain ' + values.join(', '); | ||||
|         } | ||||
|     }, | ||||
|     { | ||||
| @@ -128,7 +170,7 @@ export const OPERATIONS = [ | ||||
|         appliesTo: ['string'], | ||||
|         inputCount: 1, | ||||
|         getDescription: function (values) { | ||||
|             return ' starts with ' + values[0]; | ||||
|             return ' starts with ' + values.join(', '); | ||||
|         } | ||||
|     }, | ||||
|     { | ||||
| @@ -140,7 +182,7 @@ export const OPERATIONS = [ | ||||
|         appliesTo: ['string'], | ||||
|         inputCount: 1, | ||||
|         getDescription: function (values) { | ||||
|             return ' ends with ' + values[0]; | ||||
|             return ' ends with ' + values.join(', '); | ||||
|         } | ||||
|     }, | ||||
|     { | ||||
| @@ -152,7 +194,7 @@ export const OPERATIONS = [ | ||||
|         appliesTo: ['string'], | ||||
|         inputCount: 1, | ||||
|         getDescription: function (values) { | ||||
|             return ' is exactly ' + values[0]; | ||||
|             return ' is exactly ' + values.join(', '); | ||||
|         } | ||||
|     }, | ||||
|     { | ||||
| @@ -182,25 +224,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 ' + values.join(', '); | ||||
|         } | ||||
|     }, | ||||
|     { | ||||
|         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 ' + values.join(', '); | ||||
|         } | ||||
|     }, | ||||
|     { | ||||
|         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)); | ||||
|             } 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> | ||||
							
								
								
									
										239
									
								
								src/plugins/notebook/components/notebook-embed.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										239
									
								
								src/plugins/notebook/components/notebook-embed.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,239 @@ | ||||
| <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() { | ||||
|             this.openmct.time.stopClock(); | ||||
|             this.openmct.time.bounds({ | ||||
|                 start: this.embed.bounds.start, | ||||
|                 end: this.embed.bounds.end | ||||
|             }); | ||||
|  | ||||
|             const link = this.embed.historicLink; | ||||
|             if (!link) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             window.location.href = link; | ||||
|             const message = 'Time bounds 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(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> | ||||
							
								
								
									
										114
									
								
								src/plugins/notebook/components/notebook-menu-switcher.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								src/plugins/notebook/components/notebook-menu-switcher.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,114 @@ | ||||
| <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 {}; | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     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) { | ||||
|             let element = document.getElementsByClassName("l-shell__main-container")[0]; | ||||
|             const bounds = this.openmct.time.bounds(); | ||||
|             const objectPath = this.openmct.router.path; | ||||
|             const snapshotMeta = { | ||||
|                 bounds, | ||||
|                 link: window.location.href, | ||||
|                 objectPath, | ||||
|                 openmct: this.openmct | ||||
|             }; | ||||
|  | ||||
|             this.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> | ||||
							
								
								
									
										563
									
								
								src/plugins/notebook/components/notebook.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										563
									
								
								src/plugins/notebook/components/notebook.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,563 @@ | ||||
| <template> | ||||
| <div class="c-notebook"> | ||||
|     <div class="c-notebook__head"> | ||||
|         <Search class="c-notebook__search" | ||||
|                 :value="search" | ||||
|                 @input="throttledSearchItem" | ||||
|                 @clear="throttledSearchItem" | ||||
|         /> | ||||
|     </div> | ||||
|     <SearchResults v-if="search.length" | ||||
|                    ref="searchResults" | ||||
|                    :results="getSearchResults()" | ||||
|                    @changeSectionPage="changeSelectedSection" | ||||
|     /> | ||||
|  | ||||
|     <div v-if="!search.length" | ||||
|          class="c-notebook__body" | ||||
|     > | ||||
|         <Sidebar ref="sidebar" | ||||
|                  class="c-notebook__nav c-sidebar c-drawer c-drawer--align-left" | ||||
|                  :class="[{'is-expanded': showNav}, {'c-drawer--push': !sidebarCoversEntries}, {'c-drawer--overlays': sidebarCoversEntries}]" | ||||
|                  :default-page-id="defaultPageId" | ||||
|                  :default-section-id="defaultSectionId" | ||||
|                  :domain-object="internalDomainObject" | ||||
|                  :page-title="internalDomainObject.configuration.pageTitle" | ||||
|                  :pages="pages" | ||||
|                  :section-title="internalDomainObject.configuration.sectionTitle" | ||||
|                  :sections="sections" | ||||
|                  :sidebar-covers-entries="sidebarCoversEntries" | ||||
|                  @updatePage="updatePage" | ||||
|                  @updateSection="updateSection" | ||||
|                  @toggleNav="toggleNav" | ||||
|         /> | ||||
|         <div class="c-notebook__page-view"> | ||||
|             <div class="c-notebook__page-view__header"> | ||||
|                 <button class="c-notebook__toggle-nav-button c-icon-button c-icon-button--major icon-menu-hamburger" | ||||
|                         @click="toggleNav" | ||||
|                 ></button> | ||||
|                 <div class="c-notebook__page-view__path c-path"> | ||||
|                     <span class="c-notebook__path__section c-path__item"> | ||||
|                         {{ getSelectedSection() ? getSelectedSection().name : '' }} | ||||
|                     </span> | ||||
|                     <span class="c-notebook__path__page c-path__item"> | ||||
|                         {{ getSelectedPage() ? getSelectedPage().name : '' }} | ||||
|                     </span> | ||||
|                 </div> | ||||
|                 <div class="c-notebook__page-view__controls"> | ||||
|                     <select v-model="showTime" | ||||
|                             class="c-notebook__controls__time" | ||||
|                     > | ||||
|                         <option value="0" | ||||
|                                 :selected="showTime === 0" | ||||
|                         > | ||||
|                             Show all | ||||
|                         </option> | ||||
|                         <option value="1" | ||||
|                                 :selected="showTime === 1" | ||||
|                         >Last hour</option> | ||||
|                         <option value="8" | ||||
|                                 :selected="showTime === 8" | ||||
|                         >Last 8 hours</option> | ||||
|                         <option value="24" | ||||
|                                 :selected="showTime === 24" | ||||
|                         >Last 24 hours</option> | ||||
|                     </select> | ||||
|                     <select v-model="defaultSort" | ||||
|                             class="c-notebook__controls__time" | ||||
|                     > | ||||
|                         <option value="newest" | ||||
|                                 :selected="defaultSort === 'newest'" | ||||
|                         >Newest first</option> | ||||
|                         <option value="oldest" | ||||
|                                 :selected="defaultSort === 'oldest'" | ||||
|                         >Oldest first</option> | ||||
|                     </select> | ||||
|                 </div> | ||||
|             </div> | ||||
|             <div class="c-notebook__drag-area icon-plus" | ||||
|                  @click="newEntry()" | ||||
|                  @dragover="dragOver" | ||||
|                  @drop.capture="dropCapture" | ||||
|                  @drop="dropOnEntry($event)" | ||||
|             > | ||||
|                 <span class="c-notebook__drag-area__label"> | ||||
|                     To start a new entry, click here or drag and drop any object | ||||
|                 </span> | ||||
|             </div> | ||||
|             <div v-if="selectedSection && selectedPage" | ||||
|                  ref="notebookEntries" | ||||
|                  class="c-notebook__entries" | ||||
|             > | ||||
|                 <NotebookEntry v-for="entry in filteredAndSortedEntries" | ||||
|                                :key="entry.id" | ||||
|                                :entry="entry" | ||||
|                                :domain-object="internalDomainObject" | ||||
|                                :selected-page="getSelectedPage()" | ||||
|                                :selected-section="getSelectedSection()" | ||||
|                                :read-only="false" | ||||
|                                @updateEntries="updateEntries" | ||||
|                 /> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import NotebookEntry from './notebook-entry.vue'; | ||||
| import Search from '@/ui/components/search.vue'; | ||||
| import SearchResults from './search-results.vue'; | ||||
| import Sidebar from './sidebar.vue'; | ||||
| import { clearDefaultNotebook, getDefaultNotebook, setDefaultNotebook, setDefaultNotebookSection, setDefaultNotebookPage } from '../utils/notebook-storage'; | ||||
| import { addNotebookEntry, createNewEmbed, getNotebookEntries } from '../utils/notebook-entries'; | ||||
| import { throttle } from 'lodash'; | ||||
|  | ||||
| const DEFAULT_CLASS = 'is-notebook-default'; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct', 'domainObject', 'snapshotContainer'], | ||||
|     components: { | ||||
|         NotebookEntry, | ||||
|         Search, | ||||
|         SearchResults, | ||||
|         Sidebar | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             defaultPageId: getDefaultNotebook() ? getDefaultNotebook().page.id : '', | ||||
|             defaultSectionId: getDefaultNotebook() ? getDefaultNotebook().section.id : '', | ||||
|             defaultSort: this.domainObject.configuration.defaultSort, | ||||
|             focusEntryId: null, | ||||
|             internalDomainObject: this.domainObject, | ||||
|             search: '', | ||||
|             showTime: 0, | ||||
|             showNav: false, | ||||
|             sidebarCoversEntries: false | ||||
|         } | ||||
|     }, | ||||
|     computed: { | ||||
|         filteredAndSortedEntries() { | ||||
|             const filterTime = Date.now(); | ||||
|             const pageEntries = getNotebookEntries(this.internalDomainObject, this.selectedSection, this.selectedPage) || []; | ||||
|  | ||||
|             const hours = parseInt(this.showTime, 10); | ||||
|             const filteredPageEntriesByTime = hours | ||||
|                 ? pageEntries.filter(entry => (filterTime - entry.createdOn) <= hours * 60 * 60 * 1000) | ||||
|                 : pageEntries; | ||||
|  | ||||
|             return this.defaultSort === 'oldest' | ||||
|                 ? filteredPageEntriesByTime | ||||
|                 : [...filteredPageEntriesByTime].reverse(); | ||||
|         }, | ||||
|         pages() { | ||||
|             return this.getPages() || []; | ||||
|         }, | ||||
|         sections() { | ||||
|             return this.internalDomainObject.configuration.sections || []; | ||||
|         }, | ||||
|         selectedPage() { | ||||
|             const pages = this.getPages(); | ||||
|             if (!pages) { | ||||
|                 return null; | ||||
|             } | ||||
|  | ||||
|             return pages.find(page => page.isSelected); | ||||
|         }, | ||||
|         selectedSection() { | ||||
|             if (!this.sections.length) { | ||||
|                 return null; | ||||
|             } | ||||
|  | ||||
|             return this.sections.find(section => section.isSelected); | ||||
|         } | ||||
|     }, | ||||
|     watch: { | ||||
|     }, | ||||
|     beforeMount() { | ||||
|         this.throttledSearchItem = throttle(this.searchItem, 500); | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.unlisten = this.openmct.objects.observe(this.internalDomainObject, '*', this.updateInternalDomainObject); | ||||
|         this.formatSidebar(); | ||||
|         window.addEventListener('orientationchange', this.formatSidebar); | ||||
|  | ||||
|         this.navigateToSectionPage(); | ||||
|     }, | ||||
|     beforeDestroy() { | ||||
|         if (this.unlisten) { | ||||
|             this.unlisten(); | ||||
|         } | ||||
|     }, | ||||
|     updated: function () { | ||||
|         this.$nextTick(function () { | ||||
|             this.focusOnEntryId(); | ||||
|         }) | ||||
|     }, | ||||
|     methods: { | ||||
|         addDefaultClass() { | ||||
|             const classList = this.internalDomainObject.classList || []; | ||||
|             if (classList.includes(DEFAULT_CLASS)) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             classList.push(DEFAULT_CLASS); | ||||
|             this.mutateObject('classList', classList); | ||||
|         }, | ||||
|         changeSelectedSection({ sectionId, pageId }) { | ||||
|             const sections = this.sections.map(s => { | ||||
|                 s.isSelected = false; | ||||
|  | ||||
|                 if (s.id === sectionId) { | ||||
|                     s.isSelected = true; | ||||
|                 } | ||||
|  | ||||
|                 s.pages.forEach((p, i) => { | ||||
|                     p.isSelected = false; | ||||
|  | ||||
|                     if (pageId && pageId === p.id) { | ||||
|                         p.isSelected = true; | ||||
|                     } | ||||
|  | ||||
|                     if (!pageId && i === 0) { | ||||
|                         p.isSelected = true; | ||||
|                     } | ||||
|                 }); | ||||
|  | ||||
|                 return s; | ||||
|             }); | ||||
|  | ||||
|             this.updateSection({ sections }); | ||||
|             this.throttledSearchItem(''); | ||||
|         }, | ||||
|         createNotebookStorageObject() { | ||||
|             const notebookMeta = { | ||||
|                 name: this.internalDomainObject.name, | ||||
|                 identifier: this.internalDomainObject.identifier | ||||
|             }; | ||||
|             const page = this.getSelectedPage(); | ||||
|             const section = this.getSelectedSection(); | ||||
|  | ||||
|             return { | ||||
|                 notebookMeta, | ||||
|                 section, | ||||
|                 page | ||||
|             } | ||||
|         }, | ||||
|         dragOver(event) { | ||||
|             event.preventDefault(); | ||||
|             event.dataTransfer.dropEffect = "copy"; | ||||
|         }, | ||||
|         dropCapture(event) { | ||||
|             const isEditing = this.openmct.editor.isEditing(); | ||||
|             if (isEditing) { | ||||
|                 this.openmct.editor.cancel(); | ||||
|             } | ||||
|         }, | ||||
|         dropOnEntry(event) { | ||||
|             event.preventDefault(); | ||||
|             event.stopImmediatePropagation(); | ||||
|  | ||||
|             const snapshotId = event.dataTransfer.getData('snapshot/id'); | ||||
|             if (snapshotId.length) { | ||||
|                 const snapshot = this.snapshotContainer.getSnapshot(snapshotId); | ||||
|                 this.newEntry(snapshot); | ||||
|                 this.snapshotContainer.removeSnapshot(snapshotId); | ||||
|  | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             const data = event.dataTransfer.getData('openmct/domain-object-path'); | ||||
|             const objectPath = JSON.parse(data); | ||||
|             const bounds = this.openmct.time.bounds(); | ||||
|             const snapshotMeta = { | ||||
|                 bounds, | ||||
|                 link: null, | ||||
|                 objectPath, | ||||
|                 openmct: this.openmct | ||||
|             }; | ||||
|             const embed = createNewEmbed(snapshotMeta); | ||||
|             this.newEntry(embed); | ||||
|         }, | ||||
|         focusOnEntryId() { | ||||
|             if (!this.focusEntryId) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             const element = this.$refs.notebookEntries.querySelector(`#${this.focusEntryId}`); | ||||
|  | ||||
|             if (!element) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             element.focus(); | ||||
|             this.focusEntryId = null; | ||||
|         }, | ||||
|         formatSidebar() { | ||||
|             /* | ||||
|                 Determine if the sidebar should slide over content, or compress it | ||||
|                 Slide over checks: | ||||
|                 - phone (all orientations) | ||||
|                 - tablet portrait | ||||
|                 - in a layout frame (within .c-so-view) | ||||
|             */ | ||||
|             const classList = document.querySelector('body').classList; | ||||
|             const isPhone = Array.from(classList).includes('phone'); | ||||
|             const isTablet = Array.from(classList).includes('tablet'); | ||||
|             const isPortrait = window.screen.orientation.type.includes('portrait'); | ||||
|             const isInLayout = !!this.$el.closest('.c-so-view'); | ||||
|             const sidebarCoversEntries = (isPhone || (isTablet && isPortrait) || isInLayout); | ||||
|             this.sidebarCoversEntries = sidebarCoversEntries; | ||||
|         }, | ||||
|         getDefaultNotebookObject() { | ||||
|             const oldNotebookStorage = getDefaultNotebook(); | ||||
|             if (!oldNotebookStorage) { | ||||
|                 return null; | ||||
|             } | ||||
|  | ||||
|             return this.openmct.objects.get(oldNotebookStorage.notebookMeta.identifier).then(d => d); | ||||
|         }, | ||||
|         getPage(section, id) { | ||||
|             return section.pages.find(p => p.id === id); | ||||
|         }, | ||||
|         getSection(id) { | ||||
|             return this.sections.find(s => s.id === id); | ||||
|         }, | ||||
|         getSearchResults() { | ||||
|             if (!this.search.length) { | ||||
|                 return []; | ||||
|             } | ||||
|  | ||||
|             const output = []; | ||||
|             const entries = this.internalDomainObject.configuration.entries; | ||||
|             const sectionKeys = Object.keys(entries); | ||||
|             sectionKeys.forEach(sectionKey => { | ||||
|                 const pages = entries[sectionKey]; | ||||
|                 const pageKeys = Object.keys(pages); | ||||
|                 pageKeys.forEach(pageKey => { | ||||
|                     const pageEntries = entries[sectionKey][pageKey]; | ||||
|                     pageEntries.forEach(entry => { | ||||
|                         if (entry.text && entry.text.toLowerCase().includes(this.search.toLowerCase())) { | ||||
|                             const section = this.getSection(sectionKey); | ||||
|                             output.push({ | ||||
|                                 section, | ||||
|                                 page: this.getPage(section, pageKey), | ||||
|                                 entry | ||||
|                             }); | ||||
|                         } | ||||
|                     }); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             return output; | ||||
|         }, | ||||
|         getPages() { | ||||
|             const selectedSection = this.getSelectedSection(); | ||||
|             if (!selectedSection || !selectedSection.pages.length) { | ||||
|                 return []; | ||||
|             } | ||||
|  | ||||
|             return selectedSection.pages; | ||||
|         }, | ||||
|         getSelectedPage() { | ||||
|             const pages = this.getPages(); | ||||
|             if (!pages) { | ||||
|                 return null; | ||||
|             } | ||||
|  | ||||
|             const selectedPage = pages.find(page => page.isSelected); | ||||
|             if (selectedPage) { | ||||
|                 return selectedPage; | ||||
|             } | ||||
|  | ||||
|             if (!selectedPage && !pages.length) { | ||||
|                 return null; | ||||
|             } | ||||
|  | ||||
|             pages[0].isSelected = true; | ||||
|  | ||||
|             return pages[0]; | ||||
|         }, | ||||
|         getSelectedSection() { | ||||
|             if (!this.sections.length) { | ||||
|                 return null; | ||||
|             } | ||||
|  | ||||
|             return this.sections.find(section => section.isSelected); | ||||
|         }, | ||||
|         mutateObject(key, value) { | ||||
|             this.openmct.objects.mutate(this.internalDomainObject, key, value); | ||||
|         }, | ||||
|         navigateToSectionPage() { | ||||
|             const { pageId, sectionId } = this.openmct.router.getParams(); | ||||
|             if(!pageId || !sectionId) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             const sections = this.sections.map(s => { | ||||
|                 s.isSelected = false; | ||||
|                 if (s.id === sectionId) { | ||||
|                     s.isSelected = true; | ||||
|                     s.pages.forEach(p => p.isSelected = (p.id === pageId)); | ||||
|                 } | ||||
|  | ||||
|                 return s; | ||||
|             }); | ||||
|  | ||||
|             this.updateSection({ sections }); | ||||
|         }, | ||||
|         newEntry(embed = null) { | ||||
|             this.search = ''; | ||||
|             const notebookStorage = this.createNotebookStorageObject(); | ||||
|             this.updateDefaultNotebook(notebookStorage); | ||||
|             const id = addNotebookEntry(this.openmct, this.internalDomainObject, notebookStorage, embed); | ||||
|             this.focusEntryId = id; | ||||
|             this.search = ''; | ||||
|         }, | ||||
|         orientationChange() { | ||||
|             this.formatSidebar(); | ||||
|         }, | ||||
|         removeDefaultClass(domainObject) { | ||||
|             if (!domainObject) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             const classList = domainObject.classList || []; | ||||
|             const index = classList.indexOf(DEFAULT_CLASS); | ||||
|             if (!classList.length || index < 0) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             classList.splice(index, 1); | ||||
|             this.openmct.objects.mutate(domainObject, 'classList', classList); | ||||
|         }, | ||||
|         searchItem(input) { | ||||
|             this.search = input; | ||||
|         }, | ||||
|         toggleNav() { | ||||
|             this.showNav = !this.showNav; | ||||
|         }, | ||||
|         async updateDefaultNotebook(notebookStorage) { | ||||
|             const defaultNotebookObject = await this.getDefaultNotebookObject(); | ||||
|             this.removeDefaultClass(defaultNotebookObject); | ||||
|             setDefaultNotebook(notebookStorage); | ||||
|             this.addDefaultClass(); | ||||
|             this.defaultSectionId = notebookStorage.section.id; | ||||
|             this.defaultPageId = notebookStorage.page.id; | ||||
|         }, | ||||
|         updateDefaultNotebookPage(pages, id) { | ||||
|             if (!id) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             const notebookStorage = getDefaultNotebook(); | ||||
|             if (!notebookStorage | ||||
|                     || notebookStorage.notebookMeta.identifier.key !== this.internalDomainObject.identifier.key) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             const defaultNotebookPage = notebookStorage.page; | ||||
|             const page = pages.find(p => p.id === id); | ||||
|             if (!page && defaultNotebookPage.id === id) { | ||||
|                 this.defaultSectionId = null; | ||||
|                 this.defaultPageId = null | ||||
|                 this.removeDefaultClass(this.internalDomainObject); | ||||
|                 clearDefaultNotebook(); | ||||
|  | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             if (id !== defaultNotebookPage.id) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             setDefaultNotebookPage(page); | ||||
|         }, | ||||
|         updateDefaultNotebookSection(sections, id) { | ||||
|             if (!id) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             const notebookStorage = getDefaultNotebook(); | ||||
|             if (!notebookStorage | ||||
|                     || notebookStorage.notebookMeta.identifier.key !== this.internalDomainObject.identifier.key) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             const defaultNotebookSection = notebookStorage.section; | ||||
|             const section = sections.find(s => s.id === id); | ||||
|             if (!section && defaultNotebookSection.id === id) { | ||||
|                 this.defaultSectionId = null; | ||||
|                 this.defaultPageId = null | ||||
|                 this.removeDefaultClass(this.internalDomainObject); | ||||
|                 clearDefaultNotebook(); | ||||
|  | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             if (section.id !== defaultNotebookSection.id) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             setDefaultNotebookSection(section); | ||||
|         }, | ||||
|         updateEntries(entries) { | ||||
|             const configuration = this.internalDomainObject.configuration; | ||||
|             const notebookEntries = configuration.entries || {}; | ||||
|             notebookEntries[this.selectedSection.id][this.selectedPage.id] = entries; | ||||
|  | ||||
|             this.mutateObject('configuration.entries', notebookEntries); | ||||
|         }, | ||||
|         updateInternalDomainObject(domainObject) { | ||||
|             this.internalDomainObject = domainObject; | ||||
|         }, | ||||
|         updatePage({ pages = [], id = null}) { | ||||
|             const selectedSection = this.getSelectedSection(); | ||||
|             if (!selectedSection) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             selectedSection.pages = pages; | ||||
|             const sections = this.sections.map(section => { | ||||
|                 if (section.id === selectedSection.id) { | ||||
|                     section = selectedSection; | ||||
|                 } | ||||
|  | ||||
|                 return section; | ||||
|             }); | ||||
|  | ||||
|             this.updateSection({ sections }); | ||||
|             this.updateDefaultNotebookPage(pages, id); | ||||
|         }, | ||||
|         updateParams(sections) { | ||||
|             const selectedSection = sections.find(s => s.isSelected); | ||||
|             if (!selectedSection) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             const selectedPage = selectedSection.pages.find(p => p.isSelected); | ||||
|             if (!selectedPage) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             const sectionId = selectedSection.id; | ||||
|             const pageId = selectedPage.id; | ||||
|  | ||||
|             if (!sectionId || !pageId) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             this.openmct.router.updateParams({ | ||||
|                 sectionId, | ||||
|                 pageId | ||||
|             }); | ||||
|         }, | ||||
|         updateSection({ sections, id = null }) { | ||||
|             this.mutateObject('configuration.sections', sections); | ||||
|  | ||||
|             this.updateParams(sections); | ||||
|             this.updateDefaultNotebookSection(sections, id); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| </script> | ||||
							
								
								
									
										132
									
								
								src/plugins/notebook/components/page-collection.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								src/plugins/notebook/components/page-collection.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,132 @@ | ||||
| <template> | ||||
| <ul class="c-list"> | ||||
|     <li v-for="page in pages" | ||||
|         :key="page.id" | ||||
|         class="c-list__item-h" | ||||
|     > | ||||
|         <Page ref="pageComponent" | ||||
|               :default-page-id="defaultPageId" | ||||
|               :page="page" | ||||
|               :page-title="pageTitle" | ||||
|               @deletePage="deletePage" | ||||
|               @renamePage="updatePage" | ||||
|               @selectPage="selectPage" | ||||
|         /> | ||||
|     </li> | ||||
| </ul> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import { deleteNotebookEntries } from '../utils/notebook-entries'; | ||||
| import { getDefaultNotebook } from '../utils/notebook-storage'; | ||||
| import Page from './page-component.vue'; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct'], | ||||
|     components: { | ||||
|         Page | ||||
|     }, | ||||
|     props: { | ||||
|         defaultPageId: { | ||||
|             type: String, | ||||
|             default() { | ||||
|                 return ''; | ||||
|             } | ||||
|         }, | ||||
|         domainObject: { | ||||
|             type: Object, | ||||
|             default() { | ||||
|                 return {}; | ||||
|             } | ||||
|         }, | ||||
|         pages: { | ||||
|             type: Array, | ||||
|             required: true, | ||||
|             default() { | ||||
|                 return []; | ||||
|             } | ||||
|         }, | ||||
|         sections: { | ||||
|             type: Array, | ||||
|             required: true, | ||||
|             default() { | ||||
|                 return []; | ||||
|             } | ||||
|         }, | ||||
|         pageTitle: { | ||||
|             type: String, | ||||
|             default() { | ||||
|                 return ''; | ||||
|             } | ||||
|         }, | ||||
|         sidebarCoversEntries: { | ||||
|             type: Boolean, | ||||
|             default() { | ||||
|                 return false; | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|         } | ||||
|     }, | ||||
|     watch: { | ||||
|     }, | ||||
|     mounted() { | ||||
|     }, | ||||
|     destroyed() { | ||||
|     }, | ||||
|     methods: { | ||||
|         deletePage(id) { | ||||
|             const selectedSection = this.sections.find(s => s.isSelected); | ||||
|             const page = this.pages.filter(p => p.id !== id); | ||||
|             deleteNotebookEntries(this.openmct, this.domainObject, selectedSection, page); | ||||
|  | ||||
|             const selectedPage = this.pages.find(p => p.isSelected); | ||||
|             const defaultNotebook = getDefaultNotebook(); | ||||
|             const defaultpage = defaultNotebook && defaultNotebook.page; | ||||
|             const isPageSelected = selectedPage && selectedPage.id === id; | ||||
|             const isPageDefault = defaultpage && defaultpage.id === id; | ||||
|             const pages = this.pages.filter(s => s.id !== id); | ||||
|  | ||||
|             if (isPageSelected && defaultpage) { | ||||
|                 pages.forEach(s => { | ||||
|                     s.isSelected = false; | ||||
|                     if (defaultpage && defaultpage.id === s.id) { | ||||
|                         s.isSelected = true; | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             if (pages.length && isPageSelected && (!defaultpage || isPageDefault)) { | ||||
|                 pages[0].isSelected = true; | ||||
|             } | ||||
|  | ||||
|             this.$emit('updatePage', { pages, id }); | ||||
|         }, | ||||
|         selectPage(id) { | ||||
|             const pages = this.pages.map(page => { | ||||
|                 const isSelected = page.id === id; | ||||
|                 page.isSelected = isSelected; | ||||
|  | ||||
|                 return page; | ||||
|             }); | ||||
|  | ||||
|             this.$emit('updatePage', { pages, id }); | ||||
|  | ||||
|             // Add test here for whether or not to toggle the nav | ||||
|             if (this.sidebarCoversEntries) { | ||||
|                 this.$emit('toggleNav'); | ||||
|             } | ||||
|         }, | ||||
|         updatePage(newPage) { | ||||
|             const id = newPage.id; | ||||
|             const pages = this.pages.map(page => | ||||
|                 page.id === id | ||||
|                     ? newPage | ||||
|                     : page); | ||||
|             this.$emit('updatePage', { pages, id }); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| </script> | ||||
							
								
								
									
										127
									
								
								src/plugins/notebook/components/page-component.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								src/plugins/notebook/components/page-component.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,127 @@ | ||||
| <template> | ||||
| <div class="c-list__item js-list__item" | ||||
|      :class="[{ 'is-selected': page.isSelected, 'is-notebook-default' : (defaultPageId === page.id) }]" | ||||
|      :data-id="page.id" | ||||
|      @click="selectPage" | ||||
| > | ||||
|     <span class="c-list__item__name js-list__item__name" | ||||
|           :data-id="page.id" | ||||
|           @keydown.enter="updateName" | ||||
|           @blur="updateName" | ||||
|     >{{ page.name.length ? page.name : `Unnamed ${pageTitle}` }}</span> | ||||
|     <PopupMenu :popup-menu-items="popupMenuItems" /> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import PopupMenu from './popup-menu.vue'; | ||||
| import RemoveDialog from '../utils/removeDialog'; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct'], | ||||
|     components: { | ||||
|         PopupMenu | ||||
|     }, | ||||
|     props: { | ||||
|         defaultPageId: { | ||||
|             type: String, | ||||
|             default() { | ||||
|                 return ''; | ||||
|             } | ||||
|         }, | ||||
|         page: { | ||||
|             type: Object, | ||||
|             required: true | ||||
|         }, | ||||
|         pageTitle: { | ||||
|             type: String, | ||||
|             default() { | ||||
|                 return ''; | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             popupMenuItems: [], | ||||
|             removeActionString: `Delete ${this.pageTitle}` | ||||
|         } | ||||
|     }, | ||||
|     watch: { | ||||
|         page(newPage) { | ||||
|             this.toggleContentEditable(newPage); | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.addPopupMenuItems(); | ||||
|         this.toggleContentEditable(); | ||||
|     }, | ||||
|     destroyed() { | ||||
|     }, | ||||
|     methods: { | ||||
|         addPopupMenuItems() { | ||||
|             const removePage = { | ||||
|                 cssClass: 'icon-trash', | ||||
|                 name: this.removeActionString, | ||||
|                 callback: this.getRemoveDialog.bind(this) | ||||
|             } | ||||
|  | ||||
|             this.popupMenuItems = [removePage]; | ||||
|         }, | ||||
|         deletePage(success) { | ||||
|             if (!success) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             this.$emit('deletePage', this.page.id); | ||||
|         }, | ||||
|         getRemoveDialog() { | ||||
|             const message = 'This action will delete this page and all of its entries. Do you want to continue?'; | ||||
|             const options = { | ||||
|                 name: this.removeActionString, | ||||
|                 callback: this.deletePage.bind(this), | ||||
|                 message | ||||
|             } | ||||
|             const removeDialog = new RemoveDialog(this.openmct, options); | ||||
|             removeDialog.show(); | ||||
|         }, | ||||
|         selectPage(event) { | ||||
|             const target = event.target; | ||||
|             const page = target.closest('.js-list__item'); | ||||
|             const input = page.querySelector('.js-list__item__name'); | ||||
|  | ||||
|             if (page.className.indexOf('is-selected') > -1) { | ||||
|                 input.contentEditable = true; | ||||
|                 input.classList.add('c-input-inline'); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             const id = target.dataset.id; | ||||
|             if (!id) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             this.$emit('selectPage', id); | ||||
|         }, | ||||
|         toggleContentEditable(page = this.page) { | ||||
|             const pageTitle = this.$el.querySelector('span'); | ||||
|             pageTitle.contentEditable = page.isSelected; | ||||
|         }, | ||||
|         updateName(event) { | ||||
|             const target = event.target; | ||||
|             const name = target.textContent.toString(); | ||||
|             target.contentEditable = false; | ||||
|             target.classList.remove('c-input-inline'); | ||||
|  | ||||
|             if (this.page.name === name) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             if (name === '') { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             this.$emit('renamePage', Object.assign(this.page, { name })); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| </script> | ||||
							
								
								
									
										93
									
								
								src/plugins/notebook/components/popup-menu.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								src/plugins/notebook/components/popup-menu.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,93 @@ | ||||
| <template> | ||||
| <button | ||||
|     class="c-popup-menu-button c-disclosure-button" | ||||
|     title="popup menu" | ||||
|     @click="showMenuItems" | ||||
| > | ||||
| </button> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import MenuItems from './menu-items.vue'; | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct'], | ||||
|     props: { | ||||
|         domainObject: { | ||||
|             type: Object, | ||||
|             default() { | ||||
|                 return {}; | ||||
|             } | ||||
|         }, | ||||
|         popupMenuItems: { | ||||
|             type: Array, | ||||
|             default() { | ||||
|                 return []; | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             menuItems: null | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|     }, | ||||
|     methods: { | ||||
|         calculateMenuPosition(event, element) { | ||||
|             let eventPosX = event.clientX; | ||||
|             let eventPosY = event.clientY; | ||||
|  | ||||
|             let menuDimensions = element.getBoundingClientRect(); | ||||
|             let overflowX = (eventPosX + menuDimensions.width) - document.body.clientWidth; | ||||
|             let overflowY = (eventPosY + menuDimensions.height) - document.body.clientHeight; | ||||
|  | ||||
|             if (overflowX > 0) { | ||||
|                 eventPosX = eventPosX - overflowX; | ||||
|             } | ||||
|  | ||||
|             if (overflowY > 0) { | ||||
|                 eventPosY = eventPosY - overflowY; | ||||
|             } | ||||
|  | ||||
|             return { | ||||
|                 x: eventPosX, | ||||
|                 y: eventPosY | ||||
|             } | ||||
|         }, | ||||
|         hideMenuItems() { | ||||
|             document.body.removeChild(this.menuItems.$el); | ||||
|             this.menuItems.$destroy(); | ||||
|             this.menuItems = null; | ||||
|             document.removeEventListener('click', this.hideMenuItems); | ||||
|  | ||||
|             return; | ||||
|         }, | ||||
|         showMenuItems($event) { | ||||
|             const menuItems = new Vue({ | ||||
|                 components: { | ||||
|                     MenuItems | ||||
|                 }, | ||||
|                 provide: { | ||||
|                     popupMenuItems: this.popupMenuItems | ||||
|                 }, | ||||
|                 template: '<MenuItems />' | ||||
|             }); | ||||
|             this.menuItems = menuItems; | ||||
|  | ||||
|             menuItems.$mount(); | ||||
|             const element = this.menuItems.$el; | ||||
|             document.body.appendChild(element); | ||||
|  | ||||
|             const position = this.calculateMenuPosition($event, element); | ||||
|             element.style.left = `${position.x}px`; | ||||
|             element.style.top = `${position.y}px`; | ||||
|  | ||||
|             setTimeout(() => { | ||||
|                 document.addEventListener('click', this.hideMenuItems); | ||||
|             }, 0); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| </script> | ||||
							
								
								
									
										50
									
								
								src/plugins/notebook/components/search-results.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								src/plugins/notebook/components/search-results.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | ||||
| <template> | ||||
| <div class="c-notebook__search-results"> | ||||
|     <div class="c-notebook__search-results__header">Search Results</div> | ||||
|     <div class="c-notebook__entries"> | ||||
|         <NotebookEntry v-for="(result, index) in results" | ||||
|                        :key="index" | ||||
|                        :result="result" | ||||
|                        :entry="result.entry" | ||||
|                        :read-only="true" | ||||
|                        :selected-page="null" | ||||
|                        :selected-section="null" | ||||
|                        @changeSectionPage="changeSectionPage" | ||||
|         /> | ||||
|     </div> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import NotebookEntry from './notebook-entry.vue'; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct', 'domainObject'], | ||||
|     components: { | ||||
|         NotebookEntry | ||||
|     }, | ||||
|     props:{ | ||||
|         results: { | ||||
|             type: Array, | ||||
|             default() { | ||||
|                 return []; | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         return {} | ||||
|     }, | ||||
|     watch: { | ||||
|         results(newResults) {} | ||||
|     }, | ||||
|     destroyed() { | ||||
|     }, | ||||
|     mounted() { | ||||
|     }, | ||||
|     methods: { | ||||
|         changeSectionPage(data) { | ||||
|             this.$emit('changeSectionPage', data); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| </script> | ||||
							
								
								
									
										113
									
								
								src/plugins/notebook/components/section-collection.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								src/plugins/notebook/components/section-collection.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,113 @@ | ||||
| <template> | ||||
| <ul class="c-list"> | ||||
|     <li v-for="section in sections" | ||||
|         :key="section.id" | ||||
|         class="c-list__item-h" | ||||
|     > | ||||
|         <sectionComponent ref="sectionComponent" | ||||
|                           :default-section-id="defaultSectionId" | ||||
|                           :section="section" | ||||
|                           :section-title="sectionTitle" | ||||
|                           @deleteSection="deleteSection" | ||||
|                           @renameSection="updateSection" | ||||
|                           @selectSection="selectSection" | ||||
|         /> | ||||
|     </li> | ||||
| </ul> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import { deleteNotebookEntries } from '../utils/notebook-entries'; | ||||
| import { getDefaultNotebook } from '../utils/notebook-storage'; | ||||
| import sectionComponent from './section-component.vue'; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct'], | ||||
|     components: { | ||||
|         sectionComponent | ||||
|     }, | ||||
|     props: { | ||||
|         defaultSectionId: { | ||||
|             type: String, | ||||
|             default() { | ||||
|                 return ''; | ||||
|             } | ||||
|         }, | ||||
|         domainObject: { | ||||
|             type: Object, | ||||
|             default() { | ||||
|                 return {}; | ||||
|             } | ||||
|         }, | ||||
|         sections: { | ||||
|             type: Array, | ||||
|             required: true, | ||||
|             default() { | ||||
|                 return []; | ||||
|             } | ||||
|         }, | ||||
|         sectionTitle: { | ||||
|             type: String, | ||||
|             default() { | ||||
|                 return ''; | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|         } | ||||
|     }, | ||||
|     watch: { | ||||
|     }, | ||||
|     mounted() { | ||||
|     }, | ||||
|     destroyed() { | ||||
|     }, | ||||
|     methods: { | ||||
|         deleteSection(id) { | ||||
|             const section = this.sections.find(s => s.id === id); | ||||
|             deleteNotebookEntries(this.openmct, this.domainObject, section); | ||||
|  | ||||
|             const selectedSection = this.sections.find(s => s.isSelected); | ||||
|             const defaultNotebook = getDefaultNotebook(); | ||||
|             const defaultSection = defaultNotebook && defaultNotebook.section; | ||||
|             const isSectionSelected = selectedSection && selectedSection.id === id; | ||||
|             const isSectionDefault = defaultSection && defaultSection.id === id; | ||||
|             const sections = this.sections.filter(s => s.id !== id); | ||||
|  | ||||
|             if (isSectionSelected && defaultSection) { | ||||
|                 sections.forEach(s => { | ||||
|                     s.isSelected = false; | ||||
|                     if (defaultSection && defaultSection.id === s.id) { | ||||
|                         s.isSelected = true; | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             if (sections.length && isSectionSelected && (!defaultSection || isSectionDefault)) { | ||||
|                 sections[0].isSelected = true; | ||||
|             } | ||||
|  | ||||
|             this.$emit('updateSection', { sections, id }); | ||||
|         }, | ||||
|         selectSection(id, newSections) { | ||||
|             const currentSections = newSections || this.sections; | ||||
|             const sections = currentSections.map(section => { | ||||
|                 const isSelected = section.id === id; | ||||
|                 section.isSelected = isSelected; | ||||
|  | ||||
|                 return section; | ||||
|             }); | ||||
|             this.$emit('updateSection', { sections, id }); | ||||
|         }, | ||||
|         updateSection(newSection) { | ||||
|             const id = newSection.id; | ||||
|             const sections = this.sections.map(section => | ||||
|                 section.id === id | ||||
|                     ? newSection | ||||
|                     : section); | ||||
|             this.$emit('updateSection', { sections, id }); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| </script> | ||||
							
								
								
									
										132
									
								
								src/plugins/notebook/components/section-component.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								src/plugins/notebook/components/section-component.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,132 @@ | ||||
| <template> | ||||
| <div class="c-list__item js-list__item" | ||||
|      :class="[{ 'is-selected': section.isSelected, 'is-notebook-default' : (defaultSectionId === section.id) }]" | ||||
|      :data-id="section.id" | ||||
|      @click="selectSection" | ||||
| > | ||||
|     <span class="c-list__item__name js-list__item__name" | ||||
|           :data-id="section.id" | ||||
|           @keydown.enter="updateName" | ||||
|           @blur="updateName" | ||||
|     >{{ section.name.length ? section.name : `Unnamed ${sectionTitle}` }}</span> | ||||
|     <PopupMenu :popup-menu-items="popupMenuItems" /> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <style lang="scss"> | ||||
| </style> | ||||
|  | ||||
| <script> | ||||
| import PopupMenu from './popup-menu.vue'; | ||||
| import RemoveDialog from '../utils/removeDialog'; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct'], | ||||
|     components: { | ||||
|         PopupMenu | ||||
|     }, | ||||
|     props: { | ||||
|         defaultSectionId: { | ||||
|             type: String, | ||||
|             default() { | ||||
|                 return ''; | ||||
|             } | ||||
|         }, | ||||
|         section: { | ||||
|             type: Object, | ||||
|             required: true | ||||
|         }, | ||||
|         sectionTitle: { | ||||
|             type: String, | ||||
|             default() { | ||||
|                 return ''; | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             popupMenuItems: [], | ||||
|             removeActionString: `Delete ${this.sectionTitle}` | ||||
|         } | ||||
|     }, | ||||
|     watch: { | ||||
|         section(newSection) { | ||||
|             this.toggleContentEditable(newSection); | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.addPopupMenuItems(); | ||||
|         this.toggleContentEditable(); | ||||
|     }, | ||||
|     destroyed() { | ||||
|     }, | ||||
|     methods: { | ||||
|         addPopupMenuItems() { | ||||
|             const removeSection = { | ||||
|                 cssClass: 'icon-trash', | ||||
|                 name: this.removeActionString, | ||||
|                 callback: this.getRemoveDialog.bind(this) | ||||
|             } | ||||
|  | ||||
|             this.popupMenuItems = [removeSection]; | ||||
|         }, | ||||
|         deleteSection(success) { | ||||
|             if (!success) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             this.$emit('deleteSection', this.section.id); | ||||
|         }, | ||||
|         getRemoveDialog() { | ||||
|             const message = 'This action will delete this section and all of its pages and entries. Do you want to continue?'; | ||||
|             const options = { | ||||
|                 name: this.removeActionString, | ||||
|                 callback: this.deleteSection.bind(this), | ||||
|                 message | ||||
|             } | ||||
|  | ||||
|             const removeDialog = new RemoveDialog(this.openmct, options); | ||||
|             removeDialog.show(); | ||||
|         }, | ||||
|         selectSection(event) { | ||||
|             const target = event.target; | ||||
|             const section = target.closest('.js-list__item'); | ||||
|             const input = section.querySelector('.js-list__item__name'); | ||||
|  | ||||
|             if (section.className.indexOf('is-selected') > -1) { | ||||
|                 input.contentEditable = true; | ||||
|                 input.classList.add('c-input-inline'); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             const id = target.dataset.id; | ||||
|  | ||||
|             if (!id) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             this.$emit('selectSection', id); | ||||
|         }, | ||||
|         toggleContentEditable(section = this.section) { | ||||
|             const sectionTitle = this.$el.querySelector('span'); | ||||
|             sectionTitle.contentEditable = section.isSelected; | ||||
|         }, | ||||
|         updateName(event) { | ||||
|             const target = event.target; | ||||
|             target.contentEditable = false; | ||||
|             target.classList.remove('c-input-inline'); | ||||
|             const name = target.textContent.trim(); | ||||
|  | ||||
|             if (this.section.name === name) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             if (name === '') { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             this.$emit('renameSection', Object.assign(this.section, { name })); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| </script> | ||||
							
								
								
									
										119
									
								
								src/plugins/notebook/components/sidebar.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								src/plugins/notebook/components/sidebar.scss
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,119 @@ | ||||
| .c-sidebar { | ||||
|     @include userSelectNone(); | ||||
|     background: $sideBarBg; | ||||
|     display: flex; | ||||
|     justify-content: stretch; | ||||
|     max-width: 300px; | ||||
|  | ||||
|     &.c-drawer--push.is-expanded { | ||||
|         margin-right: $interiorMargin; | ||||
|         width: 50%; | ||||
|     } | ||||
|  | ||||
|     &.c-drawer--overlays.is-expanded { | ||||
|         width: 95%; | ||||
|     } | ||||
|  | ||||
|     > * { | ||||
|         // Hardcoded for two columns | ||||
|         background: $sideBarBg; | ||||
|         display: flex; | ||||
|         flex: 1 1 50%; | ||||
|         flex-direction: column; | ||||
|  | ||||
|         + * { | ||||
|             margin-left: $interiorMarginSm; | ||||
|         } | ||||
|  | ||||
|         > * + * { | ||||
|             // Add margin-top to first and second level children | ||||
|             margin-top: $interiorMargin; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     &__pane { | ||||
|         > * + * { margin-top: $interiorMargin; } | ||||
|     } | ||||
|  | ||||
|     &__header-w { | ||||
|         // Wraps header, used for page pane with collapse button | ||||
|         display: flex; | ||||
|         flex: 0 0 auto; | ||||
|         background: $sideBarHeaderBg; | ||||
|         align-items: center; | ||||
|  | ||||
|         .c-icon-button { | ||||
|             font-size: 0.8em; | ||||
|             color: $colorBodyFg; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     &__header { | ||||
|         color: $sideBarHeaderFg; | ||||
|         display: flex; | ||||
|         flex: 1 1 auto; | ||||
|         padding: $interiorMargin; | ||||
|         text-transform: uppercase; | ||||
|  | ||||
|         > * { | ||||
|             @include ellipsize(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     &__contents-and-controls { | ||||
|         // Encloses pane buttons and contents elements | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|         flex: 1 1 auto; | ||||
|  | ||||
|         > * { | ||||
|             margin: auto $interiorMargin $interiorMargin $interiorMargin; | ||||
|  | ||||
|             &:first-child { | ||||
|                 border-bottom: 1px solid $colorInteriorBorder; | ||||
|                 flex: 0 0 auto; | ||||
|             } | ||||
|  | ||||
|             + * { | ||||
|                 margin-top: $interiorMargin; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     &__contents { | ||||
|         flex: 1 1 auto; | ||||
|         overflow-x: hidden; | ||||
|         overflow-y: auto; | ||||
|         padding: auto $interiorMargin; | ||||
|     } | ||||
|  | ||||
|     .c-list-button { | ||||
|         .c-button { | ||||
|             font-size: 0.8em; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     .c-list__item { | ||||
|         @include hover() { | ||||
|             [class*="__menu-indicator"] { | ||||
|                 opacity: 0.7; | ||||
|                 transition: $transIn; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         > * + * { | ||||
|             margin-left: $interiorMargin; | ||||
|         } | ||||
|  | ||||
|         &__name { | ||||
|             flex: 0 1 auto; | ||||
|         } | ||||
|  | ||||
|         &__menu-indicator { | ||||
|             flex: 0 0 auto; | ||||
|             font-size: 0.8em; | ||||
|             opacity: 0; | ||||
|             transition: $transOut; | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										189
									
								
								src/plugins/notebook/components/sidebar.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										189
									
								
								src/plugins/notebook/components/sidebar.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,189 @@ | ||||
| <template> | ||||
| <div class="c-sidebar c-drawer c-drawer--align-left"> | ||||
|     <div class="c-sidebar__pane"> | ||||
|         <div class="c-sidebar__header-w"> | ||||
|             <div class="c-sidebar__header"> | ||||
|                 <span class="c-sidebar__header-label">{{ sectionTitle }}</span> | ||||
|             </div> | ||||
|         </div> | ||||
|         <div class="c-sidebar__contents-and-controls"> | ||||
|             <button class="c-list-button" | ||||
|                     @click="addSection" | ||||
|             > | ||||
|                 <span class="c-button c-list-button__button icon-plus"></span> | ||||
|                 <span class="c-list-button__label">Add {{ sectionTitle }}</span> | ||||
|             </button> | ||||
|             <SectionCollection class="c-sidebar__contents" | ||||
|                                :default-section-id="defaultSectionId" | ||||
|                                :domain-object="domainObject" | ||||
|                                :sections="sections" | ||||
|                                :section-title="sectionTitle" | ||||
|                                @updateSection="updateSection" | ||||
|             /> | ||||
|         </div> | ||||
|     </div> | ||||
|     <div class="c-sidebar__pane"> | ||||
|         <div class="c-sidebar__header-w"> | ||||
|             <div class="c-sidebar__header"> | ||||
|                 <span class="c-sidebar__header-label">{{ pageTitle }}</span> | ||||
|             </div> | ||||
|             <button class="c-click-icon c-click-icon--major icon-x-in-circle" | ||||
|                     @click="toggleNav" | ||||
|             ></button> | ||||
|         </div> | ||||
|  | ||||
|         <div class="c-sidebar__contents-and-controls"> | ||||
|             <button class="c-list-button" | ||||
|                     @click="addPage" | ||||
|             > | ||||
|                 <span class="c-button c-list-button__button icon-plus"></span> | ||||
|                 <span class="c-list-button__label">Add {{ pageTitle }}</span> | ||||
|             </button> | ||||
|             <PageCollection ref="pageCollection" | ||||
|                             class="c-sidebar__contents" | ||||
|                             :default-page-id="defaultPageId" | ||||
|                             :domain-object="domainObject" | ||||
|                             :pages="pages" | ||||
|                             :sections="sections" | ||||
|                             :sidebar-covers-entries="sidebarCoversEntries" | ||||
|                             :page-title="pageTitle" | ||||
|                             @toggleNav="toggleNav" | ||||
|                             @updatePage="updatePage" | ||||
|             /> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import SectionCollection from './section-collection.vue'; | ||||
| import PageCollection from './page-collection.vue'; | ||||
| import uuid from 'uuid'; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct'], | ||||
|     components: { | ||||
|         SectionCollection, | ||||
|         PageCollection | ||||
|     }, | ||||
|     props: { | ||||
|         defaultPageId: { | ||||
|             type: String, | ||||
|             default() { | ||||
|                 return ''; | ||||
|             } | ||||
|         }, | ||||
|         defaultSectionId: { | ||||
|             type: String, | ||||
|             default() { | ||||
|                 return ''; | ||||
|             } | ||||
|         }, | ||||
|         domainObject: { | ||||
|             type: Object, | ||||
|             default() { | ||||
|                 return {}; | ||||
|             } | ||||
|         }, | ||||
|         pages: { | ||||
|             type: Array, | ||||
|             required: true, | ||||
|             default() { | ||||
|                 return []; | ||||
|             } | ||||
|         }, | ||||
|         pageTitle: { | ||||
|             type: String, | ||||
|             default() { | ||||
|                 return ''; | ||||
|             } | ||||
|         }, | ||||
|         sections: { | ||||
|             type: Array, | ||||
|             required: true, | ||||
|             default() { | ||||
|                 return []; | ||||
|             } | ||||
|         }, | ||||
|         sectionTitle: { | ||||
|             type: String, | ||||
|             default() { | ||||
|                 return ''; | ||||
|             } | ||||
|         }, | ||||
|         sidebarCoversEntries: { | ||||
|             type: Boolean, | ||||
|             default() { | ||||
|                 return false; | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|         } | ||||
|     }, | ||||
|     watch: { | ||||
|         pages(newpages) { | ||||
|             if (!newpages.length) { | ||||
|                 this.addPage(); | ||||
|             } | ||||
|         }, | ||||
|         sections(newSections) { | ||||
|             if (!newSections.length) { | ||||
|                 this.addSection(); | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         if (!this.sections.length) { | ||||
|             this.addSection(); | ||||
|         } | ||||
|     }, | ||||
|     destroyed() { | ||||
|     }, | ||||
|     methods: { | ||||
|         addPage() { | ||||
|             const pageTitle = this.pageTitle; | ||||
|             const id = uuid(); | ||||
|             const page = { | ||||
|                 id, | ||||
|                 isDefault : false, | ||||
|                 isSelected: true, | ||||
|                 name : `Unnamed ${pageTitle}`, | ||||
|                 pageTitle | ||||
|             }; | ||||
|  | ||||
|             this.pages.forEach(p => p.isSelected = false); | ||||
|             const pages = this.pages.concat(page); | ||||
|  | ||||
|             this.updatePage({ pages, id }); | ||||
|         }, | ||||
|         addSection() { | ||||
|             const sectionTitle = this.sectionTitle; | ||||
|             const id = uuid(); | ||||
|             const section = { | ||||
|                 id, | ||||
|                 isDefault : false, | ||||
|                 isSelected: true, | ||||
|                 name : `Unnamed ${sectionTitle}`, | ||||
|                 pages : [], | ||||
|                 sectionTitle | ||||
|             }; | ||||
|  | ||||
|             this.sections.forEach(s => s.isSelected = false); | ||||
|             const sections = this.sections.concat(section); | ||||
|  | ||||
|             this.updateSection({ sections, id }); | ||||
|         }, | ||||
|         toggleNav() { | ||||
|             this.$emit('toggleNav'); | ||||
|         }, | ||||
|         updatePage({ pages, id }) { | ||||
|             this.$emit('updatePage', { pages, id }); | ||||
|         }, | ||||
|         updateSection({ sections, id }) { | ||||
|             this.$emit('updateSection', { sections, id }); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| </script> | ||||
| @@ -3,10 +3,10 @@ | ||||
|     <div class="c-notebook-snapshot__header l-browse-bar"> | ||||
|         <div class="l-browse-bar__start"> | ||||
|             <div class="l-browse-bar__object-name--w"> | ||||
|                 <span class="l-browse-bar__object-name" | ||||
|                 <span class="c-object-label l-browse-bar__object-name" | ||||
|                     v-bind:class="embed.cssClass" | ||||
|                 > | ||||
|                     {{embed.name}} | ||||
|                     <span class="c-object-label__name">{{ embed.name }}</span> | ||||
|                 </span> | ||||
|             </div> | ||||
|         </div> | ||||
| @@ -21,9 +21,8 @@ | ||||
|         </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <div class="c-notebook-snapshot__image"> | ||||
|         <div class="image-main s-image-main" | ||||
|             :style="{ backgroundImage: 'url(' + embed.snapshot.src + ')' }" | ||||
|         ></div> | ||||
|     <div class="c-notebook-snapshot__image" | ||||
|          :style="{ backgroundImage: 'url(' + embed.snapshot.src + ')' }" | ||||
|     > | ||||
|     </div> | ||||
| </div> | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user