Compare commits
39 Commits
search-api
...
version-1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a4ff80f96 | ||
|
|
77b720d00d | ||
|
|
ba982671b2 | ||
|
|
5df7d92d64 | ||
|
|
a8228406de | ||
|
|
2401473012 | ||
|
|
e502fb88fa | ||
|
|
37a52cb011 | ||
|
|
04fb4e8a82 | ||
|
|
5646a252f7 | ||
|
|
0e6ce7f58b | ||
|
|
8cd6a4c6a3 | ||
|
|
02fc162197 | ||
|
|
84d21a3695 | ||
|
|
1a6369c2b9 | ||
|
|
463c44679d | ||
|
|
c1f3ea4e61 | ||
|
|
142b767470 | ||
|
|
184b716b53 | ||
|
|
e53399495b | ||
|
|
d27f73579b | ||
|
|
1ae8199e89 | ||
|
|
2deb4e8474 | ||
|
|
7f10681424 | ||
|
|
c756adad6f | ||
|
|
f3d593bc1e | ||
|
|
b637307de6 | ||
|
|
b6e0208e71 | ||
|
|
631876cab3 | ||
|
|
a192d46c2b | ||
|
|
6923f17645 | ||
|
|
87a45de05b | ||
|
|
ab76451360 | ||
|
|
a91179091f | ||
|
|
5f7e34ce6c | ||
|
|
db33f0538a | ||
|
|
257a8e2e2d | ||
|
|
baa8078d23 | ||
|
|
ee60013f45 |
@@ -76,6 +76,7 @@ define([
|
||||
|
||||
workerRequest[prop] = Number(workerRequest[prop]);
|
||||
});
|
||||
|
||||
workerRequest.name = domainObject.name;
|
||||
|
||||
return workerRequest;
|
||||
|
||||
@@ -108,7 +108,6 @@
|
||||
|
||||
for (; nextStep < end && data.length < 5000; nextStep += step) {
|
||||
data.push({
|
||||
name: request.name,
|
||||
utc: nextStep,
|
||||
yesterday: nextStep - 60 * 60 * 24 * 1000,
|
||||
sin: sin(nextStep, period, amplitude, offset, phase, randomness),
|
||||
|
||||
@@ -27,7 +27,7 @@ define([
|
||||
) {
|
||||
function ImageryPlugin() {
|
||||
|
||||
var IMAGE_SAMPLES = [
|
||||
const IMAGE_SAMPLES = [
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18731.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18732.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18733.jpg",
|
||||
@@ -47,13 +47,14 @@ define([
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18747.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18748.jpg"
|
||||
];
|
||||
const IMAGE_DELAY = 20000;
|
||||
|
||||
function pointForTimestamp(timestamp, name) {
|
||||
return {
|
||||
name: name,
|
||||
utc: Math.floor(timestamp / 5000) * 5000,
|
||||
local: Math.floor(timestamp / 5000) * 5000,
|
||||
url: IMAGE_SAMPLES[Math.floor(timestamp / 5000) % IMAGE_SAMPLES.length]
|
||||
utc: Math.floor(timestamp / IMAGE_DELAY) * IMAGE_DELAY,
|
||||
local: Math.floor(timestamp / IMAGE_DELAY) * IMAGE_DELAY,
|
||||
url: IMAGE_SAMPLES[Math.floor(timestamp / IMAGE_DELAY) % IMAGE_SAMPLES.length]
|
||||
};
|
||||
}
|
||||
|
||||
@@ -64,7 +65,7 @@ define([
|
||||
subscribe: function (domainObject, callback) {
|
||||
var interval = setInterval(function () {
|
||||
callback(pointForTimestamp(Date.now(), domainObject.name));
|
||||
}, 5000);
|
||||
}, IMAGE_DELAY);
|
||||
|
||||
return function () {
|
||||
clearInterval(interval);
|
||||
@@ -81,9 +82,9 @@ define([
|
||||
var start = options.start;
|
||||
var end = Math.min(options.end, Date.now());
|
||||
var data = [];
|
||||
while (start <= end && data.length < 5000) {
|
||||
while (start <= end && data.length < IMAGE_DELAY) {
|
||||
data.push(pointForTimestamp(start, domainObject.name));
|
||||
start += 5000;
|
||||
start += IMAGE_DELAY;
|
||||
}
|
||||
|
||||
return Promise.resolve(data);
|
||||
|
||||
88
index.html
88
index.html
@@ -30,12 +30,50 @@
|
||||
<link rel="icon" type="image/png" href="dist/favicons/favicon-96x96.png" sizes="96x96" type="image/x-icon">
|
||||
<link rel="icon" type="image/png" href="dist/favicons/favicon-32x32.png" sizes="32x32" type="image/x-icon">
|
||||
<link rel="icon" type="image/png" href="dist/favicons/favicon-16x16.png" sizes="16x16" type="image/x-icon">
|
||||
<style type="text/css">
|
||||
@keyframes splash-spinner {
|
||||
0% {
|
||||
transform: translate(-50%, -50%) rotate(0deg); }
|
||||
100% {
|
||||
transform: translate(-50%, -50%) rotate(360deg); } }
|
||||
|
||||
#splash-screen {
|
||||
background-color: black;
|
||||
position: absolute;
|
||||
top: 0; right: 0; bottom: 0; left: 0;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
#splash-screen:before {
|
||||
animation-name: splash-spinner;
|
||||
animation-duration: 0.5s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-timing-function: linear;
|
||||
border-radius: 50%;
|
||||
border-color: rgba(255,255,255,0.25);
|
||||
border-top-color: white;
|
||||
border-style: solid;
|
||||
border-width: 10px;
|
||||
content: '';
|
||||
display: block;
|
||||
opacity: 0.25;
|
||||
position: absolute;
|
||||
left: 50%; top: 50%;
|
||||
height: 100px; width: 100px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
<script>
|
||||
const THIRTY_SECONDS = 30 * 1000;
|
||||
const THIRTY_MINUTES = THIRTY_SECONDS * 60;
|
||||
const ONE_MINUTE = THIRTY_SECONDS * 2;
|
||||
const FIVE_MINUTES = ONE_MINUTE * 5;
|
||||
const FIFTEEN_MINUTES = FIVE_MINUTES * 3;
|
||||
const THIRTY_MINUTES = FIFTEEN_MINUTES * 2;
|
||||
const ONE_HOUR = THIRTY_MINUTES * 2;
|
||||
const TWO_HOURS = ONE_HOUR * 2;
|
||||
const ONE_DAY = ONE_HOUR * 24;
|
||||
|
||||
[
|
||||
'example/eventGenerator'
|
||||
@@ -48,6 +86,7 @@
|
||||
openmct.install(openmct.plugins.MyItems());
|
||||
openmct.install(openmct.plugins.Generator());
|
||||
openmct.install(openmct.plugins.ExampleImagery());
|
||||
openmct.install(openmct.plugins.Timeline());
|
||||
openmct.install(openmct.plugins.UTCTimeSystem());
|
||||
openmct.install(openmct.plugins.AutoflowView({
|
||||
type: "telemetry.panel"
|
||||
@@ -72,21 +111,21 @@
|
||||
{
|
||||
label: 'Last Day',
|
||||
bounds: {
|
||||
start: () => Date.now() - 1000 * 60 * 60 * 24,
|
||||
start: () => Date.now() - ONE_DAY,
|
||||
end: () => Date.now()
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Last 2 hours',
|
||||
bounds: {
|
||||
start: () => Date.now() - 1000 * 60 * 60 * 2,
|
||||
start: () => Date.now() - TWO_HOURS,
|
||||
end: () => Date.now()
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Last hour',
|
||||
bounds: {
|
||||
start: () => Date.now() - 1000 * 60 * 60,
|
||||
start: () => Date.now() - ONE_HOUR,
|
||||
end: () => Date.now()
|
||||
}
|
||||
}
|
||||
@@ -95,7 +134,7 @@
|
||||
records: 10,
|
||||
// maximum duration between start and end bounds
|
||||
// for utc-based time systems this is in milliseconds
|
||||
limit: 1000 * 60 * 60 * 24
|
||||
limit: ONE_DAY
|
||||
},
|
||||
{
|
||||
name: "Realtime",
|
||||
@@ -104,7 +143,44 @@
|
||||
clockOffsets: {
|
||||
start: - THIRTY_MINUTES,
|
||||
end: THIRTY_SECONDS
|
||||
}
|
||||
},
|
||||
presets: [
|
||||
{
|
||||
label: '1 Hour',
|
||||
bounds: {
|
||||
start: - ONE_HOUR,
|
||||
end: THIRTY_SECONDS
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '30 Minutes',
|
||||
bounds: {
|
||||
start: - THIRTY_MINUTES,
|
||||
end: THIRTY_SECONDS
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '15 Minutes',
|
||||
bounds: {
|
||||
start: - FIFTEEN_MINUTES,
|
||||
end: THIRTY_SECONDS
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '5 Minutes',
|
||||
bounds: {
|
||||
start: - FIVE_MINUTES,
|
||||
end: THIRTY_SECONDS
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '1 Minute',
|
||||
bounds: {
|
||||
start: - ONE_MINUTE,
|
||||
end: THIRTY_SECONDS
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}));
|
||||
|
||||
@@ -86,7 +86,7 @@ module.exports = (config) => {
|
||||
reports: ['html', 'lcovonly', 'text-summary'],
|
||||
thresholds: {
|
||||
global: {
|
||||
lines: 64
|
||||
lines: 65
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openmct",
|
||||
"version": "1.3.0-SNAPSHOT",
|
||||
"version": "1.4.0",
|
||||
"description": "The Open MCT core platform",
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -114,7 +114,12 @@ define(["objectUtils"],
|
||||
var self = this,
|
||||
domainObject = this.domainObject;
|
||||
|
||||
let newStyleObject = objectUtils.toNewFormat(domainObject.getModel(), domainObject.getId());
|
||||
const identifier = {
|
||||
namespace: this.getSpace(),
|
||||
key: this.getKey()
|
||||
};
|
||||
|
||||
let newStyleObject = objectUtils.toNewFormat(domainObject.getModel(), identifier);
|
||||
|
||||
return this.openmct.objects
|
||||
.save(newStyleObject)
|
||||
@@ -146,6 +151,7 @@ define(["objectUtils"],
|
||||
return domainObject.useCapability("mutation", function () {
|
||||
return model;
|
||||
}, modified);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -99,8 +99,8 @@ define(
|
||||
|
||||
mockNewStyleDomainObject = Object.assign({}, model);
|
||||
mockNewStyleDomainObject.identifier = {
|
||||
namespace: "",
|
||||
key: id
|
||||
namespace: SPACE,
|
||||
key: key
|
||||
};
|
||||
|
||||
// Simulate mutation capability
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
this source code distribution or the Licensing information page available
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
<div class="c-clock l-time-display" ng-controller="ClockController as clock">
|
||||
<div class="c-clock l-time-display u-style-receiver js-style-receiver" ng-controller="ClockController as clock">
|
||||
<div class="c-clock__timezone">
|
||||
{{clock.zone()}}
|
||||
</div>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
this source code distribution or the Licensing information page available
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
<div class="c-timer is-{{timer.timerState}}" ng-controller="TimerController as timer">
|
||||
<div class="c-timer u-style-receiver js-style-receiver is-{{timer.timerState}}" ng-controller="TimerController as timer">
|
||||
<div class="c-timer__controls">
|
||||
<button ng-click="timer.clickStopButton()"
|
||||
ng-hide="timer.timerState == 'stopped'"
|
||||
|
||||
@@ -30,7 +30,6 @@ define([
|
||||
"./src/controllers/CompositeController",
|
||||
"./src/controllers/ColorController",
|
||||
"./src/controllers/DialogButtonController",
|
||||
"./src/controllers/SnapshotPreviewController",
|
||||
"./res/templates/controls/autocomplete.html",
|
||||
"./res/templates/controls/checkbox.html",
|
||||
"./res/templates/controls/datetime.html",
|
||||
@@ -44,8 +43,7 @@ define([
|
||||
"./res/templates/controls/menu-button.html",
|
||||
"./res/templates/controls/dialog.html",
|
||||
"./res/templates/controls/radio.html",
|
||||
"./res/templates/controls/file-input.html",
|
||||
"./res/templates/controls/snap-view.html"
|
||||
"./res/templates/controls/file-input.html"
|
||||
], function (
|
||||
MCTForm,
|
||||
MCTControl,
|
||||
@@ -56,7 +54,6 @@ define([
|
||||
CompositeController,
|
||||
ColorController,
|
||||
DialogButtonController,
|
||||
SnapshotPreviewController,
|
||||
autocompleteTemplate,
|
||||
checkboxTemplate,
|
||||
datetimeTemplate,
|
||||
@@ -70,8 +67,7 @@ define([
|
||||
menuButtonTemplate,
|
||||
dialogTemplate,
|
||||
radioTemplate,
|
||||
fileInputTemplate,
|
||||
snapViewTemplate
|
||||
fileInputTemplate
|
||||
) {
|
||||
|
||||
return {
|
||||
@@ -157,10 +153,6 @@ define([
|
||||
{
|
||||
"key": "file-input",
|
||||
"template": fileInputTemplate
|
||||
},
|
||||
{
|
||||
"key": "snap-view",
|
||||
"template": snapViewTemplate
|
||||
}
|
||||
],
|
||||
"controllers": [
|
||||
@@ -194,14 +186,6 @@ define([
|
||||
"$scope",
|
||||
"dialogService"
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "SnapshotPreviewController",
|
||||
"implementation": SnapshotPreviewController,
|
||||
"depends": [
|
||||
"$scope",
|
||||
"openmct"
|
||||
]
|
||||
}
|
||||
],
|
||||
"components": [
|
||||
|
||||
@@ -1,36 +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.
|
||||
-->
|
||||
<span ng-controller="SnapshotPreviewController"
|
||||
class='form-control shell'>
|
||||
<span class='field control {{structure.cssClass}}'>
|
||||
<image
|
||||
class="c-ne__embed__snap-thumb"
|
||||
src="{{imageUrl || structure.src}}"
|
||||
ng-click="previewImage(imageUrl || structure.src)"
|
||||
name="mctControl">
|
||||
</image>
|
||||
<br>
|
||||
<a title="Annotate" class="s-button icon-pencil" ng-click="annotateImage(ngModel, field, imageUrl || structure.src)">
|
||||
<span class="title-label">Annotate</span>
|
||||
</a>
|
||||
</span>
|
||||
</span>
|
||||
@@ -29,7 +29,6 @@ define(["zepto"], function ($) {
|
||||
* @memberof platform/forms
|
||||
*/
|
||||
function FileInputService() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -38,7 +37,7 @@ define(["zepto"], function ($) {
|
||||
*
|
||||
* @returns {Promise} promise for an object containing file meta-data
|
||||
*/
|
||||
FileInputService.prototype.getInput = function () {
|
||||
FileInputService.prototype.getInput = function (fileType) {
|
||||
var input = this.newInput();
|
||||
var read = this.readFile;
|
||||
var fileInfo = {};
|
||||
@@ -51,6 +50,10 @@ define(["zepto"], function ($) {
|
||||
file = this.files[0];
|
||||
input.remove();
|
||||
if (file) {
|
||||
if (fileType && (!file.type || (file.type !== fileType))) {
|
||||
reject("Incompatible file type");
|
||||
}
|
||||
|
||||
read(file)
|
||||
.then(function (contents) {
|
||||
fileInfo.name = file.name;
|
||||
|
||||
@@ -40,7 +40,7 @@ define(
|
||||
}
|
||||
|
||||
function handleClick() {
|
||||
fileInputService.getInput().then(function (result) {
|
||||
fileInputService.getInput(scope.structure.type).then(function (result) {
|
||||
setText(result.name);
|
||||
scope.ngModel[scope.field] = result;
|
||||
control.$setValidity("file-input", true);
|
||||
|
||||
@@ -1,132 +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.
|
||||
*****************************************************************************/
|
||||
|
||||
define(
|
||||
[
|
||||
'painterro'
|
||||
],
|
||||
function (Painterro) {
|
||||
|
||||
function SnapshotPreviewController($scope, openmct) {
|
||||
|
||||
$scope.previewImage = function (imageUrl) {
|
||||
let imageDiv = document.createElement('div');
|
||||
imageDiv.classList = 'image-main s-image-main';
|
||||
imageDiv.style.backgroundImage = `url(${imageUrl})`;
|
||||
|
||||
let previewImageOverlay = openmct.overlays.overlay(
|
||||
{
|
||||
element: imageDiv,
|
||||
size: 'large',
|
||||
buttons: [
|
||||
{
|
||||
label: 'Done',
|
||||
callback: function () {
|
||||
previewImageOverlay.dismiss();
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
$scope.annotateImage = function (ngModel, field, imageUrl) {
|
||||
$scope.imageUrl = imageUrl;
|
||||
|
||||
let div = document.createElement('div'),
|
||||
painterroInstance = {},
|
||||
save = false;
|
||||
|
||||
div.id = 'snap-annotation';
|
||||
|
||||
let annotateImageOverlay = openmct.overlays.overlay(
|
||||
{
|
||||
element: div,
|
||||
size: 'large',
|
||||
buttons: [
|
||||
{
|
||||
label: 'Cancel',
|
||||
callback: function () {
|
||||
save = false;
|
||||
painterroInstance.save();
|
||||
annotateImageOverlay.dismiss();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Save',
|
||||
callback: function () {
|
||||
save = true;
|
||||
painterroInstance.save();
|
||||
annotateImageOverlay.dismiss();
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
);
|
||||
|
||||
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) {
|
||||
let url = image.asBlob(),
|
||||
reader = new window.FileReader();
|
||||
|
||||
reader.readAsDataURL(url);
|
||||
reader.onloadend = function () {
|
||||
$scope.imageUrl = reader.result;
|
||||
ngModel[field] = reader.result;
|
||||
};
|
||||
} else {
|
||||
ngModel.field = imageUrl;
|
||||
console.warn('You cancelled the annotation!!!');
|
||||
}
|
||||
|
||||
done(true);
|
||||
}
|
||||
}).show(imageUrl);
|
||||
};
|
||||
}
|
||||
|
||||
return SnapshotPreviewController;
|
||||
}
|
||||
);
|
||||
@@ -128,7 +128,7 @@ define([
|
||||
};
|
||||
|
||||
ObjectServiceProvider.prototype.get = function (key) {
|
||||
const keyString = utils.makeKeyString(key);
|
||||
let keyString = utils.makeKeyString(key);
|
||||
|
||||
return this.objectService.getObjects([keyString])
|
||||
.then(function (results) {
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import utils from 'objectUtils';
|
||||
|
||||
export default class LegacyPersistenceAdapter {
|
||||
constructor(openmct) {
|
||||
this.openmct = openmct;
|
||||
@@ -33,8 +35,31 @@ export default class LegacyPersistenceAdapter {
|
||||
return Promise.resolve(Object.keys(this.openmct.objects.providers));
|
||||
}
|
||||
|
||||
updateObject(legacyDomainObject) {
|
||||
return this.openmct.objects.save(legacyDomainObject.useCapability('adapter'));
|
||||
createObject(space, key, legacyDomainObject) {
|
||||
let object = utils.toNewFormat(legacyDomainObject, {
|
||||
namespace: space,
|
||||
key: key
|
||||
});
|
||||
|
||||
return this.openmct.objects.save(object);
|
||||
}
|
||||
|
||||
deleteObject(space, key) {
|
||||
const identifier = {
|
||||
namespace: space,
|
||||
key: key
|
||||
};
|
||||
|
||||
return this.openmct.objects.delete(identifier);
|
||||
}
|
||||
|
||||
updateObject(space, key, legacyDomainObject) {
|
||||
let object = utils.toNewFormat(legacyDomainObject, {
|
||||
namespace: space,
|
||||
key: key
|
||||
});
|
||||
|
||||
return this.openmct.objects.save(object);
|
||||
}
|
||||
|
||||
readObject(space, key) {
|
||||
|
||||
@@ -47,6 +47,7 @@ define([
|
||||
this.providers = {};
|
||||
this.rootRegistry = new RootRegistry();
|
||||
this.rootProvider = new RootObjectProvider.default(this.rootRegistry);
|
||||
this.cache = {};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -154,6 +155,11 @@ define([
|
||||
* has been saved, or be rejected if it cannot be saved
|
||||
*/
|
||||
ObjectAPI.prototype.get = function (identifier) {
|
||||
let keystring = this.makeKeyString(identifier);
|
||||
if (this.cache[keystring] !== undefined) {
|
||||
return this.cache[keystring];
|
||||
}
|
||||
|
||||
identifier = utils.parseKeyString(identifier);
|
||||
const provider = this.getProvider(identifier);
|
||||
|
||||
@@ -165,7 +171,15 @@ define([
|
||||
throw new Error('Provider does not support get!');
|
||||
}
|
||||
|
||||
return provider.get(identifier);
|
||||
let objectPromise = provider.get(identifier);
|
||||
|
||||
this.cache[keystring] = objectPromise;
|
||||
|
||||
return objectPromise.then(result => {
|
||||
delete this.cache[keystring];
|
||||
|
||||
return result;
|
||||
});
|
||||
};
|
||||
|
||||
ObjectAPI.prototype.delete = function () {
|
||||
|
||||
@@ -59,4 +59,25 @@ describe("The Object API", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("The get function", () => {
|
||||
describe("when a provider is available", () => {
|
||||
let mockProvider;
|
||||
beforeEach(() => {
|
||||
mockProvider = jasmine.createSpyObj("mock provider", [
|
||||
"get"
|
||||
]);
|
||||
mockProvider.get.and.returnValue(Promise.resolve(mockDomainObject));
|
||||
objectAPI.addProvider(TEST_NAMESPACE, mockProvider);
|
||||
});
|
||||
|
||||
it("Caches multiple requests for the same object", () => {
|
||||
expect(mockProvider.get.calls.count()).toBe(0);
|
||||
objectAPI.get(mockDomainObject.identifier);
|
||||
expect(mockProvider.get.calls.count()).toBe(1);
|
||||
objectAPI.get(mockDomainObject.identifier);
|
||||
expect(mockProvider.get.calls.count()).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
*****************************************************************************/
|
||||
|
||||
<template>
|
||||
<div class="c-lad-table-wrapper">
|
||||
<div class="c-lad-table-wrapper u-style-receiver js-style-receiver">
|
||||
<table class="c-table c-lad-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
.c-cs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
|
||||
@@ -21,21 +21,22 @@
|
||||
*****************************************************************************/
|
||||
|
||||
<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) }"
|
||||
<div class="c-style has-local-controls c-toolbar">
|
||||
<div class="c-style__controls">
|
||||
<div :class="[
|
||||
{ 'is-style-invisible': styleItem.style && 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"
|
||||
>
|
||||
ABC
|
||||
</span>
|
||||
</span>
|
||||
<span class="c-toolbar">
|
||||
<span class="c-style-thumb__text"
|
||||
:class="{ 'hide-nice': !hasProperty(styleItem.style.color) }"
|
||||
>
|
||||
ABC
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<toolbar-color-picker v-if="hasProperty(styleItem.style.border)"
|
||||
class="c-style__toolbar-button--border-color u-menu-to--center"
|
||||
:options="borderColorOption"
|
||||
@@ -61,7 +62,14 @@
|
||||
:options="isStyleInvisibleOption"
|
||||
@change="updateStyleValue"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Save Styles -->
|
||||
<toolbar-button v-if="canSaveStyle"
|
||||
class="c-style__toolbar-button--save c-local-controls--show-on-hover c-icon-button c-icon-button--major"
|
||||
:options="saveOptions"
|
||||
@click="saveItemStyle()"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -80,12 +88,11 @@ export default {
|
||||
ToolbarColorPicker,
|
||||
ToolbarToggleButton
|
||||
},
|
||||
inject: [
|
||||
'openmct'
|
||||
],
|
||||
inject: ['openmct'],
|
||||
props: {
|
||||
isEditing: {
|
||||
type: Boolean
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
mixedStyles: {
|
||||
type: Array,
|
||||
@@ -93,6 +100,10 @@ export default {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
nonSpecificFontProperties: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
styleItem: {
|
||||
type: Object,
|
||||
required: true
|
||||
@@ -182,7 +193,16 @@ export default {
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
},
|
||||
saveOptions() {
|
||||
return {
|
||||
icon: 'icon-save',
|
||||
title: 'Save style',
|
||||
isEditing: this.isEditing
|
||||
};
|
||||
},
|
||||
canSaveStyle() {
|
||||
return this.isEditing && !this.mixedStyles.length && !this.nonSpecificFontProperties.length;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -216,6 +236,9 @@ export default {
|
||||
}
|
||||
|
||||
this.$emit('persist', this.styleItem, item.property);
|
||||
},
|
||||
saveItemStyle() {
|
||||
this.$emit('save-style', this.itemStyle);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -31,6 +31,11 @@
|
||||
<div class="c-inspect-styles__header">
|
||||
Object Style
|
||||
</div>
|
||||
<FontStyleEditor
|
||||
v-if="canStyleFont"
|
||||
:font-style="consolidatedFontStyle"
|
||||
@set-font-property="setFontProperty"
|
||||
/>
|
||||
<div class="c-inspect-styles__content">
|
||||
<div v-if="staticStyle"
|
||||
class="c-inspect-styles__style"
|
||||
@@ -39,7 +44,9 @@
|
||||
:style-item="staticStyle"
|
||||
:is-editing="allowEditing"
|
||||
:mixed-styles="mixedStyles"
|
||||
:non-specific-font-properties="nonSpecificFontProperties"
|
||||
@persist="updateStaticStyle"
|
||||
@save-style="saveStyle"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
@@ -58,10 +65,11 @@
|
||||
</div>
|
||||
<div class="c-inspect-styles__content c-inspect-styles__condition-set">
|
||||
<a v-if="conditionSetDomainObject"
|
||||
class="c-object-label icon-conditional"
|
||||
class="c-object-label"
|
||||
:href="navigateToPath"
|
||||
@click="navigateOrPreview"
|
||||
>
|
||||
<span class="c-object-label__type-icon icon-conditional"></span>
|
||||
<span class="c-object-label__name">{{ conditionSetDomainObject.name }}</span>
|
||||
</a>
|
||||
<template v-if="allowEditing">
|
||||
@@ -80,6 +88,12 @@
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<FontStyleEditor
|
||||
v-if="canStyleFont"
|
||||
:font-style="consolidatedFontStyle"
|
||||
@set-font-property="setFontProperty"
|
||||
/>
|
||||
|
||||
<div v-if="conditionsLoaded"
|
||||
class="c-inspect-styles__conditions"
|
||||
>
|
||||
@@ -97,8 +111,10 @@
|
||||
/>
|
||||
<style-editor class="c-inspect-styles__editor"
|
||||
:style-item="conditionStyle"
|
||||
:non-specific-font-properties="nonSpecificFontProperties"
|
||||
:is-editing="allowEditing"
|
||||
@persist="updateConditionalStyle"
|
||||
@save-style="saveStyle"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -108,6 +124,7 @@
|
||||
|
||||
<script>
|
||||
|
||||
import FontStyleEditor from '@/ui/inspector/styles/FontStyleEditor.vue';
|
||||
import StyleEditor from "./StyleEditor.vue";
|
||||
import PreviewAction from "@/ui/preview/PreviewAction.js";
|
||||
import { getApplicableStylesForItem, getConsolidatedStyleValues, getConditionSetIdentifierForItem } from "@/plugins/condition/utils/styleUtils";
|
||||
@@ -116,16 +133,30 @@ import ConditionError from "@/plugins/condition/components/ConditionError.vue";
|
||||
import ConditionDescription from "@/plugins/condition/components/ConditionDescription.vue";
|
||||
import Vue from 'vue';
|
||||
|
||||
const NON_SPECIFIC = '??';
|
||||
const NON_STYLEABLE_CONTAINER_TYPES = [
|
||||
'layout',
|
||||
'flexible-layout',
|
||||
'tabs'
|
||||
];
|
||||
const NON_STYLEABLE_LAYOUT_ITEM_TYPES = [
|
||||
'line-view',
|
||||
'box-view',
|
||||
'image-view'
|
||||
];
|
||||
|
||||
export default {
|
||||
name: 'StylesView',
|
||||
components: {
|
||||
FontStyleEditor,
|
||||
StyleEditor,
|
||||
ConditionError,
|
||||
ConditionDescription
|
||||
},
|
||||
inject: [
|
||||
'openmct',
|
||||
'selection'
|
||||
'selection',
|
||||
'stylesManager'
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
@@ -139,19 +170,80 @@ export default {
|
||||
conditionsLoaded: false,
|
||||
navigateToPath: '',
|
||||
selectedConditionId: '',
|
||||
locked: false
|
||||
items: [],
|
||||
domainObject: undefined,
|
||||
consolidatedFontStyle: {}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
locked() {
|
||||
return this.selection.some(selectionPath => {
|
||||
const self = selectionPath[0].context.item;
|
||||
const parent = selectionPath.length > 1 ? selectionPath[1].context.item : undefined;
|
||||
|
||||
return (self && self.locked) || (parent && parent.locked);
|
||||
});
|
||||
},
|
||||
allowEditing() {
|
||||
return this.isEditing && !this.locked;
|
||||
},
|
||||
styleableFontItems() {
|
||||
return this.selection.filter(selectionPath => {
|
||||
const item = selectionPath[0].context.item;
|
||||
const itemType = item && item.type;
|
||||
const layoutItem = selectionPath[0].context.layoutItem;
|
||||
const layoutItemType = layoutItem && layoutItem.type;
|
||||
|
||||
if (itemType && NON_STYLEABLE_CONTAINER_TYPES.includes(itemType)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (layoutItemType && NON_STYLEABLE_LAYOUT_ITEM_TYPES.includes(layoutItemType)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
},
|
||||
computedconsolidatedFontStyle() {
|
||||
let consolidatedFontStyle;
|
||||
const styles = [];
|
||||
|
||||
this.styleableFontItems.forEach(styleable => {
|
||||
const fontStyle = this.getFontStyle(styleable[0]);
|
||||
|
||||
styles.push(fontStyle);
|
||||
});
|
||||
|
||||
if (styles.length) {
|
||||
const hasConsolidatedFontSize = styles.length && styles.every((fontStyle, i, arr) => fontStyle.fontSize === arr[0].fontSize);
|
||||
const hasConsolidatedFont = styles.length && styles.every((fontStyle, i, arr) => fontStyle.font === arr[0].font);
|
||||
|
||||
consolidatedFontStyle = {
|
||||
fontSize: hasConsolidatedFontSize ? styles[0].fontSize : NON_SPECIFIC,
|
||||
font: hasConsolidatedFont ? styles[0].font : NON_SPECIFIC
|
||||
};
|
||||
}
|
||||
|
||||
return consolidatedFontStyle;
|
||||
},
|
||||
nonSpecificFontProperties() {
|
||||
if (!this.consolidatedFontStyle) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Object.keys(this.consolidatedFontStyle).filter(property => this.consolidatedFontStyle[property] === NON_SPECIFIC);
|
||||
},
|
||||
canStyleFont() {
|
||||
return this.styleableFontItems.length && this.allowEditing;
|
||||
}
|
||||
},
|
||||
destroyed() {
|
||||
this.removeListeners();
|
||||
this.openmct.editor.off('isEditing', this.setEditState);
|
||||
this.stylesManager.off('styleSelected', this.applyStyleToSelection);
|
||||
},
|
||||
mounted() {
|
||||
this.items = [];
|
||||
this.previewAction = new PreviewAction(this.openmct);
|
||||
this.isMultipleSelection = this.selection.length > 1;
|
||||
this.getObjectsAndItemsFromSelection();
|
||||
@@ -166,7 +258,10 @@ export default {
|
||||
this.initializeStaticStyle();
|
||||
}
|
||||
|
||||
this.setConsolidatedFontStyle();
|
||||
|
||||
this.openmct.editor.on('isEditing', this.setEditState);
|
||||
this.stylesManager.on('styleSelected', this.applyStyleToSelection);
|
||||
},
|
||||
methods: {
|
||||
getObjectStyles() {
|
||||
@@ -178,10 +273,10 @@ export default {
|
||||
}
|
||||
} else if (this.items.length) {
|
||||
const itemId = this.items[0].id;
|
||||
if (this.domainObject.configuration && this.domainObject.configuration.objectStyles && this.domainObject.configuration.objectStyles[itemId]) {
|
||||
if (this.domainObject && this.domainObject.configuration && this.domainObject.configuration.objectStyles && this.domainObject.configuration.objectStyles[itemId]) {
|
||||
objectStyles = this.domainObject.configuration.objectStyles[itemId];
|
||||
}
|
||||
} else if (this.domainObject.configuration && this.domainObject.configuration.objectStyles) {
|
||||
} else if (this.domainObject && this.domainObject.configuration && this.domainObject.configuration.objectStyles) {
|
||||
objectStyles = this.domainObject.configuration.objectStyles;
|
||||
}
|
||||
|
||||
@@ -219,6 +314,18 @@ export default {
|
||||
isItemType(type, item) {
|
||||
return item && (item.type === type);
|
||||
},
|
||||
canPersistObject(item) {
|
||||
// for now the only way to tell if an object can be persisted is if it is creatable.
|
||||
let creatable = false;
|
||||
if (item) {
|
||||
const type = this.openmct.types.get(item.type);
|
||||
if (type && type.definition) {
|
||||
creatable = (type.definition.creatable === true);
|
||||
}
|
||||
}
|
||||
|
||||
return creatable;
|
||||
},
|
||||
hasConditionalStyle(domainObject, layoutItem) {
|
||||
const id = layoutItem ? layoutItem.id : undefined;
|
||||
|
||||
@@ -235,13 +342,8 @@ export default {
|
||||
this.selection.forEach((selectionItem) => {
|
||||
const item = selectionItem[0].context.item;
|
||||
const layoutItem = selectionItem[0].context.layoutItem;
|
||||
const layoutDomainObject = selectionItem[0].context.item;
|
||||
const isChildItem = selectionItem.length > 1;
|
||||
|
||||
if (layoutDomainObject && layoutDomainObject.locked) {
|
||||
this.locked = true;
|
||||
}
|
||||
|
||||
if (!isChildItem) {
|
||||
domainObject = item;
|
||||
itemStyle = getApplicableStylesForItem(item);
|
||||
@@ -251,7 +353,7 @@ export default {
|
||||
} else {
|
||||
this.canHide = true;
|
||||
domainObject = selectionItem[1].context.item;
|
||||
if (item && !layoutItem || this.isItemType('subobject-view', layoutItem)) {
|
||||
if (item && !layoutItem || (this.isItemType('subobject-view', layoutItem) && this.canPersistObject(item))) {
|
||||
subObjects.push(item);
|
||||
itemStyle = getApplicableStylesForItem(item);
|
||||
if (this.hasConditionalStyle(item)) {
|
||||
@@ -275,7 +377,7 @@ export default {
|
||||
const {styles, mixedStyles} = getConsolidatedStyleValues(itemInitialStyles);
|
||||
this.initialStyles = styles;
|
||||
this.mixedStyles = mixedStyles;
|
||||
|
||||
// main layout
|
||||
this.domainObject = domainObject;
|
||||
this.removeListeners();
|
||||
if (this.domainObject) {
|
||||
@@ -298,6 +400,7 @@ export default {
|
||||
isKeyItemId(key) {
|
||||
return (key !== 'styles')
|
||||
&& (key !== 'staticStyle')
|
||||
&& (key !== 'fontStyle')
|
||||
&& (key !== 'defaultConditionId')
|
||||
&& (key !== 'selectedConditionId')
|
||||
&& (key !== 'conditionSetIdentifier');
|
||||
@@ -637,6 +740,124 @@ export default {
|
||||
},
|
||||
persist(domainObject, style) {
|
||||
this.openmct.objects.mutate(domainObject, 'configuration.objectStyles', style);
|
||||
},
|
||||
applyStyleToSelection(style) {
|
||||
if (!this.allowEditing) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateSelectionFontStyle(style);
|
||||
this.updateSelectionStyle(style);
|
||||
},
|
||||
updateSelectionFontStyle(style) {
|
||||
const fontSizeProperty = {
|
||||
fontSize: style.fontSize
|
||||
};
|
||||
const fontProperty = {
|
||||
font: style.font
|
||||
};
|
||||
|
||||
this.setFontProperty(fontSizeProperty);
|
||||
this.setFontProperty(fontProperty);
|
||||
},
|
||||
updateSelectionStyle(style) {
|
||||
const foundStyle = this.findStyleByConditionId(this.selectedConditionId);
|
||||
|
||||
if (foundStyle && !this.isStaticAndConditionalStyles) {
|
||||
Object.entries(style).forEach(([property, value]) => {
|
||||
if (foundStyle.style[property] !== undefined && foundStyle.style[property] !== value) {
|
||||
foundStyle.style[property] = value;
|
||||
}
|
||||
});
|
||||
this.getAndPersistStyles();
|
||||
} else {
|
||||
this.removeConditionSet();
|
||||
Object.entries(style).forEach(([property, value]) => {
|
||||
if (this.staticStyle.style[property] !== undefined && this.staticStyle.style[property] !== value) {
|
||||
this.staticStyle.style[property] = value;
|
||||
this.getAndPersistStyles(property);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
saveStyle(style) {
|
||||
const styleToSave = {
|
||||
...style,
|
||||
...this.consolidatedFontStyle
|
||||
};
|
||||
|
||||
this.stylesManager.save(styleToSave);
|
||||
},
|
||||
setConsolidatedFontStyle() {
|
||||
const styles = [];
|
||||
|
||||
this.styleableFontItems.forEach(styleable => {
|
||||
const fontStyle = this.getFontStyle(styleable[0]);
|
||||
|
||||
styles.push(fontStyle);
|
||||
});
|
||||
|
||||
if (styles.length) {
|
||||
const hasConsolidatedFontSize = styles.length && styles.every((fontStyle, i, arr) => fontStyle.fontSize === arr[0].fontSize);
|
||||
const hasConsolidatedFont = styles.length && styles.every((fontStyle, i, arr) => fontStyle.font === arr[0].font);
|
||||
|
||||
const fontSize = hasConsolidatedFontSize ? styles[0].fontSize : NON_SPECIFIC;
|
||||
const font = hasConsolidatedFont ? styles[0].font : NON_SPECIFIC;
|
||||
|
||||
this.$set(this.consolidatedFontStyle, 'fontSize', fontSize);
|
||||
this.$set(this.consolidatedFontStyle, 'font', font);
|
||||
}
|
||||
},
|
||||
getFontStyle(selectionPath) {
|
||||
const item = selectionPath.context.item;
|
||||
const layoutItem = selectionPath.context.layoutItem;
|
||||
let fontStyle = item && item.configuration && item.configuration.fontStyle;
|
||||
|
||||
// support for legacy where font styling in layouts only
|
||||
if (!fontStyle) {
|
||||
fontStyle = {
|
||||
fontSize: layoutItem && layoutItem.fontSize || 'default',
|
||||
font: layoutItem && layoutItem.font || 'default'
|
||||
};
|
||||
}
|
||||
|
||||
return fontStyle;
|
||||
},
|
||||
setFontProperty(fontStyleObject) {
|
||||
let layoutDomainObject;
|
||||
const [property, value] = Object.entries(fontStyleObject)[0];
|
||||
|
||||
this.styleableFontItems.forEach(styleable => {
|
||||
if (!this.isLayoutObject(styleable)) {
|
||||
const fontStyle = this.getFontStyle(styleable[0]);
|
||||
fontStyle[property] = value;
|
||||
|
||||
this.openmct.objects.mutate(styleable[0].context.item, 'configuration.fontStyle', fontStyle);
|
||||
} else {
|
||||
// all layoutItems in this context will share same parent layout
|
||||
if (!layoutDomainObject) {
|
||||
layoutDomainObject = styleable[1].context.item;
|
||||
}
|
||||
|
||||
// save layout item font style to parent layout configuration
|
||||
const layoutItemIndex = styleable[0].context.index;
|
||||
const layoutItemConfiguration = layoutDomainObject.configuration.items[layoutItemIndex];
|
||||
|
||||
layoutItemConfiguration[property] = value;
|
||||
}
|
||||
});
|
||||
|
||||
if (layoutDomainObject) {
|
||||
this.openmct.objects.mutate(layoutDomainObject, 'configuration.items', layoutDomainObject.configuration.items);
|
||||
}
|
||||
|
||||
// sync vue component on font update
|
||||
this.$set(this.consolidatedFontStyle, property, value);
|
||||
},
|
||||
isLayoutObject(selectionPath) {
|
||||
const layoutItemType = selectionPath[0].context.layoutItem && selectionPath[0].context.layoutItem.type;
|
||||
|
||||
return layoutItemType && layoutItemType !== 'subobject-view';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -40,9 +40,11 @@
|
||||
}
|
||||
|
||||
&__condition-set {
|
||||
align-items: baseline;
|
||||
border-bottom: 1px solid $colorInteriorBorder;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding-bottom: $interiorMargin;
|
||||
|
||||
.c-object-label {
|
||||
flex: 1 1 auto;
|
||||
@@ -53,7 +55,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__style,
|
||||
&__style {
|
||||
padding-bottom: $interiorMargin;
|
||||
}
|
||||
|
||||
&__condition {
|
||||
padding: $interiorMargin;
|
||||
}
|
||||
|
||||
@@ -146,6 +146,8 @@ describe('the plugin', function () {
|
||||
let displayLayoutItem;
|
||||
let lineLayoutItem;
|
||||
let boxLayoutItem;
|
||||
let notCreatableObjectItem;
|
||||
let notCreatableObject;
|
||||
let selection;
|
||||
let component;
|
||||
let styleViewComponentObject;
|
||||
@@ -264,6 +266,19 @@ describe('the plugin', function () {
|
||||
"stroke": "#717171",
|
||||
"type": "line-view",
|
||||
"id": "57d49a28-7863-43bd-9593-6570758916f0"
|
||||
},
|
||||
{
|
||||
"width": 32,
|
||||
"height": 18,
|
||||
"x": 36,
|
||||
"y": 8,
|
||||
"identifier": {
|
||||
"key": "~TEST~image",
|
||||
"namespace": "test-space"
|
||||
},
|
||||
"hasFrame": true,
|
||||
"type": "subobject-view",
|
||||
"id": "6d9fe81b-a3ce-4e59-b404-a4a0be1a5d85"
|
||||
}
|
||||
],
|
||||
"layoutGrid": [
|
||||
@@ -297,6 +312,52 @@ describe('the plugin', function () {
|
||||
"type": "box-view",
|
||||
"id": "89b88746-d325-487b-aec4-11b79afff9e8"
|
||||
};
|
||||
notCreatableObjectItem = {
|
||||
"width": 32,
|
||||
"height": 18,
|
||||
"x": 36,
|
||||
"y": 8,
|
||||
"identifier": {
|
||||
"key": "~TEST~image",
|
||||
"namespace": "test-space"
|
||||
},
|
||||
"hasFrame": true,
|
||||
"type": "subobject-view",
|
||||
"id": "6d9fe81b-a3ce-4e59-b404-a4a0be1a5d85"
|
||||
};
|
||||
notCreatableObject = {
|
||||
"identifier": {
|
||||
"key": "~TEST~image",
|
||||
"namespace": "test-space"
|
||||
},
|
||||
"name": "test~image",
|
||||
"location": "test-space:~TEST",
|
||||
"type": "test.image",
|
||||
"telemetry": {
|
||||
"values": [
|
||||
{
|
||||
"key": "value",
|
||||
"name": "Value",
|
||||
"hints": {
|
||||
"image": 1,
|
||||
"priority": 0
|
||||
},
|
||||
"format": "image",
|
||||
"source": "value"
|
||||
},
|
||||
{
|
||||
"key": "utc",
|
||||
"source": "timestamp",
|
||||
"name": "Timestamp",
|
||||
"format": "iso",
|
||||
"hints": {
|
||||
"domain": 1,
|
||||
"priority": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
selection = [
|
||||
[{
|
||||
context: {
|
||||
@@ -316,6 +377,19 @@ describe('the plugin', function () {
|
||||
"index": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
context: {
|
||||
item: displayLayoutItem,
|
||||
"supportsMultiSelect": true
|
||||
}
|
||||
}],
|
||||
[{
|
||||
context: {
|
||||
"item": notCreatableObject,
|
||||
"layoutItem": notCreatableObjectItem,
|
||||
"index": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
context: {
|
||||
item: displayLayoutItem,
|
||||
@@ -344,7 +418,7 @@ describe('the plugin', function () {
|
||||
});
|
||||
|
||||
it('initializes the items in the view', () => {
|
||||
expect(styleViewComponentObject.items.length).toBe(2);
|
||||
expect(styleViewComponentObject.items.length).toBe(3);
|
||||
});
|
||||
|
||||
it('initializes conditional styles', () => {
|
||||
@@ -363,7 +437,7 @@ describe('the plugin', function () {
|
||||
|
||||
return Vue.nextTick().then(() => {
|
||||
expect(styleViewComponentObject.domainObject.configuration.objectStyles).toBeDefined();
|
||||
[boxLayoutItem, lineLayoutItem].forEach((item) => {
|
||||
[boxLayoutItem, lineLayoutItem, notCreatableObjectItem].forEach((item) => {
|
||||
const itemStyles = styleViewComponentObject.domainObject.configuration.objectStyles[item.id].styles;
|
||||
expect(itemStyles.length).toBe(2);
|
||||
const foundStyle = itemStyles.find((style) => {
|
||||
@@ -385,7 +459,7 @@ describe('the plugin', function () {
|
||||
|
||||
return Vue.nextTick().then(() => {
|
||||
expect(styleViewComponentObject.domainObject.configuration.objectStyles).toBeDefined();
|
||||
[boxLayoutItem, lineLayoutItem].forEach((item) => {
|
||||
[boxLayoutItem, lineLayoutItem, notCreatableObjectItem].forEach((item) => {
|
||||
const itemStyle = styleViewComponentObject.domainObject.configuration.objectStyles[item.id].staticStyle;
|
||||
expect(itemStyle).toBeDefined();
|
||||
const applicableStyles = getApplicableStylesForItem(styleViewComponentObject.domainObject, item);
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
<template>
|
||||
<component :is="urlDefined ? 'a' : 'span'"
|
||||
class="c-condition-widget"
|
||||
class="c-condition-widget u-style-receiver js-style-receiver"
|
||||
:href="urlDefined ? internalDomainObject.url : null"
|
||||
>
|
||||
<div class="c-condition-widget__label">
|
||||
|
||||
@@ -73,7 +73,6 @@ define(['lodash'], function (_) {
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const VIEW_TYPES = {
|
||||
'telemetry-view': {
|
||||
value: 'telemetry-view',
|
||||
@@ -96,7 +95,6 @@ define(['lodash'], function (_) {
|
||||
class: 'icon-tabular-realtime'
|
||||
}
|
||||
};
|
||||
|
||||
const APPLICABLE_VIEWS = {
|
||||
'telemetry-view': [
|
||||
VIEW_TYPES['telemetry.plot.overlay'],
|
||||
@@ -390,29 +388,6 @@ define(['lodash'], function (_) {
|
||||
}
|
||||
}
|
||||
|
||||
function getTextSizeMenu(selectedParent, selection) {
|
||||
const TEXT_SIZE = [8, 9, 10, 11, 12, 13, 14, 15, 16, 20, 24, 30, 36, 48, 72, 96, 128];
|
||||
|
||||
return {
|
||||
control: "select-menu",
|
||||
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) + ".size";
|
||||
},
|
||||
title: "Set text size",
|
||||
options: TEXT_SIZE.map(size => {
|
||||
return {
|
||||
value: size + "px"
|
||||
};
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
function getTextButton(selectedParent, selection) {
|
||||
return {
|
||||
control: "button",
|
||||
@@ -423,7 +398,7 @@ define(['lodash'], function (_) {
|
||||
property: function (selectionPath) {
|
||||
return getPath(selectionPath);
|
||||
},
|
||||
icon: "icon-font",
|
||||
icon: "icon-pencil",
|
||||
title: "Edit text properties",
|
||||
dialog: DIALOG_FORM.text
|
||||
};
|
||||
@@ -623,6 +598,33 @@ define(['lodash'], function (_) {
|
||||
}
|
||||
}
|
||||
|
||||
function getToggleGridButton(selection, selectionPath) {
|
||||
const ICON_GRID_SHOW = 'icon-grid-on';
|
||||
const ICON_GRID_HIDE = 'icon-grid-off';
|
||||
|
||||
let displayLayoutContext;
|
||||
|
||||
if (selection.length === 1 && selectionPath === undefined) {
|
||||
displayLayoutContext = selection[0][0].context;
|
||||
} else {
|
||||
displayLayoutContext = selectionPath[1].context;
|
||||
}
|
||||
|
||||
return {
|
||||
control: "button",
|
||||
domainObject: displayLayoutContext.item,
|
||||
icon: ICON_GRID_SHOW,
|
||||
method: function () {
|
||||
displayLayoutContext.toggleGrid();
|
||||
|
||||
this.icon = this.icon === ICON_GRID_SHOW
|
||||
? ICON_GRID_HIDE
|
||||
: ICON_GRID_SHOW;
|
||||
},
|
||||
secondary: true
|
||||
};
|
||||
}
|
||||
|
||||
function getSeparator() {
|
||||
return {
|
||||
control: "separator"
|
||||
@@ -637,7 +639,9 @@ define(['lodash'], function (_) {
|
||||
}
|
||||
|
||||
if (isMainLayoutSelected(selectedObjects[0])) {
|
||||
return [getAddButton(selectedObjects)];
|
||||
return [
|
||||
getToggleGridButton(selectedObjects),
|
||||
getAddButton(selectedObjects)];
|
||||
}
|
||||
|
||||
let toolbar = {
|
||||
@@ -649,11 +653,11 @@ define(['lodash'], function (_) {
|
||||
'display-mode': [],
|
||||
'telemetry-value': [],
|
||||
'style': [],
|
||||
'text-style': [],
|
||||
'position': [],
|
||||
'duplicate': [],
|
||||
'unit-toggle': [],
|
||||
'remove': []
|
||||
'remove': [],
|
||||
'toggle-grid': []
|
||||
};
|
||||
|
||||
selectedObjects.forEach(selectionPath => {
|
||||
@@ -699,12 +703,6 @@ define(['lodash'], function (_) {
|
||||
toolbar['telemetry-value'] = [getTelemetryValueMenu(selectionPath, selectedObjects)];
|
||||
}
|
||||
|
||||
if (toolbar['text-style'].length === 0) {
|
||||
toolbar['text-style'] = [
|
||||
getTextSizeMenu(selectedParent, selectedObjects)
|
||||
];
|
||||
}
|
||||
|
||||
if (toolbar.position.length === 0) {
|
||||
toolbar.position = [
|
||||
getStackOrder(selectedParent, selectionPath),
|
||||
@@ -730,12 +728,6 @@ define(['lodash'], function (_) {
|
||||
}
|
||||
}
|
||||
} else if (layoutItem.type === 'text-view') {
|
||||
if (toolbar['text-style'].length === 0) {
|
||||
toolbar['text-style'] = [
|
||||
getTextSizeMenu(selectedParent, selectedObjects)
|
||||
];
|
||||
}
|
||||
|
||||
if (toolbar.position.length === 0) {
|
||||
toolbar.position = [
|
||||
getStackOrder(selectedParent, selectionPath),
|
||||
@@ -800,6 +792,10 @@ define(['lodash'], function (_) {
|
||||
if (toolbar.duplicate.length === 0) {
|
||||
toolbar.duplicate = [getDuplicateButton(selectedParent, selectionPath, selectedObjects)];
|
||||
}
|
||||
|
||||
if (toolbar['toggle-grid'].length === 0) {
|
||||
toolbar['toggle-grid'] = [getToggleGridButton(selectedObjects, selectionPath)];
|
||||
}
|
||||
});
|
||||
|
||||
let toolbarArray = Object.values(toolbar);
|
||||
|
||||
@@ -56,6 +56,28 @@ define(function () {
|
||||
1
|
||||
],
|
||||
required: true
|
||||
},
|
||||
{
|
||||
name: "Horizontal size (px)",
|
||||
control: "numberfield",
|
||||
cssClass: "l-input-sm l-numeric",
|
||||
property: [
|
||||
"configuration",
|
||||
"layoutDimensions",
|
||||
0
|
||||
],
|
||||
required: false
|
||||
},
|
||||
{
|
||||
name: "Vertical size (px)",
|
||||
control: "numberfield",
|
||||
cssClass: "l-input-sm l-numeric",
|
||||
property: [
|
||||
"configuration",
|
||||
"layoutDimensions",
|
||||
1
|
||||
],
|
||||
required: false
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
@endMove="() => $emit('endMove')"
|
||||
>
|
||||
<div
|
||||
class="c-box-view"
|
||||
class="c-box-view u-style-receiver js-style-receiver"
|
||||
:class="[styleClass]"
|
||||
:style="style"
|
||||
></div>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="l-layout"
|
||||
class="l-layout u-style-receiver js-style-receiver"
|
||||
:class="{
|
||||
'is-multi-selected': selectedLayoutItems.length > 1,
|
||||
'allow-editing': isEditing
|
||||
@@ -31,21 +31,19 @@
|
||||
@click.capture="bypassSelection"
|
||||
@drop="handleDrop"
|
||||
>
|
||||
<!-- Background grid -->
|
||||
<div
|
||||
<display-layout-grid
|
||||
v-if="isEditing"
|
||||
class="l-layout__grid-holder c-grid"
|
||||
:grid-size="gridSize"
|
||||
:show-grid="showGrid"
|
||||
/>
|
||||
<div
|
||||
v-if="shouldDisplayLayoutDimensions"
|
||||
class="l-layout__dimensions"
|
||||
:style="layoutDimensionsStyle"
|
||||
>
|
||||
<div
|
||||
v-if="gridSize[0] >= 3"
|
||||
class="c-grid__x l-grid l-grid-x"
|
||||
:style="[{ backgroundSize: gridSize[0] + 'px 100%' }]"
|
||||
></div>
|
||||
<div
|
||||
v-if="gridSize[1] >= 3"
|
||||
class="c-grid__y l-grid l-grid-y"
|
||||
:style="[{ backgroundSize: '100%' + gridSize[1] + 'px' }]"
|
||||
></div>
|
||||
<div class="l-layout__dimensions-vals">
|
||||
{{ layoutDimensions[0] }},{{ layoutDimensions[1] }}
|
||||
</div>
|
||||
</div>
|
||||
<component
|
||||
:is="item.type"
|
||||
@@ -81,6 +79,7 @@ import TextView from './TextView.vue';
|
||||
import LineView from './LineView.vue';
|
||||
import ImageView from './ImageView.vue';
|
||||
import EditMarquee from './EditMarquee.vue';
|
||||
import DisplayLayoutGrid from './DisplayLayoutGrid.vue';
|
||||
import _ from 'lodash';
|
||||
|
||||
const TELEMETRY_IDENTIFIER_FUNCTIONS = {
|
||||
@@ -127,6 +126,7 @@ const DUPLICATE_OFFSET = 3;
|
||||
|
||||
let components = ITEM_TYPE_VIEW_MAP;
|
||||
components['edit-marquee'] = EditMarquee;
|
||||
components['display-layout-grid'] = DisplayLayoutGrid;
|
||||
|
||||
function getItemDefinition(itemType, ...options) {
|
||||
let itemView = ITEM_TYPE_VIEW_MAP[itemType];
|
||||
@@ -140,6 +140,7 @@ function getItemDefinition(itemType, ...options) {
|
||||
|
||||
export default {
|
||||
components: components,
|
||||
inject: ['openmct', 'options', 'objectPath'],
|
||||
props: {
|
||||
domainObject: {
|
||||
type: Object,
|
||||
@@ -156,7 +157,8 @@ export default {
|
||||
return {
|
||||
internalDomainObject: domainObject,
|
||||
initSelectIndex: undefined,
|
||||
selection: []
|
||||
selection: [],
|
||||
showGrid: true
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -171,6 +173,23 @@ export default {
|
||||
return this.itemIsInCurrentSelection(item);
|
||||
});
|
||||
},
|
||||
layoutDimensions() {
|
||||
return this.internalDomainObject.configuration.layoutDimensions;
|
||||
},
|
||||
shouldDisplayLayoutDimensions() {
|
||||
return this.layoutDimensions
|
||||
&& this.layoutDimensions[0] > 0
|
||||
&& this.layoutDimensions[1] > 0;
|
||||
},
|
||||
layoutDimensionsStyle() {
|
||||
const width = `${this.layoutDimensions[0]}px`;
|
||||
const height = `${this.layoutDimensions[1]}px`;
|
||||
|
||||
return {
|
||||
width,
|
||||
height
|
||||
};
|
||||
},
|
||||
showMarquee() {
|
||||
let selectionPath = this.selection[0];
|
||||
let singleSelectedLine = this.selection.length === 1
|
||||
@@ -179,7 +198,13 @@ export default {
|
||||
return this.isEditing && selectionPath && selectionPath.length > 1 && !singleSelectedLine;
|
||||
}
|
||||
},
|
||||
inject: ['openmct', 'options', 'objectPath'],
|
||||
watch: {
|
||||
isEditing(value) {
|
||||
if (value) {
|
||||
this.showGrid = value;
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.unlisten = this.openmct.objects.observe(this.internalDomainObject, '*', function (obj) {
|
||||
this.internalDomainObject = JSON.parse(JSON.stringify(obj));
|
||||
@@ -798,6 +823,9 @@ export default {
|
||||
|
||||
this.removeItem(selection);
|
||||
this.initSelectIndex = this.layoutItems.length - 1; //restore selection
|
||||
},
|
||||
toggleGrid() {
|
||||
this.showGrid = !this.showGrid;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
34
src/plugins/displayLayout/components/DisplayLayoutGrid.vue
Normal file
34
src/plugins/displayLayout/components/DisplayLayoutGrid.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div
|
||||
class="l-layout__grid-holder"
|
||||
:class="{ 'c-grid': showGrid }"
|
||||
>
|
||||
<div
|
||||
v-if="gridSize[0] >= 3"
|
||||
class="c-grid__x l-grid l-grid-x"
|
||||
:style="[{ backgroundSize: gridSize[0] + 'px 100%' }]"
|
||||
></div>
|
||||
<div
|
||||
v-if="gridSize[1] >= 3"
|
||||
class="c-grid__y l-grid l-grid-y"
|
||||
:style="[{ backgroundSize: '100%' + gridSize[1] + 'px' }]"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
gridSize: {
|
||||
type: Array,
|
||||
required: true,
|
||||
validator: (arr) => arr && arr.length === 2
|
||||
&& arr.every(el => typeof el === 'number')
|
||||
},
|
||||
showGrid: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -81,6 +81,7 @@ export default {
|
||||
style() {
|
||||
let backgroundImage = 'url(' + this.item.url + ')';
|
||||
let border = '1px solid ' + this.item.stroke;
|
||||
|
||||
if (this.itemStyle) {
|
||||
if (this.itemStyle.imageUrl !== undefined) {
|
||||
backgroundImage = 'url(' + this.itemStyle.imageUrl + ')';
|
||||
|
||||
@@ -35,6 +35,8 @@
|
||||
:object-path="currentObjectPath"
|
||||
:has-frame="item.hasFrame"
|
||||
:show-edit-view="false"
|
||||
:layout-font-size="item.fontSize"
|
||||
:layout-font="item.font"
|
||||
/>
|
||||
</layout-frame>
|
||||
</template>
|
||||
@@ -73,6 +75,8 @@ export default {
|
||||
y: position[1],
|
||||
identifier: domainObject.identifier,
|
||||
hasFrame: hasFrameByDefault(domainObject.type),
|
||||
fontSize: 'default',
|
||||
font: 'default',
|
||||
viewKey
|
||||
};
|
||||
},
|
||||
|
||||
@@ -30,12 +30,14 @@
|
||||
>
|
||||
<div
|
||||
v-if="domainObject"
|
||||
class="c-telemetry-view"
|
||||
class="u-style-receiver c-telemetry-view"
|
||||
:class="{
|
||||
styleClass,
|
||||
'is-missing': domainObject.status === 'missing'
|
||||
}"
|
||||
:style="styleObject"
|
||||
:data-font-size="item.fontSize"
|
||||
:data-font="item.font"
|
||||
@contextmenu.prevent="showContextMenu"
|
||||
>
|
||||
<div class="is-missing__indicator"
|
||||
@@ -95,7 +97,8 @@ export default {
|
||||
stroke: "",
|
||||
fill: "",
|
||||
color: "",
|
||||
size: "13px"
|
||||
fontSize: 'default',
|
||||
font: 'default'
|
||||
};
|
||||
},
|
||||
inject: ['openmct', 'objectPath'],
|
||||
@@ -150,10 +153,15 @@ export default {
|
||||
return unit;
|
||||
},
|
||||
styleObject() {
|
||||
return Object.assign({}, {
|
||||
fontSize: this.item.size
|
||||
}, this.itemStyle);
|
||||
let size;
|
||||
//for legacy size support
|
||||
if (!this.item.fontSize) {
|
||||
size = this.item.size;
|
||||
}
|
||||
|
||||
return Object.assign({}, {
|
||||
size
|
||||
}, this.itemStyle);
|
||||
},
|
||||
fieldName() {
|
||||
return this.valueMetadata && this.valueMetadata.name;
|
||||
|
||||
@@ -29,7 +29,9 @@
|
||||
@endMove="() => $emit('endMove')"
|
||||
>
|
||||
<div
|
||||
class="c-text-view"
|
||||
class="c-text-view u-style-receiver js-style-receiver"
|
||||
:data-font-size="item.fontSize"
|
||||
:data-font="item.font"
|
||||
:class="[styleClass]"
|
||||
:style="style"
|
||||
>
|
||||
@@ -47,13 +49,14 @@ export default {
|
||||
return {
|
||||
fill: '',
|
||||
stroke: '',
|
||||
size: '13px',
|
||||
color: '',
|
||||
x: 1,
|
||||
y: 1,
|
||||
width: 10,
|
||||
height: 5,
|
||||
text: element.text
|
||||
text: element.text,
|
||||
fontSize: 'default',
|
||||
font: 'default'
|
||||
};
|
||||
},
|
||||
inject: ['openmct'],
|
||||
@@ -84,8 +87,14 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
style() {
|
||||
let size;
|
||||
//legacy size support
|
||||
if (!this.item.fontSize) {
|
||||
size = this.item.size;
|
||||
}
|
||||
|
||||
return Object.assign({
|
||||
fontSize: this.item.size
|
||||
size
|
||||
}, this.itemStyle);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -17,10 +17,29 @@
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
|
||||
&__grid-holder {
|
||||
&__grid-holder,
|
||||
&__dimensions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&__dimensions {
|
||||
$b: 1px dashed $editDimensionsColor;
|
||||
border-right: $b;
|
||||
border-bottom: $b;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
|
||||
&-vals {
|
||||
$p: 2px;
|
||||
color: $editDimensionsColor;
|
||||
display: inline-block;
|
||||
font-style: italic;
|
||||
position: absolute;
|
||||
bottom: $p; right: $p;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
&__frame {
|
||||
position: absolute;
|
||||
}
|
||||
@@ -34,6 +53,10 @@
|
||||
> .l-layout {
|
||||
background: $editUIGridColorBg;
|
||||
|
||||
> [class*="__dimensions"] {
|
||||
display: block;
|
||||
}
|
||||
|
||||
> [class*="__grid-holder"] {
|
||||
display: block;
|
||||
}
|
||||
@@ -42,12 +65,16 @@
|
||||
}
|
||||
|
||||
.l-layout__frame {
|
||||
&[s-selected],
|
||||
&[s-selected]:not([multi-select="true"]),
|
||||
&[s-selected-parent] {
|
||||
// Display grid and allow edit marquee to display in nested layouts when editing
|
||||
> * > * > .l-layout + .allow-editing {
|
||||
> * > * > .l-layout.allow-editing {
|
||||
box-shadow: inset $editUIGridColorFg 0 0 2px 1px;
|
||||
|
||||
> [class*="__dimensions"] {
|
||||
display: block;
|
||||
}
|
||||
|
||||
> [class*='grid-holder'] {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ export default {
|
||||
inject: ['openmct'],
|
||||
data() {
|
||||
return {
|
||||
objectStyle: undefined,
|
||||
itemStyle: undefined,
|
||||
styleClass: ''
|
||||
};
|
||||
|
||||
@@ -72,7 +72,8 @@ export default function DisplayLayoutPlugin(options) {
|
||||
duplicateItem: component && component.$refs.displayLayout.duplicateItem,
|
||||
switchViewType: component && component.$refs.displayLayout.switchViewType,
|
||||
mergeMultipleTelemetryViews: component && component.$refs.displayLayout.mergeMultipleTelemetryViews,
|
||||
mergeMultipleOverlayPlots: component && component.$refs.displayLayout.mergeMultipleOverlayPlots
|
||||
mergeMultipleOverlayPlots: component && component.$refs.displayLayout.mergeMultipleOverlayPlots,
|
||||
toggleGrid: component && component.$refs.displayLayout.toggleGrid
|
||||
};
|
||||
},
|
||||
onEditModeChange: function (isEditing) {
|
||||
|
||||
@@ -340,6 +340,7 @@ describe('the plugin', function () {
|
||||
|
||||
it('provides controls including separators', () => {
|
||||
const displayLayoutToolbar = openmct.toolbars.get(selection);
|
||||
|
||||
expect(displayLayoutToolbar.length).toBe(9);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,20 +3,26 @@
|
||||
@include userSelectNone();
|
||||
background: $colorFilterBg;
|
||||
color: $colorFilterFg;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.9em;
|
||||
margin-top: $interiorMarginSm;
|
||||
padding: 2px;
|
||||
text-transform: uppercase;
|
||||
|
||||
&:before {
|
||||
font-family: symbolsfont-12px;
|
||||
content: $glyph-icon-filter;
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
margin-right: $interiorMarginSm;
|
||||
}
|
||||
|
||||
&--mixed {
|
||||
.c-filter-indication__mixed {
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
&__label {
|
||||
+ .c-filter-indication__label {
|
||||
&:before {
|
||||
content: ', ';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.c-filter-tree-item {
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
|
||||
body.desktop & {
|
||||
flex-flow: row wrap;
|
||||
align-content: flex-start;
|
||||
|
||||
&__item {
|
||||
height: $gridItemDesk;
|
||||
width: $gridItemDesk;
|
||||
|
||||
@@ -1,116 +1,224 @@
|
||||
<template>
|
||||
<div class="c-imagery">
|
||||
<div
|
||||
tabindex="0"
|
||||
class="c-imagery"
|
||||
@keyup="arrowUpHandler"
|
||||
@keydown="arrowDownHandler"
|
||||
@mouseover="focusElement"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<input v-model="filters.contrast"
|
||||
class="icon-contrast"
|
||||
type="range"
|
||||
min="0"
|
||||
max="500"
|
||||
>
|
||||
<div class="h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover c-image-controls__controls">
|
||||
<span class="c-image-controls__sliders"
|
||||
draggable="true"
|
||||
@dragstart="startDrag"
|
||||
>
|
||||
<div class="c-image-controls__slider-wrapper icon-brightness">
|
||||
<input v-model="filters.brightness"
|
||||
type="range"
|
||||
min="0"
|
||||
max="500"
|
||||
>
|
||||
</div>
|
||||
<div class="c-image-controls__slider-wrapper icon-contrast">
|
||||
<input v-model="filters.contrast"
|
||||
type="range"
|
||||
min="0"
|
||||
max="500"
|
||||
>
|
||||
</div>
|
||||
</span>
|
||||
<span class="holder flex-elem t-reset-btn-holder c-imagery__lc__reset-btn">
|
||||
<span class="t-reset-btn-holder c-imagery__lc__reset-btn c-image-controls__btn-reset">
|
||||
<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 has-local-controls"
|
||||
:class="{'paused unnsynced': paused(),'stale':false }"
|
||||
:style="{'background-image': getImageUrl() ? `url(${getImageUrl()})` : 'none',
|
||||
'filter': `brightness(${filters.brightness}%) contrast(${filters.contrast}%)`}"
|
||||
<div class="c-imagery__main-image__bg"
|
||||
:class="{'paused unnsynced': isPaused,'stale':false }"
|
||||
>
|
||||
<div class="c-local-controls c-local-controls--show-on-hover c-imagery__prev-next-buttons">
|
||||
<button class="c-nav c-nav--prev"
|
||||
title="Previous image"
|
||||
:disabled="isPrevDisabled()"
|
||||
@click="prevImage()"
|
||||
></button>
|
||||
<button class="c-nav c-nav--next"
|
||||
title="Next image"
|
||||
:disabled="isNextDisabled()"
|
||||
@click="nextImage()"
|
||||
></button>
|
||||
</div>
|
||||
<div class="c-imagery__main-image__image"
|
||||
:style="{
|
||||
'background-image': imageUrl ? `url(${imageUrl})` : 'none',
|
||||
'filter': `brightness(${filters.brightness}%) contrast(${filters.contrast}%)`
|
||||
}"
|
||||
:data-openmct-image-timestamp="time"
|
||||
:data-openmct-object-keystring="keyString"
|
||||
></div>
|
||||
</div>
|
||||
<div class="c-local-controls c-local-controls--show-on-hover c-imagery__prev-next-buttons">
|
||||
<button class="c-nav c-nav--prev"
|
||||
title="Previous image"
|
||||
:disabled="isPrevDisabled"
|
||||
@click="prevImage()"
|
||||
></button>
|
||||
<button class="c-nav c-nav--next"
|
||||
title="Next image"
|
||||
:disabled="isNextDisabled"
|
||||
@click="nextImage()"
|
||||
></button>
|
||||
</div>
|
||||
|
||||
<div class="c-imagery__control-bar">
|
||||
<div class="c-imagery__timestamp">{{ getTime() }}</div>
|
||||
<div class="h-local-controls flex-elem">
|
||||
<div class="c-imagery__time">
|
||||
<div class="c-imagery__timestamp u-style-receiver js-style-receiver">{{ time }}</div>
|
||||
<div
|
||||
v-if="canTrackDuration"
|
||||
:class="{'c-imagery--new': isImageNew && !refreshCSS}"
|
||||
class="c-imagery__age icon-timer"
|
||||
>{{ formattedDuration }}</div>
|
||||
</div>
|
||||
<div class="h-local-controls">
|
||||
<button
|
||||
class="c-button icon-pause pause-play"
|
||||
:class="{'is-paused': paused()}"
|
||||
@click="paused(!paused(), true)"
|
||||
:class="{'is-paused': isPaused}"
|
||||
@click="paused(!isPaused, 'button')"
|
||||
></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="thumbsWrapper"
|
||||
class="c-imagery__thumbs-wrapper"
|
||||
:class="{'is-paused': paused()}"
|
||||
:class="{'is-paused': isPaused}"
|
||||
@scroll="handleScroll"
|
||||
>
|
||||
<div v-for="(imageData, index) in imageHistory"
|
||||
:key="index"
|
||||
<div v-for="(datum, index) in imageHistory"
|
||||
:key="datum.url"
|
||||
class="c-imagery__thumb c-thumb"
|
||||
:class="{selected: imageData.selected}"
|
||||
@click="setSelectedImage(imageData)"
|
||||
:class="{ selected: focusedImageIndex === index && isPaused }"
|
||||
@click="setFocusedImage(index, thumbnailClick)"
|
||||
>
|
||||
<img class="c-thumb__image"
|
||||
:src="getImageUrl(imageData)"
|
||||
:src="formatImageUrl(datum)"
|
||||
>
|
||||
<div class="c-thumb__timestamp">{{ getTime(imageData) }}</div>
|
||||
<div class="c-thumb__timestamp">{{ formatTime(datum) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import moment from 'moment';
|
||||
|
||||
const DEFAULT_DURATION_FORMATTER = 'duration';
|
||||
const REFRESH_CSS_MS = 500;
|
||||
const DURATION_TRACK_MS = 1000;
|
||||
const ARROW_DOWN_DELAY_CHECK_MS = 400;
|
||||
const ARROW_SCROLL_RATE_MS = 100;
|
||||
const THUMBNAIL_CLICKED = true;
|
||||
|
||||
const ONE_MINUTE = 60 * 1000;
|
||||
const FIVE_MINUTES = 5 * ONE_MINUTE;
|
||||
const ONE_HOUR = ONE_MINUTE * 60;
|
||||
const EIGHT_HOURS = 8 * ONE_HOUR;
|
||||
const TWENTYFOUR_HOURS = EIGHT_HOURS * 3;
|
||||
|
||||
const ARROW_RIGHT = 39;
|
||||
const ARROW_LEFT = 37;
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject'],
|
||||
data() {
|
||||
let timeSystem = this.openmct.time.timeSystem();
|
||||
|
||||
return {
|
||||
autoScroll: true,
|
||||
durationFormatter: undefined,
|
||||
filters: {
|
||||
brightness: 100,
|
||||
contrast: 100
|
||||
},
|
||||
image: {
|
||||
selected: ''
|
||||
},
|
||||
imageFormat: '',
|
||||
imageHistory: [],
|
||||
imageUrl: '',
|
||||
thumbnailClick: THUMBNAIL_CLICKED,
|
||||
isPaused: false,
|
||||
metadata: {},
|
||||
requestCount: 0,
|
||||
timeFormat: ''
|
||||
timeSystem: timeSystem,
|
||||
timeFormatter: undefined,
|
||||
refreshCSS: false,
|
||||
keyString: undefined,
|
||||
focusedImageIndex: undefined,
|
||||
numericDuration: undefined
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
bounds() {
|
||||
return this.openmct.time.bounds();
|
||||
time() {
|
||||
return this.formatTime(this.focusedImage);
|
||||
},
|
||||
imageUrl() {
|
||||
return this.formatImageUrl(this.focusedImage);
|
||||
},
|
||||
isImageNew() {
|
||||
let cutoff = FIVE_MINUTES;
|
||||
let age = this.numericDuration;
|
||||
|
||||
return age < cutoff && !this.refreshCSS;
|
||||
},
|
||||
canTrackDuration() {
|
||||
return this.openmct.time.clock() && this.timeSystem.isUTCBased;
|
||||
},
|
||||
isNextDisabled() {
|
||||
let disabled = false;
|
||||
|
||||
if (this.focusedImageIndex === -1 || this.focusedImageIndex === this.imageHistory.length - 1) {
|
||||
disabled = true;
|
||||
}
|
||||
|
||||
return disabled;
|
||||
},
|
||||
isPrevDisabled() {
|
||||
let disabled = false;
|
||||
|
||||
if (this.focusedImageIndex === 0 || this.imageHistory.length < 2) {
|
||||
disabled = true;
|
||||
}
|
||||
|
||||
return disabled;
|
||||
},
|
||||
focusedImage() {
|
||||
return this.imageHistory[this.focusedImageIndex];
|
||||
},
|
||||
parsedSelectedTime() {
|
||||
return this.parseTime(this.focusedImage);
|
||||
},
|
||||
formattedDuration() {
|
||||
let result = 'N/A';
|
||||
let negativeAge = -1;
|
||||
|
||||
if (this.numericDuration > TWENTYFOUR_HOURS) {
|
||||
negativeAge *= (this.numericDuration / TWENTYFOUR_HOURS);
|
||||
result = moment.duration(negativeAge, 'days').humanize(true);
|
||||
} else if (this.numericDuration > EIGHT_HOURS) {
|
||||
negativeAge *= (this.numericDuration / ONE_HOUR);
|
||||
result = moment.duration(negativeAge, 'hours').humanize(true);
|
||||
} else if (this.durationFormatter) {
|
||||
result = this.durationFormatter.format(this.numericDuration);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
focusedImageIndex() {
|
||||
this.trackDuration();
|
||||
this.resetAgeCSS();
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// set
|
||||
this.keystring = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
|
||||
this.imageFormat = this.openmct.telemetry.getValueFormatter(this.metadata.valuesForHints(['image'])[0]);
|
||||
// initialize
|
||||
this.timeKey = this.openmct.time.timeSystem().key;
|
||||
this.timeFormat = this.openmct.telemetry.getValueFormatter(this.metadata.value(this.timeKey));
|
||||
// listen
|
||||
this.openmct.time.on('bounds', this.boundsChange);
|
||||
this.openmct.time.on('timeSystem', this.timeSystemChange);
|
||||
this.openmct.time.on('clock', this.clockChange);
|
||||
|
||||
// set
|
||||
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
|
||||
this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
|
||||
this.imageFormatter = this.openmct.telemetry.getValueFormatter(this.metadata.valuesForHints(['image'])[0]);
|
||||
|
||||
// initialize
|
||||
this.timeKey = this.timeSystem.key;
|
||||
this.timeFormatter = this.getFormatter(this.timeKey);
|
||||
|
||||
// kickoff
|
||||
this.subscribe();
|
||||
this.requestHistory();
|
||||
@@ -124,38 +232,55 @@ export default {
|
||||
delete this.unsubscribe;
|
||||
}
|
||||
|
||||
this.stopDurationTracking();
|
||||
this.openmct.time.off('bounds', this.boundsChange);
|
||||
this.openmct.time.off('timeSystem', this.timeSystemChange);
|
||||
this.openmct.time.off('clock', this.clockChange);
|
||||
},
|
||||
methods: {
|
||||
focusElement() {
|
||||
this.$el.focus();
|
||||
},
|
||||
datumIsNotValid(datum) {
|
||||
if (this.imageHistory.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const datumTime = this.timeFormat.format(datum);
|
||||
const datumURL = this.imageFormat.format(datum);
|
||||
const lastHistoryTime = this.timeFormat.format(this.imageHistory.slice(-1)[0]);
|
||||
const lastHistoryURL = this.imageFormat.format(this.imageHistory.slice(-1)[0]);
|
||||
const datumURL = this.formatImageUrl(datum);
|
||||
const lastHistoryURL = this.formatImageUrl(this.imageHistory.slice(-1)[0]);
|
||||
|
||||
// datum is not valid if it matches the last datum in history,
|
||||
// or it is before the last datum in the history
|
||||
const datumTimeCheck = this.timeFormat.parse(datum);
|
||||
const historyTimeCheck = this.timeFormat.parse(this.imageHistory.slice(-1)[0]);
|
||||
const matchesLast = (datumTime === lastHistoryTime) && (datumURL === lastHistoryURL);
|
||||
const datumTimeCheck = this.parseTime(datum);
|
||||
const historyTimeCheck = this.parseTime(this.imageHistory.slice(-1)[0]);
|
||||
const matchesLast = (datumTimeCheck === historyTimeCheck) && (datumURL === lastHistoryURL);
|
||||
const isStale = datumTimeCheck < historyTimeCheck;
|
||||
|
||||
return matchesLast || isStale;
|
||||
},
|
||||
getImageUrl(datum) {
|
||||
return datum
|
||||
? this.imageFormat.format(datum)
|
||||
: this.imageUrl;
|
||||
formatImageUrl(datum) {
|
||||
if (!datum) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.imageFormatter.format(datum);
|
||||
},
|
||||
getTime(datum) {
|
||||
return datum
|
||||
? this.timeFormat.format(datum)
|
||||
: this.time;
|
||||
formatTime(datum) {
|
||||
if (!datum) {
|
||||
return;
|
||||
}
|
||||
|
||||
let dateTimeStr = this.timeFormatter.format(datum);
|
||||
|
||||
// Replace ISO "T" with a space to allow wrapping
|
||||
return dateTimeStr.replace("T", " ");
|
||||
},
|
||||
parseTime(datum) {
|
||||
if (!datum) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.timeFormatter.parse(datum);
|
||||
},
|
||||
handleScroll() {
|
||||
const thumbsWrapper = this.$refs.thumbsWrapper;
|
||||
@@ -168,26 +293,35 @@ export default {
|
||||
|| (scrollHeight - scrollTop) > 2 * clientHeight;
|
||||
this.autoScroll = !disableScroll;
|
||||
},
|
||||
paused(state, button = false) {
|
||||
if (arguments.length > 0 && state !== this.isPaused) {
|
||||
this.unselectAllImages();
|
||||
this.isPaused = state;
|
||||
if (state === true && button) {
|
||||
// If we are pausing, select the latest image in imageHistory
|
||||
this.setSelectedImage(this.imageHistory[this.imageHistory.length - 1]);
|
||||
}
|
||||
paused(state, type) {
|
||||
|
||||
if (this.nextDatum) {
|
||||
this.updateValues(this.nextDatum);
|
||||
delete this.nextDatum;
|
||||
} else {
|
||||
this.updateValues(this.imageHistory[this.imageHistory.length - 1]);
|
||||
}
|
||||
this.isPaused = state;
|
||||
|
||||
this.autoScroll = true;
|
||||
if (type === 'button') {
|
||||
this.setFocusedImage(this.imageHistory.length - 1);
|
||||
}
|
||||
|
||||
return this.isPaused;
|
||||
if (this.nextImageIndex) {
|
||||
this.setFocusedImage(this.nextImageIndex);
|
||||
delete this.nextImageIndex;
|
||||
}
|
||||
|
||||
this.autoScroll = true;
|
||||
},
|
||||
scrollToFocused() {
|
||||
const thumbsWrapper = this.$refs.thumbsWrapper;
|
||||
if (!thumbsWrapper) {
|
||||
return;
|
||||
}
|
||||
|
||||
let domThumb = thumbsWrapper.children[this.focusedImageIndex];
|
||||
|
||||
if (domThumb) {
|
||||
domThumb.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center'
|
||||
});
|
||||
}
|
||||
},
|
||||
scrollToRight() {
|
||||
if (this.isPaused || !this.$refs.thumbsWrapper || !this.autoScroll) {
|
||||
@@ -201,22 +335,17 @@ export default {
|
||||
|
||||
setTimeout(() => this.$refs.thumbsWrapper.scrollLeft = scrollWidth, 0);
|
||||
},
|
||||
setSelectedImage(image) {
|
||||
// If we are paused and the current image IS selected, unpause
|
||||
// Otherwise, set current image and pause
|
||||
if (!image) {
|
||||
setFocusedImage(index, thumbnailClick = false) {
|
||||
if (this.isPaused && !thumbnailClick) {
|
||||
this.nextImageIndex = index;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isPaused && image.selected) {
|
||||
this.paused(false);
|
||||
this.unselectAllImages();
|
||||
} else {
|
||||
this.imageUrl = this.getImageUrl(image);
|
||||
this.time = this.getTime(image);
|
||||
this.focusedImageIndex = index;
|
||||
|
||||
if (thumbnailClick && !this.isPaused) {
|
||||
this.paused(true);
|
||||
this.unselectAllImages();
|
||||
image.selected = true;
|
||||
}
|
||||
},
|
||||
boundsChange(bounds, isTick) {
|
||||
@@ -224,98 +353,162 @@ export default {
|
||||
this.requestHistory();
|
||||
}
|
||||
},
|
||||
requestHistory() {
|
||||
const requestId = ++this.requestCount;
|
||||
async requestHistory() {
|
||||
let bounds = this.openmct.time.bounds();
|
||||
this.requestCount++;
|
||||
const requestId = this.requestCount;
|
||||
this.imageHistory = [];
|
||||
this.openmct.telemetry
|
||||
.request(this.domainObject, this.bounds)
|
||||
.then((values = []) => {
|
||||
if (this.requestCount === requestId) {
|
||||
// add each image to the history
|
||||
// update values for the very last image (set current image time and url)
|
||||
values.forEach((datum, index) => this.updateHistory(datum, index === values.length - 1));
|
||||
}
|
||||
let data = await this.openmct.telemetry
|
||||
.request(this.domainObject, bounds) || [];
|
||||
|
||||
if (this.requestCount === requestId) {
|
||||
data.forEach((datum, index) => {
|
||||
this.updateHistory(datum, index === data.length - 1);
|
||||
});
|
||||
}
|
||||
},
|
||||
timeSystemChange(system) {
|
||||
// reset timesystem dependent variables
|
||||
this.timeKey = system.key;
|
||||
this.timeFormat = this.openmct.telemetry.getValueFormatter(this.metadata.value(this.timeKey));
|
||||
this.timeSystem = this.openmct.time.timeSystem();
|
||||
this.timeKey = this.timeSystem.key;
|
||||
this.timeFormatter = this.getFormatter(this.timeKey);
|
||||
this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
|
||||
this.trackDuration();
|
||||
},
|
||||
clockChange(clock) {
|
||||
this.trackDuration();
|
||||
},
|
||||
subscribe() {
|
||||
this.unsubscribe = this.openmct.telemetry
|
||||
.subscribe(this.domainObject, (datum) => {
|
||||
let parsedTimestamp = this.timeFormat.parse(datum);
|
||||
let parsedTimestamp = this.parseTime(datum);
|
||||
let bounds = this.openmct.time.bounds();
|
||||
|
||||
if (parsedTimestamp >= this.bounds.start && parsedTimestamp <= this.bounds.end) {
|
||||
if (parsedTimestamp >= bounds.start && parsedTimestamp <= bounds.end) {
|
||||
this.updateHistory(datum);
|
||||
}
|
||||
});
|
||||
},
|
||||
unselectAllImages() {
|
||||
this.imageHistory.forEach(image => image.selected = false);
|
||||
},
|
||||
updateHistory(datum, updateValues = true) {
|
||||
updateHistory(datum, setFocused = true) {
|
||||
if (this.datumIsNotValid(datum)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.imageHistory.push(datum);
|
||||
|
||||
if (updateValues) {
|
||||
this.updateValues(datum);
|
||||
if (setFocused) {
|
||||
this.setFocusedImage(this.imageHistory.length - 1);
|
||||
}
|
||||
},
|
||||
updateValues(datum) {
|
||||
if (this.isPaused) {
|
||||
this.nextDatum = datum;
|
||||
getFormatter(key) {
|
||||
let metadataValue = this.metadata.value(key) || { format: key };
|
||||
let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
|
||||
|
||||
return valueFormatter;
|
||||
},
|
||||
trackDuration() {
|
||||
if (this.canTrackDuration) {
|
||||
this.stopDurationTracking();
|
||||
this.updateDuration();
|
||||
this.durationTracker = window.setInterval(
|
||||
this.updateDuration, DURATION_TRACK_MS
|
||||
);
|
||||
} else {
|
||||
this.stopDurationTracking();
|
||||
}
|
||||
},
|
||||
stopDurationTracking() {
|
||||
window.clearInterval(this.durationTracker);
|
||||
},
|
||||
updateDuration() {
|
||||
let currentTime = this.openmct.time.clock().currentValue();
|
||||
this.numericDuration = currentTime - this.parsedSelectedTime;
|
||||
},
|
||||
resetAgeCSS() {
|
||||
this.refreshCSS = true;
|
||||
// unable to make this work with nextTick
|
||||
setTimeout(() => {
|
||||
this.refreshCSS = false;
|
||||
}, REFRESH_CSS_MS);
|
||||
},
|
||||
nextImage() {
|
||||
if (this.isNextDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.time = this.timeFormat.format(datum);
|
||||
this.imageUrl = this.imageFormat.format(datum);
|
||||
},
|
||||
selectedImageIndex() {
|
||||
return this.imageHistory.findIndex(image => image.selected);
|
||||
},
|
||||
setSelectedByIndex(index) {
|
||||
this.setSelectedImage(this.imageHistory[index]);
|
||||
},
|
||||
nextImage() {
|
||||
let index = this.selectedImageIndex();
|
||||
this.setSelectedByIndex(++index);
|
||||
let index = this.focusedImageIndex;
|
||||
|
||||
this.setFocusedImage(++index, THUMBNAIL_CLICKED);
|
||||
if (index === this.imageHistory.length - 1) {
|
||||
this.paused(false);
|
||||
}
|
||||
},
|
||||
prevImage() {
|
||||
let index = this.selectedImageIndex();
|
||||
if (index === -1) {
|
||||
this.setSelectedByIndex(this.imageHistory.length - 2);
|
||||
if (this.isPrevDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
let index = this.focusedImageIndex;
|
||||
|
||||
if (index === this.imageHistory.length - 1) {
|
||||
this.setFocusedImage(this.imageHistory.length - 2, THUMBNAIL_CLICKED);
|
||||
} else {
|
||||
this.setSelectedByIndex(--index);
|
||||
this.setFocusedImage(--index, THUMBNAIL_CLICKED);
|
||||
}
|
||||
},
|
||||
isNextDisabled() {
|
||||
let disabled = false;
|
||||
let index = this.selectedImageIndex();
|
||||
|
||||
if (index === -1 || index === this.imageHistory.length - 1) {
|
||||
disabled = true;
|
||||
}
|
||||
|
||||
return disabled;
|
||||
startDrag(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
},
|
||||
isPrevDisabled() {
|
||||
let disabled = false;
|
||||
let index = this.selectedImageIndex();
|
||||
arrowDownHandler(event) {
|
||||
let key = event.keyCode;
|
||||
|
||||
if (index === 0 || this.imageHistory.length < 2) {
|
||||
disabled = true;
|
||||
if (this.isLeftOrRightArrowKey(key)) {
|
||||
this.arrowDown = true;
|
||||
window.clearTimeout(this.arrowDownDelayTimeout);
|
||||
this.arrowDownDelayTimeout = window.setTimeout(() => {
|
||||
this.arrowKeyScroll(this.directionByKey(key));
|
||||
}, ARROW_DOWN_DELAY_CHECK_MS);
|
||||
}
|
||||
},
|
||||
arrowUpHandler(event) {
|
||||
let key = event.keyCode;
|
||||
|
||||
window.clearTimeout(this.arrowDownDelayTimeout);
|
||||
|
||||
if (this.isLeftOrRightArrowKey(key)) {
|
||||
this.arrowDown = false;
|
||||
let direction = this.directionByKey(key);
|
||||
this[direction + 'Image']();
|
||||
}
|
||||
},
|
||||
arrowKeyScroll(direction) {
|
||||
if (this.arrowDown) {
|
||||
this.arrowKeyScrolling = true;
|
||||
this[direction + 'Image']();
|
||||
setTimeout(() => {
|
||||
this.arrowKeyScroll(direction);
|
||||
}, ARROW_SCROLL_RATE_MS);
|
||||
} else {
|
||||
window.clearTimeout(this.arrowDownDelayTimeout);
|
||||
this.arrowKeyScrolling = false;
|
||||
this.scrollToFocused();
|
||||
}
|
||||
},
|
||||
directionByKey(keyCode) {
|
||||
let direction;
|
||||
|
||||
if (keyCode === ARROW_LEFT) {
|
||||
direction = 'prev';
|
||||
}
|
||||
|
||||
return disabled;
|
||||
if (keyCode === ARROW_RIGHT) {
|
||||
direction = 'next';
|
||||
}
|
||||
|
||||
return direction;
|
||||
},
|
||||
isLeftOrRightArrowKey(keyCode) {
|
||||
return [ARROW_RIGHT, ARROW_LEFT].includes(keyCode);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
.c-imagery {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
> * + * {
|
||||
margin-top: $interiorMargin;
|
||||
@@ -15,24 +19,75 @@
|
||||
}
|
||||
|
||||
&__main-image {
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
height: 100%;
|
||||
&__bg {
|
||||
background-color: $colorPlotBg;
|
||||
border: 1px solid transparent;
|
||||
flex: 1 1 auto;
|
||||
|
||||
&.unnsynced{
|
||||
@include sUnsynced();
|
||||
&.unnsynced{
|
||||
@include sUnsynced();
|
||||
}
|
||||
}
|
||||
|
||||
&__image {
|
||||
@include abs(); // Safari fix
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
}
|
||||
}
|
||||
|
||||
&__control-bar {
|
||||
padding: 5px 0 0 0;
|
||||
&__control-bar,
|
||||
&__time {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: baseline;
|
||||
|
||||
> * + * {
|
||||
margin-left: $interiorMarginSm;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&__control-bar {
|
||||
margin-top: 2px;
|
||||
padding: $interiorMarginSm 0;
|
||||
justify-content: space-between;
|
||||
|
||||
}
|
||||
|
||||
&__time {
|
||||
flex: 0 1 auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__timestamp,
|
||||
&__age {
|
||||
@include ellipsize();
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
&__timestamp {
|
||||
flex: 1 1 auto;
|
||||
flex-shrink: 10;
|
||||
}
|
||||
|
||||
&__age {
|
||||
border-radius: $controlCr;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: baseline;
|
||||
padding: 1px $interiorMarginSm;
|
||||
|
||||
&:before {
|
||||
opacity: 0.5;
|
||||
margin-right: $interiorMarginSm;
|
||||
}
|
||||
}
|
||||
|
||||
&--new {
|
||||
// New imagery
|
||||
$bgColor: $colorOk;
|
||||
background: rgba($bgColor, 0.5);
|
||||
@include flash($animName: flashImageAge, $dur: 250ms, $valStart: rgba($colorOk, 0.7), $valEnd: rgba($colorOk, 0));
|
||||
}
|
||||
|
||||
&__thumbs-wrapper {
|
||||
@@ -91,11 +146,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.s-image-main {
|
||||
background-color: $colorPlotBg;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
/*************************************** IMAGERY LOCAL CONTROLS*/
|
||||
.c-imagery {
|
||||
.h-local-controls--overlay-content {
|
||||
@@ -105,7 +155,7 @@
|
||||
background: $colorLocalControlOvrBg;
|
||||
border-radius: $basicCr;
|
||||
max-width: 200px;
|
||||
min-width: 100px;
|
||||
min-width: 70px;
|
||||
width: 35%;
|
||||
align-items: center;
|
||||
padding: $interiorMargin $interiorMarginLg;
|
||||
@@ -126,6 +176,7 @@
|
||||
&__lc {
|
||||
&__reset-btn {
|
||||
$bc: $scrollbarTrackColorBg;
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
border-right: 1px solid $bc;
|
||||
@@ -148,9 +199,51 @@
|
||||
}
|
||||
}
|
||||
|
||||
.c-image-controls {
|
||||
// Brightness/contrast
|
||||
|
||||
&__controls {
|
||||
// Sliders and reset element
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: $interiorMargin; // Need some extra space due to proximity to close button
|
||||
}
|
||||
|
||||
&__sliders {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
|
||||
> * + * {
|
||||
margin-top: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
&__slider-wrapper {
|
||||
// A wrapper is needed to add the type icon to left of each range input
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:before {
|
||||
color: rgba($colorMenuFg, 0.5);
|
||||
margin-right: $interiorMarginSm;
|
||||
}
|
||||
|
||||
input[type='range'] {
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
&__btn-reset {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
/*************************************** BUTTONS */
|
||||
.c-button.pause-play {
|
||||
// Pause icon set by default in markup
|
||||
justify-self: end;
|
||||
|
||||
&.is-paused {
|
||||
background: $colorPausedBg !important;
|
||||
color: $colorPausedFg;
|
||||
@@ -162,14 +255,13 @@
|
||||
}
|
||||
|
||||
.c-imagery__prev-next-buttons {
|
||||
//background: rgba(deeppink, 0.2);
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
transform: translateY(-75%);
|
||||
|
||||
.c-nav {
|
||||
pointer-events: all;
|
||||
|
||||
@@ -9,10 +9,11 @@
|
||||
</div>
|
||||
<SearchResults v-if="search.length"
|
||||
ref="searchResults"
|
||||
:results="getSearchResults()"
|
||||
:domain-object="internalDomainObject"
|
||||
:results="searchedEntries"
|
||||
@changeSectionPage="changeSelectedSection"
|
||||
@updateEntries="updateEntries"
|
||||
/>
|
||||
|
||||
<div v-if="!search.length"
|
||||
class="c-notebook__body"
|
||||
>
|
||||
@@ -105,15 +106,15 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import NotebookEntry from './notebook-entry.vue';
|
||||
import NotebookEntry from './NotebookEntry.vue';
|
||||
import Search from '@/ui/components/search.vue';
|
||||
import SearchResults from './search-results.vue';
|
||||
import Sidebar from './sidebar.vue';
|
||||
import SearchResults from './SearchResults.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';
|
||||
import { DEFAULT_CLASS, addNotebookEntry, createNewEmbed, getNotebookEntries } from '../utils/notebook-entries';
|
||||
import objectUtils from 'objectUtils';
|
||||
|
||||
const DEFAULT_CLASS = 'is-notebook-default';
|
||||
import { throttle } from 'lodash';
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject', 'snapshotContainer'],
|
||||
@@ -153,6 +154,9 @@ export default {
|
||||
pages() {
|
||||
return this.getPages() || [];
|
||||
},
|
||||
searchedEntries() {
|
||||
return this.getSearchResults();
|
||||
},
|
||||
sections() {
|
||||
return this.internalDomainObject.configuration.sections || [];
|
||||
},
|
||||
@@ -172,8 +176,6 @@ export default {
|
||||
return this.sections.find(section => section.isSelected);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
},
|
||||
beforeMount() {
|
||||
this.throttledSearchItem = throttle(this.searchItem, 500);
|
||||
},
|
||||
@@ -195,15 +197,6 @@ export default {
|
||||
});
|
||||
},
|
||||
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;
|
||||
@@ -259,7 +252,7 @@ export default {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
|
||||
const snapshotId = event.dataTransfer.getData('snapshot/id');
|
||||
const snapshotId = event.dataTransfer.getData('openmct/snapshot/id');
|
||||
if (snapshotId.length) {
|
||||
const snapshot = this.snapshotContainer.getSnapshot(snapshotId);
|
||||
this.newEntry(snapshot);
|
||||
@@ -440,11 +433,20 @@ export default {
|
||||
},
|
||||
async updateDefaultNotebook(notebookStorage) {
|
||||
const defaultNotebookObject = await this.getDefaultNotebookObject();
|
||||
this.removeDefaultClass(defaultNotebookObject);
|
||||
setDefaultNotebook(this.openmct, notebookStorage);
|
||||
this.addDefaultClass();
|
||||
this.defaultSectionId = notebookStorage.section.id;
|
||||
this.defaultPageId = notebookStorage.page.id;
|
||||
if (!defaultNotebookObject) {
|
||||
setDefaultNotebook(this.openmct, notebookStorage);
|
||||
} else if (objectUtils.makeKeyString(defaultNotebookObject.identifier) !== objectUtils.makeKeyString(notebookStorage.notebookMeta.identifier)) {
|
||||
this.removeDefaultClass(defaultNotebookObject);
|
||||
setDefaultNotebook(this.openmct, notebookStorage);
|
||||
}
|
||||
|
||||
if (this.defaultSectionId.length === 0 || this.defaultSectionId !== notebookStorage.section.id) {
|
||||
this.defaultSectionId = notebookStorage.section.id;
|
||||
}
|
||||
|
||||
if (this.defaultPageId.length === 0 || this.defaultPageId !== notebookStorage.page.id) {
|
||||
this.defaultPageId = notebookStorage.page.id;
|
||||
}
|
||||
},
|
||||
updateDefaultNotebookPage(pages, id) {
|
||||
if (!id) {
|
||||
@@ -17,7 +17,7 @@
|
||||
<div v-if="embed.snapshot"
|
||||
class="c-ne__embed__time"
|
||||
>
|
||||
{{ formatTime(embed.createdOn, 'YYYY-MM-DD HH:mm:ss') }}
|
||||
{{ createdOn }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -25,10 +25,10 @@
|
||||
|
||||
<script>
|
||||
import Moment from 'moment';
|
||||
import PopupMenu from './popup-menu.vue';
|
||||
import PopupMenu from './PopupMenu.vue';
|
||||
import PreviewAction from '../../../ui/preview/PreviewAction';
|
||||
import Painterro from 'painterro';
|
||||
import RemoveDialog from '../utils/removeDialog';
|
||||
import PainterroInstance from '../utils/painterroInstance';
|
||||
import SnapshotTemplate from './snapshot-template.html';
|
||||
import Vue from 'vue';
|
||||
|
||||
@@ -56,7 +56,10 @@ export default {
|
||||
popupMenuItems: []
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
computed: {
|
||||
createdOn() {
|
||||
return this.formatTime(this.embed.createdOn, 'YYYY-MM-DD HH:mm:ss');
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.addPopupMenuItems();
|
||||
@@ -78,95 +81,44 @@ export default {
|
||||
this.popupMenuItems = [removeEmbed, preview];
|
||||
},
|
||||
annotateSnapshot() {
|
||||
const self = this;
|
||||
|
||||
let save = false;
|
||||
let painterroInstance = {};
|
||||
const annotateVue = new Vue({
|
||||
template: '<div id="snap-annotation"></div>'
|
||||
});
|
||||
}).$mount();
|
||||
|
||||
let annotateOverlay = self.openmct.overlays.overlay({
|
||||
element: annotateVue.$mount().$el,
|
||||
const painterroInstance = new PainterroInstance(annotateVue.$el, this.updateSnapshot);
|
||||
const annotateOverlay = this.openmct.overlays.overlay({
|
||||
element: annotateVue.$el,
|
||||
size: 'large',
|
||||
dismissable: false,
|
||||
buttons: [
|
||||
{
|
||||
label: 'Cancel',
|
||||
callback: function () {
|
||||
save = false;
|
||||
painterroInstance.save();
|
||||
emphasis: true,
|
||||
callback: () => {
|
||||
painterroInstance.dismiss();
|
||||
annotateOverlay.dismiss();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Save',
|
||||
callback: function () {
|
||||
|
||||
save = true;
|
||||
callback: () => {
|
||||
painterroInstance.save();
|
||||
annotateOverlay.dismiss();
|
||||
this.snapshotOverlay.dismiss();
|
||||
this.openSnapshot();
|
||||
}
|
||||
}
|
||||
],
|
||||
onDestroy: function () {
|
||||
onDestroy: () => {
|
||||
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);
|
||||
painterroInstance.intialize();
|
||||
painterroInstance.show(this.embed.snapshot.src);
|
||||
},
|
||||
changeLocation() {
|
||||
const link = this.embed.historicLink;
|
||||
if (!link) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bounds = this.openmct.time.bounds();
|
||||
const isTimeBoundChanged = this.embed.bounds.start !== bounds.start
|
||||
@@ -191,7 +143,8 @@ export default {
|
||||
this.openmct.notifications.alert(message);
|
||||
}
|
||||
|
||||
window.location.href = link;
|
||||
const url = new URL(link);
|
||||
window.location.href = url.hash;
|
||||
},
|
||||
formatTime(unixTime, timeFormat) {
|
||||
return Moment.utc(unixTime).format(timeFormat);
|
||||
@@ -209,7 +162,8 @@ export default {
|
||||
this.snapshot = new Vue({
|
||||
data: () => {
|
||||
return {
|
||||
embed: self.embed
|
||||
createdOn: this.createdOn,
|
||||
embed: this.embed
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
@@ -218,13 +172,11 @@ export default {
|
||||
exportImage: self.exportImage
|
||||
},
|
||||
template: SnapshotTemplate
|
||||
});
|
||||
}).$mount();
|
||||
|
||||
const snapshotOverlay = this.openmct.overlays.overlay({
|
||||
element: this.snapshot.$mount().$el,
|
||||
onDestroy: () => {
|
||||
this.snapshot.$destroy(true);
|
||||
},
|
||||
this.snapshotOverlay = this.openmct.overlays.overlay({
|
||||
element: this.snapshot.$el,
|
||||
onDestroy: () => this.snapshot.$destroy(true),
|
||||
size: 'large',
|
||||
dismissable: true,
|
||||
buttons: [
|
||||
@@ -232,7 +184,7 @@ export default {
|
||||
label: 'Done',
|
||||
emphasis: true,
|
||||
callback: () => {
|
||||
snapshotOverlay.dismiss();
|
||||
this.snapshotOverlay.dismiss();
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -262,6 +214,10 @@ export default {
|
||||
},
|
||||
updateEmbed(embed) {
|
||||
this.$emit('updateEmbed', embed);
|
||||
},
|
||||
updateSnapshot(snapshotObject) {
|
||||
this.embed.snapshot = snapshotObject;
|
||||
this.updateEmbed(this.embed);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,23 +1,22 @@
|
||||
<template>
|
||||
<div class="c-notebook__entry c-ne has-local-controls"
|
||||
@dragover="dragover"
|
||||
@drop.capture="dropCapture"
|
||||
@drop.prevent="dropOnEntry(entry.id, $event)"
|
||||
@dragover="changeCursor"
|
||||
@drop.capture="cancelEditMode"
|
||||
@drop.prevent="dropOnEntry"
|
||||
>
|
||||
<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>
|
||||
<span>{{ createdOnDate }}</span>
|
||||
<span>{{ createdOnTime }}</span>
|
||||
</div>
|
||||
<div class="c-ne__content">
|
||||
<div :id="entry.id"
|
||||
class="c-ne__text"
|
||||
:class="{'c-input-inline' : !readOnly }"
|
||||
:class="{'c-ne__input' : !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>
|
||||
@blur="updateEntryValue($event, entry.id)"
|
||||
@focus="updateCurrentEntryValue($event, entry.id)"
|
||||
>{{ entry.text }}</div>
|
||||
<div class="c-snapshots c-ne__embeds">
|
||||
<NotebookEmbed v-for="embed in entry.embeds"
|
||||
:key="embed.id"
|
||||
@@ -57,7 +56,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import NotebookEmbed from './notebook-embed.vue';
|
||||
import NotebookEmbed from './NotebookEmbed.vue';
|
||||
import { createNewEmbed, getEntryPosById, getNotebookEntries } from '../utils/notebook-entries';
|
||||
import Moment from 'moment';
|
||||
|
||||
@@ -106,37 +105,35 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentEntryValue: '',
|
||||
defaultEntryStyle: {
|
||||
fontStyle: 'italic',
|
||||
color: '#6e6e6e'
|
||||
},
|
||||
defaultText: 'add description'
|
||||
currentEntryValue: ''
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
entry() {
|
||||
computed: {
|
||||
createdOnDate() {
|
||||
return this.formatTime(this.entry.createdOn, 'YYYY-MM-DD');
|
||||
},
|
||||
readOnly(readOnly) {
|
||||
},
|
||||
selectedSection(selectedSection) {
|
||||
},
|
||||
selectedPage(selectedSection) {
|
||||
createdOnTime() {
|
||||
return this.formatTime(this.entry.createdOn, 'HH:mm:ss');
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.updateEntries = this.updateEntries.bind(this);
|
||||
},
|
||||
beforeDestory() {
|
||||
this.dropOnEntry = this.dropOnEntry.bind(this);
|
||||
},
|
||||
methods: {
|
||||
cancelEditMode(event) {
|
||||
const isEditing = this.openmct.editor.isEditing();
|
||||
if (isEditing) {
|
||||
this.openmct.editor.cancel();
|
||||
}
|
||||
},
|
||||
changeCursor() {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = "copy";
|
||||
},
|
||||
deleteEntry() {
|
||||
const self = this;
|
||||
if (!self.domainObject || !self.selectedSection || !self.selectedPage || !self.entry.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entryPosById = this.entryPosById(this.entry.id);
|
||||
const entryPosById = self.entryPosById(self.entry.id);
|
||||
if (entryPosById === -1) {
|
||||
return;
|
||||
}
|
||||
@@ -151,7 +148,7 @@ export default {
|
||||
callback: () => {
|
||||
const entries = getNotebookEntries(self.domainObject, self.selectedSection, self.selectedPage);
|
||||
entries.splice(entryPosById, 1);
|
||||
this.updateEntries(entries);
|
||||
self.updateEntries(entries);
|
||||
dialog.dismiss();
|
||||
}
|
||||
},
|
||||
@@ -164,24 +161,10 @@ export default {
|
||||
]
|
||||
});
|
||||
},
|
||||
dragover() {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = "copy";
|
||||
},
|
||||
dropCapture(event) {
|
||||
const isEditing = this.openmct.editor.isEditing();
|
||||
if (isEditing) {
|
||||
this.openmct.editor.cancel();
|
||||
}
|
||||
},
|
||||
dropOnEntry(entryId, $event) {
|
||||
dropOnEntry($event) {
|
||||
event.stopImmediatePropagation();
|
||||
|
||||
if (!this.domainObject || !this.selectedSection || !this.selectedPage) {
|
||||
return;
|
||||
}
|
||||
|
||||
const snapshotId = $event.dataTransfer.getData('snapshot/id');
|
||||
const snapshotId = $event.dataTransfer.getData('openmct/snapshot/id');
|
||||
if (snapshotId.length) {
|
||||
this.moveSnapshot(snapshotId);
|
||||
|
||||
@@ -190,7 +173,7 @@ export default {
|
||||
|
||||
const data = $event.dataTransfer.getData('openmct/domain-object-path');
|
||||
const objectPath = JSON.parse(data);
|
||||
const entryPos = this.entryPosById(entryId);
|
||||
const entryPos = this.entryPosById(this.entry.id);
|
||||
const bounds = this.openmct.time.bounds();
|
||||
const snapshotMeta = {
|
||||
bounds,
|
||||
@@ -246,14 +229,40 @@ export default {
|
||||
this.entry.embeds.splice(embedPosition, 1);
|
||||
this.updateEntry(this.entry);
|
||||
},
|
||||
selectTextInsideElement(element) {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(element);
|
||||
let selection = window.getSelection();
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
updateCurrentEntryValue($event) {
|
||||
if (this.readOnly) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = $event.target;
|
||||
this.currentEntryValue = target ? target.textContent : '';
|
||||
},
|
||||
textBlur($event, entryId) {
|
||||
updateEmbed(newEmbed) {
|
||||
this.entry.embeds.some(e => {
|
||||
const found = (e.id === newEmbed.id);
|
||||
if (found) {
|
||||
e = newEmbed;
|
||||
}
|
||||
|
||||
return found;
|
||||
});
|
||||
|
||||
this.updateEntry(this.entry);
|
||||
},
|
||||
updateEntry(newEntry) {
|
||||
const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
|
||||
entries.some(entry => {
|
||||
const found = (entry.id === newEntry.id);
|
||||
if (found) {
|
||||
entry = newEntry;
|
||||
}
|
||||
|
||||
return found;
|
||||
});
|
||||
|
||||
this.updateEntries(entries);
|
||||
},
|
||||
updateEntryValue($event, entryId) {
|
||||
if (!this.domainObject || !this.selectedSection || !this.selectedPage) {
|
||||
return;
|
||||
}
|
||||
@@ -266,48 +275,14 @@ export default {
|
||||
const entryPos = this.entryPosById(entryId);
|
||||
const value = target.textContent.trim();
|
||||
if (this.currentEntryValue !== value) {
|
||||
target.textContent = 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);
|
||||
}
|
||||
@@ -111,7 +111,7 @@ export default {
|
||||
|
||||
const bounds = this.openmct.time.bounds();
|
||||
const link = !this.ignoreLink
|
||||
? window.location.href
|
||||
? window.location.hash
|
||||
: null;
|
||||
|
||||
const objectPath = this.objectPath || this.openmct.router.path;
|
||||
@@ -49,8 +49,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import NotebookEmbed from './notebook-embed.vue';
|
||||
import PopupMenu from './popup-menu.vue';
|
||||
import NotebookEmbed from './NotebookEmbed.vue';
|
||||
import PopupMenu from './PopupMenu.vue';
|
||||
import RemoveDialog from '../utils/removeDialog';
|
||||
import { NOTEBOOK_SNAPSHOT_MAX_COUNT } from '../snapshot-container';
|
||||
import { EVENT_SNAPSHOTS_UPDATED } from '../notebook-constants';
|
||||
@@ -81,8 +81,6 @@ export default {
|
||||
this.snapshotContainer.on(EVENT_SNAPSHOTS_UPDATED, this.snapshotsUpdated);
|
||||
this.snapshots = this.snapshotContainer.getSnapshots();
|
||||
},
|
||||
beforeDestory() {
|
||||
},
|
||||
methods: {
|
||||
addPopupMenuItems() {
|
||||
const removeSnapshot = {
|
||||
@@ -122,7 +120,7 @@ export default {
|
||||
},
|
||||
startEmbedDrag(snapshot, event) {
|
||||
event.dataTransfer.setData('text/plain', snapshot.id);
|
||||
event.dataTransfer.setData('snapshot/id', snapshot.id);
|
||||
event.dataTransfer.setData('openmct/snapshot/id', snapshot.id);
|
||||
},
|
||||
updateSnapshot(snapshot) {
|
||||
this.snapshotContainer.updateSnapshot(snapshot);
|
||||
@@ -18,7 +18,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SnapshotContainerComponent from './notebook-snapshot-container.vue';
|
||||
import SnapshotContainerComponent from './NotebookSnapshotContainer.vue';
|
||||
import { EVENT_SNAPSHOTS_UPDATED } from '../notebook-constants';
|
||||
import { NOTEBOOK_SNAPSHOT_MAX_COUNT } from '../snapshot-container';
|
||||
import Vue from 'vue';
|
||||
@@ -19,7 +19,7 @@
|
||||
<script>
|
||||
import { deleteNotebookEntries } from '../utils/notebook-entries';
|
||||
import { getDefaultNotebook } from '../utils/notebook-storage';
|
||||
import Page from './page-component.vue';
|
||||
import Page from './PageComponent.vue';
|
||||
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
@@ -70,12 +70,6 @@ export default {
|
||||
return {
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
},
|
||||
mounted() {
|
||||
},
|
||||
destroyed() {
|
||||
},
|
||||
methods: {
|
||||
deletePage(id) {
|
||||
const selectedSection = this.sections.find(s => s.isSelected);
|
||||
@@ -14,7 +14,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PopupMenu from './popup-menu.vue';
|
||||
import PopupMenu from './PopupMenu.vue';
|
||||
import RemoveDialog from '../utils/removeDialog';
|
||||
|
||||
export default {
|
||||
@@ -55,8 +55,6 @@ export default {
|
||||
this.addPopupMenuItems();
|
||||
this.toggleContentEditable();
|
||||
},
|
||||
destroyed() {
|
||||
},
|
||||
methods: {
|
||||
addPopupMenuItems() {
|
||||
const removePage = {
|
||||
@@ -8,7 +8,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MenuItems from './menu-items.vue';
|
||||
import MenuItems from './MenuItems.vue';
|
||||
import Vue from 'vue';
|
||||
|
||||
export default {
|
||||
@@ -4,26 +4,34 @@
|
||||
<div class="c-notebook__entries">
|
||||
<NotebookEntry v-for="(result, index) in results"
|
||||
:key="index"
|
||||
:domain-object="domainObject"
|
||||
:result="result"
|
||||
:entry="result.entry"
|
||||
:read-only="true"
|
||||
:selected-page="null"
|
||||
:selected-section="null"
|
||||
:selected-page="result.page"
|
||||
:selected-section="result.section"
|
||||
@changeSectionPage="changeSectionPage"
|
||||
@updateEntries="updateEntries"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import NotebookEntry from './notebook-entry.vue';
|
||||
import NotebookEntry from './NotebookEntry.vue';
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject'],
|
||||
inject: ['openmct', 'snapshotContainer'],
|
||||
components: {
|
||||
NotebookEntry
|
||||
},
|
||||
props: {
|
||||
domainObject: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
results: {
|
||||
type: Array,
|
||||
default() {
|
||||
@@ -31,19 +39,12 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
watch: {
|
||||
results(newResults) {}
|
||||
},
|
||||
destroyed() {
|
||||
},
|
||||
mounted() {
|
||||
},
|
||||
methods: {
|
||||
changeSectionPage(data) {
|
||||
this.$emit('changeSectionPage', data);
|
||||
},
|
||||
updateEntries(entries) {
|
||||
this.$emit('updateEntries', entries);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -19,7 +19,7 @@
|
||||
<script>
|
||||
import { deleteNotebookEntries } from '../utils/notebook-entries';
|
||||
import { getDefaultNotebook } from '../utils/notebook-storage';
|
||||
import sectionComponent from './section-component.vue';
|
||||
import sectionComponent from './SectionComponent.vue';
|
||||
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
@@ -57,12 +57,6 @@ export default {
|
||||
return {
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
},
|
||||
mounted() {
|
||||
},
|
||||
destroyed() {
|
||||
},
|
||||
methods: {
|
||||
deleteSection(id) {
|
||||
const section = this.sections.find(s => s.id === id);
|
||||
@@ -17,7 +17,7 @@
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import PopupMenu from './popup-menu.vue';
|
||||
import PopupMenu from './PopupMenu.vue';
|
||||
import RemoveDialog from '../utils/removeDialog';
|
||||
|
||||
export default {
|
||||
@@ -58,8 +58,6 @@ export default {
|
||||
this.addPopupMenuItems();
|
||||
this.toggleContentEditable();
|
||||
},
|
||||
destroyed() {
|
||||
},
|
||||
methods: {
|
||||
addPopupMenuItems() {
|
||||
const removeSection = {
|
||||
@@ -56,8 +56,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SectionCollection from './section-collection.vue';
|
||||
import PageCollection from './page-collection.vue';
|
||||
import SectionCollection from './SectionCollection.vue';
|
||||
import PageCollection from './PageCollection.vue';
|
||||
import uuid from 'uuid';
|
||||
|
||||
export default {
|
||||
@@ -139,8 +139,6 @@ export default {
|
||||
this.addSection();
|
||||
}
|
||||
},
|
||||
destroyed() {
|
||||
},
|
||||
methods: {
|
||||
addPage() {
|
||||
const pageTitle = this.pageTitle;
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
<div class="l-browse-bar__end">
|
||||
<div class="l-browse-bar__snapshot-datetime">
|
||||
SNAPSHOT {{formatTime(embed.createdOn, 'YYYY-MM-DD HH:mm:ss')}}
|
||||
SNAPSHOT {{ createdOn }}
|
||||
</div>
|
||||
<span class="c-button-set c-button-set--strip-h">
|
||||
<button
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Notebook from './components/notebook.vue';
|
||||
import NotebookSnapshotIndicator from './components/notebook-snapshot-indicator.vue';
|
||||
import Notebook from './components/Notebook.vue';
|
||||
import NotebookSnapshotIndicator from './components/NotebookSnapshotIndicator.vue';
|
||||
import SnapshotContainer from './snapshot-container';
|
||||
import Vue from 'vue';
|
||||
|
||||
@@ -95,7 +95,8 @@ export default function NotebookPlugin() {
|
||||
template: '<NotebookSnapshotIndicator></NotebookSnapshotIndicator>'
|
||||
});
|
||||
const indicator = {
|
||||
element: notebookSnapshotIndicator.$mount().$el
|
||||
element: notebookSnapshotIndicator.$mount().$el,
|
||||
key: 'notebook-snapshot-indicator'
|
||||
};
|
||||
|
||||
openmct.indicators.add(indicator);
|
||||
|
||||
222
src/plugins/notebook/pluginSpec.js
Normal file
222
src/plugins/notebook/pluginSpec.js
Normal file
@@ -0,0 +1,222 @@
|
||||
/*****************************************************************************
|
||||
* 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 { createOpenMct, createMouseEvent, resetApplicationState } from 'utils/testing';
|
||||
import NotebookPlugin from './plugin';
|
||||
import Vue from 'vue';
|
||||
|
||||
let openmct;
|
||||
let notebookDefinition;
|
||||
let notebookPlugin;
|
||||
let element;
|
||||
let child;
|
||||
let appHolder;
|
||||
|
||||
const notebookDomainObject = {
|
||||
identifier: {
|
||||
key: 'notebook',
|
||||
namespace: ''
|
||||
},
|
||||
type: 'notebook'
|
||||
};
|
||||
|
||||
describe("Notebook plugin:", () => {
|
||||
beforeAll(done => {
|
||||
appHolder = document.createElement('div');
|
||||
appHolder.style.width = '640px';
|
||||
appHolder.style.height = '480px';
|
||||
|
||||
openmct = createOpenMct();
|
||||
|
||||
element = document.createElement('div');
|
||||
child = document.createElement('div');
|
||||
element.appendChild(child);
|
||||
|
||||
notebookPlugin = new NotebookPlugin();
|
||||
openmct.install(notebookPlugin);
|
||||
|
||||
notebookDefinition = openmct.types.get('notebook').definition;
|
||||
notebookDefinition.initialize(notebookDomainObject);
|
||||
|
||||
openmct.on('start', done);
|
||||
openmct.start(appHolder);
|
||||
|
||||
document.body.append(appHolder);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
appHolder.remove();
|
||||
resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
it("has type as Notebook", () => {
|
||||
expect(notebookDefinition.name).toEqual('Notebook');
|
||||
});
|
||||
|
||||
it("is creatable", () => {
|
||||
expect(notebookDefinition.creatable).toEqual(true);
|
||||
});
|
||||
|
||||
describe("Notebook view:", () => {
|
||||
let notebookViewProvider;
|
||||
let notebookView;
|
||||
|
||||
beforeEach(() => {
|
||||
const notebookViewObject = {
|
||||
...notebookDomainObject,
|
||||
id: "test-object",
|
||||
name: 'Notebook',
|
||||
configuration: {
|
||||
defaultSort: 'oldest',
|
||||
entries: {},
|
||||
pageTitle: 'Page',
|
||||
sections: [],
|
||||
sectionTitle: 'Section',
|
||||
type: 'General'
|
||||
}
|
||||
};
|
||||
|
||||
const notebookObject = {
|
||||
name: 'Notebook View',
|
||||
key: 'notebook-vue',
|
||||
creatable: true
|
||||
};
|
||||
|
||||
const applicableViews = openmct.objectViews.get(notebookViewObject);
|
||||
notebookViewProvider = applicableViews.find(viewProvider => viewProvider.key === notebookObject.key);
|
||||
notebookView = notebookViewProvider.view(notebookViewObject);
|
||||
|
||||
notebookView.show(child);
|
||||
|
||||
return Vue.nextTick();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
notebookView.destroy();
|
||||
});
|
||||
|
||||
it("provides notebook view", () => {
|
||||
expect(notebookViewProvider).toBeDefined();
|
||||
});
|
||||
|
||||
it("renders notebook element", () => {
|
||||
const notebookElement = element.querySelectorAll('.c-notebook');
|
||||
expect(notebookElement.length).toBe(1);
|
||||
});
|
||||
|
||||
it("renders major elements", () => {
|
||||
const notebookElement = element.querySelector('.c-notebook');
|
||||
const searchElement = notebookElement.querySelector('.c-search');
|
||||
const sidebarElement = notebookElement.querySelector('.c-sidebar');
|
||||
const pageViewElement = notebookElement.querySelector('.c-notebook__page-view');
|
||||
const hasMajorElements = Boolean(searchElement && sidebarElement && pageViewElement);
|
||||
|
||||
expect(hasMajorElements).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Notebook Snapshots view:", () => {
|
||||
let snapshotIndicator;
|
||||
let drawerElement;
|
||||
|
||||
function clickSnapshotIndicator() {
|
||||
const indicator = element.querySelector('.icon-notebook');
|
||||
const button = indicator.querySelector('button');
|
||||
const clickEvent = createMouseEvent('click');
|
||||
|
||||
button.dispatchEvent(clickEvent);
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
snapshotIndicator = openmct.indicators.indicatorObjects
|
||||
.find(indicator => indicator.key === 'notebook-snapshot-indicator').element;
|
||||
|
||||
element.append(snapshotIndicator);
|
||||
|
||||
return Vue.nextTick();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
snapshotIndicator.remove();
|
||||
if (drawerElement) {
|
||||
drawerElement.remove();
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
drawerElement = document.querySelector('.l-shell__drawer');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (drawerElement) {
|
||||
drawerElement.classList.remove('is-expanded');
|
||||
}
|
||||
});
|
||||
|
||||
it("has Snapshots indicator", () => {
|
||||
const hasSnapshotIndicator = snapshotIndicator !== null && snapshotIndicator !== undefined;
|
||||
expect(hasSnapshotIndicator).toBe(true);
|
||||
});
|
||||
|
||||
it("snapshots container has class isExpanded", () => {
|
||||
let classes = drawerElement.classList;
|
||||
const isExpandedBefore = classes.contains('is-expanded');
|
||||
|
||||
clickSnapshotIndicator();
|
||||
classes = drawerElement.classList;
|
||||
const isExpandedAfterFirstClick = classes.contains('is-expanded');
|
||||
|
||||
const success = isExpandedBefore === false
|
||||
&& isExpandedAfterFirstClick === true;
|
||||
|
||||
expect(success).toBe(true);
|
||||
});
|
||||
|
||||
it("snapshots container does not have class isExpanded", () => {
|
||||
let classes = drawerElement.classList;
|
||||
const isExpandedBefore = classes.contains('is-expanded');
|
||||
|
||||
clickSnapshotIndicator();
|
||||
classes = drawerElement.classList;
|
||||
const isExpandedAfterFirstClick = classes.contains('is-expanded');
|
||||
|
||||
clickSnapshotIndicator();
|
||||
classes = drawerElement.classList;
|
||||
const isExpandedAfterSecondClick = classes.contains('is-expanded');
|
||||
|
||||
const success = isExpandedBefore === false
|
||||
&& isExpandedAfterFirstClick === true
|
||||
&& isExpandedAfterSecondClick === false;
|
||||
|
||||
expect(success).toBe(true);
|
||||
});
|
||||
|
||||
it("show notebook snapshots container text", () => {
|
||||
clickSnapshotIndicator();
|
||||
|
||||
const notebookSnapshots = drawerElement.querySelector('.l-browse-bar__object-name');
|
||||
const snapshotsText = notebookSnapshots.textContent.trim();
|
||||
|
||||
expect(snapshotsText).toBe('Notebook Snapshots');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import objectLink from '../../../ui/mixins/object-link';
|
||||
|
||||
export const DEFAULT_CLASS = 'is-notebook-default';
|
||||
const TIME_BOUNDS = {
|
||||
START_BOUND: 'tc.startBound',
|
||||
END_BOUND: 'tc.endBound',
|
||||
@@ -128,6 +129,7 @@ export function addNotebookEntry(openmct, domainObject, notebookStorage, embed =
|
||||
embeds
|
||||
});
|
||||
|
||||
addDefaultClass(domainObject);
|
||||
openmct.objects.mutate(domainObject, 'configuration.entries', entries);
|
||||
|
||||
return id;
|
||||
@@ -193,5 +195,15 @@ export function deleteNotebookEntries(openmct, domainObject, selectedSection, se
|
||||
}
|
||||
|
||||
delete entries[selectedSection.id][selectedPage.id];
|
||||
|
||||
openmct.objects.mutate(domainObject, 'configuration.entries', entries);
|
||||
}
|
||||
|
||||
function addDefaultClass(domainObject) {
|
||||
const classList = domainObject.classList || [];
|
||||
if (classList.includes(DEFAULT_CLASS)) {
|
||||
return;
|
||||
}
|
||||
|
||||
classList.push(DEFAULT_CLASS);
|
||||
}
|
||||
|
||||
195
src/plugins/notebook/utils/notebook-entriesSpec.js
Normal file
195
src/plugins/notebook/utils/notebook-entriesSpec.js
Normal file
@@ -0,0 +1,195 @@
|
||||
/*****************************************************************************
|
||||
* 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 * as NotebookEntries from './notebook-entries';
|
||||
import { createOpenMct, spyOnBuiltins, resetApplicationState } from 'utils/testing';
|
||||
|
||||
const notebookStorage = {
|
||||
domainObject: {
|
||||
name: 'notebook',
|
||||
identifier: {
|
||||
namespace: '',
|
||||
key: 'test-notebook'
|
||||
}
|
||||
},
|
||||
notebookMeta: {
|
||||
name: 'notebook',
|
||||
identifier: {
|
||||
namespace: '',
|
||||
key: 'test-notebook'
|
||||
}
|
||||
},
|
||||
section: {
|
||||
id: '03a79b6a-971c-4e56-9892-ec536332c3f0',
|
||||
isDefault: true,
|
||||
isSelected: true,
|
||||
name: 'section',
|
||||
pages: [],
|
||||
sectionTitle: 'Section'
|
||||
},
|
||||
page: {
|
||||
id: '8b548fd9-2b8a-4b02-93a9-4138e22eba00',
|
||||
isDefault: true,
|
||||
isSelected: true,
|
||||
name: 'page',
|
||||
pageTitle: 'Page'
|
||||
}
|
||||
};
|
||||
|
||||
const notebookEntries = {
|
||||
'03a79b6a-971c-4e56-9892-ec536332c3f0': {
|
||||
'8b548fd9-2b8a-4b02-93a9-4138e22eba00': []
|
||||
}
|
||||
};
|
||||
|
||||
const notebookDomainObject = {
|
||||
identifier: {
|
||||
key: 'notebook',
|
||||
namespace: ''
|
||||
},
|
||||
type: 'notebook',
|
||||
configuration: {
|
||||
defaultSort: 'oldest',
|
||||
entries: notebookEntries,
|
||||
pageTitle: 'Page',
|
||||
sections: [],
|
||||
sectionTitle: 'Section',
|
||||
type: 'General'
|
||||
}
|
||||
};
|
||||
|
||||
const selectedSection = {
|
||||
id: '03a79b6a-971c-4e56-9892-ec536332c3f0',
|
||||
isDefault: false,
|
||||
isSelected: true,
|
||||
name: 'Day 1',
|
||||
pages: [
|
||||
{
|
||||
id: '54deb3d5-8267-4be4-95e9-3579ed8c082d',
|
||||
isDefault: false,
|
||||
isSelected: false,
|
||||
name: 'Shift 1',
|
||||
pageTitle: 'Page'
|
||||
},
|
||||
{
|
||||
id: '2ea41c78-8e60-4657-a350-53f1a1fa3021',
|
||||
isDefault: false,
|
||||
isSelected: false,
|
||||
name: 'Shift 2',
|
||||
pageTitle: 'Page'
|
||||
},
|
||||
{
|
||||
id: '8b548fd9-2b8a-4b02-93a9-4138e22eba00',
|
||||
isDefault: false,
|
||||
isSelected: true,
|
||||
name: 'Unnamed Page',
|
||||
pageTitle: 'Page'
|
||||
}
|
||||
],
|
||||
sectionTitle: 'Section'
|
||||
};
|
||||
|
||||
const selectedPage = {
|
||||
id: '8b548fd9-2b8a-4b02-93a9-4138e22eba00',
|
||||
isDefault: false,
|
||||
isSelected: true,
|
||||
name: 'Unnamed Page',
|
||||
pageTitle: 'Page'
|
||||
};
|
||||
|
||||
let openmct;
|
||||
|
||||
describe('Notebook Entries:', () => {
|
||||
beforeEach(done => {
|
||||
openmct = createOpenMct();
|
||||
window.localStorage.setItem('notebook-storage', null);
|
||||
spyOnBuiltins(['mutate'], openmct.objects);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
notebookDomainObject.configuration.entries[selectedSection.id][selectedPage.id] = [];
|
||||
resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
it('getNotebookEntries has no entries', () => {
|
||||
const entries = NotebookEntries.getNotebookEntries(notebookDomainObject, selectedSection, selectedPage);
|
||||
|
||||
expect(entries.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('addNotebookEntry mutates object', () => {
|
||||
NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage);
|
||||
|
||||
expect(openmct.objects.mutate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('addNotebookEntry adds entry', () => {
|
||||
NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage);
|
||||
const entries = NotebookEntries.getNotebookEntries(notebookDomainObject, selectedSection, selectedPage);
|
||||
|
||||
expect(entries.length).toEqual(1);
|
||||
});
|
||||
|
||||
it('getEntryPosById returns valid position', () => {
|
||||
const entryId = NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage);
|
||||
const position = NotebookEntries.getEntryPosById(entryId, notebookDomainObject, selectedSection, selectedPage);
|
||||
|
||||
expect(position).toEqual(0);
|
||||
});
|
||||
|
||||
it('getEntryPosById returns valid position', () => {
|
||||
const entryId1 = NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage);
|
||||
const position1 = NotebookEntries.getEntryPosById(entryId1, notebookDomainObject, selectedSection, selectedPage);
|
||||
|
||||
const entryId2 = NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage);
|
||||
const position2 = NotebookEntries.getEntryPosById(entryId2, notebookDomainObject, selectedSection, selectedPage);
|
||||
|
||||
const entryId3 = NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage);
|
||||
const position3 = NotebookEntries.getEntryPosById(entryId3, notebookDomainObject, selectedSection, selectedPage);
|
||||
|
||||
const success = position1 === 0
|
||||
&& position2 === 1
|
||||
&& position3 === 2;
|
||||
|
||||
expect(success).toBe(true);
|
||||
});
|
||||
|
||||
it('deleteNotebookEntries mutates object', () => {
|
||||
openmct.objects.mutate.calls.reset();
|
||||
|
||||
NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage);
|
||||
NotebookEntries.deleteNotebookEntries(openmct, notebookDomainObject, selectedSection, selectedPage);
|
||||
|
||||
expect(openmct.objects.mutate).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('deleteNotebookEntries deletes correct entry', () => {
|
||||
NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage);
|
||||
NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage);
|
||||
|
||||
NotebookEntries.deleteNotebookEntries(openmct, notebookDomainObject, selectedSection, selectedPage);
|
||||
const afterEntries = NotebookEntries.getNotebookEntries(notebookDomainObject, selectedSection, selectedPage);
|
||||
|
||||
expect(afterEntries).toEqual(null);
|
||||
});
|
||||
});
|
||||
@@ -60,7 +60,6 @@ export function setDefaultNotebookSection(section) {
|
||||
|
||||
notebookStorage.section = section;
|
||||
saveDefaultNotebook(notebookStorage);
|
||||
|
||||
}
|
||||
|
||||
export function setDefaultNotebookPage(page) {
|
||||
|
||||
125
src/plugins/notebook/utils/notebook-storageSpec.js
Normal file
125
src/plugins/notebook/utils/notebook-storageSpec.js
Normal file
@@ -0,0 +1,125 @@
|
||||
/*****************************************************************************
|
||||
* 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 * as NotebookStorage from './notebook-storage';
|
||||
import { createOpenMct, resetApplicationState } from 'utils/testing';
|
||||
|
||||
const notebookStorage = {
|
||||
domainObject: {
|
||||
name: 'notebook',
|
||||
identifier: {
|
||||
namespace: '',
|
||||
key: 'test-notebook'
|
||||
}
|
||||
},
|
||||
notebookMeta: {
|
||||
name: 'notebook',
|
||||
identifier: {
|
||||
namespace: '',
|
||||
key: 'test-notebook'
|
||||
}
|
||||
},
|
||||
section: {
|
||||
id: 'temp-section',
|
||||
isDefault: false,
|
||||
isSelected: true,
|
||||
name: 'section',
|
||||
pages: [],
|
||||
sectionTitle: 'Section'
|
||||
},
|
||||
page: {
|
||||
id: 'temp-page',
|
||||
isDefault: false,
|
||||
isSelected: true,
|
||||
name: 'page',
|
||||
pageTitle: 'Page'
|
||||
}
|
||||
};
|
||||
|
||||
let openmct = createOpenMct();
|
||||
|
||||
describe('Notebook Storage:', () => {
|
||||
beforeEach((done) => {
|
||||
openmct = createOpenMct();
|
||||
window.localStorage.setItem('notebook-storage', null);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
it('has empty local Storage', () => {
|
||||
expect(window.localStorage).not.toBeNull();
|
||||
});
|
||||
|
||||
it('has null notebookstorage on clearDefaultNotebook', () => {
|
||||
window.localStorage.setItem('notebook-storage', notebookStorage);
|
||||
NotebookStorage.clearDefaultNotebook();
|
||||
const defaultNotebook = NotebookStorage.getDefaultNotebook();
|
||||
|
||||
expect(defaultNotebook).toBeNull();
|
||||
});
|
||||
|
||||
it('has correct notebookstorage on setDefaultNotebook', () => {
|
||||
NotebookStorage.setDefaultNotebook(openmct, notebookStorage);
|
||||
const defaultNotebook = NotebookStorage.getDefaultNotebook();
|
||||
|
||||
expect(JSON.stringify(defaultNotebook)).toBe(JSON.stringify(notebookStorage));
|
||||
});
|
||||
|
||||
it('has correct section on setDefaultNotebookSection', () => {
|
||||
const section = {
|
||||
id: 'new-temp-section',
|
||||
isDefault: true,
|
||||
isSelected: true,
|
||||
name: 'new section',
|
||||
pages: [],
|
||||
sectionTitle: 'Section'
|
||||
};
|
||||
|
||||
NotebookStorage.setDefaultNotebook(openmct, notebookStorage);
|
||||
NotebookStorage.setDefaultNotebookSection(section);
|
||||
|
||||
const defaultNotebook = NotebookStorage.getDefaultNotebook();
|
||||
const newSection = defaultNotebook.section;
|
||||
expect(JSON.stringify(section)).toBe(JSON.stringify(newSection));
|
||||
});
|
||||
|
||||
it('has correct page on setDefaultNotebookPage', () => {
|
||||
const page = {
|
||||
id: 'new-temp-page',
|
||||
isDefault: true,
|
||||
isSelected: true,
|
||||
name: 'new page',
|
||||
pageTitle: 'Page'
|
||||
};
|
||||
|
||||
NotebookStorage.setDefaultNotebook(openmct, notebookStorage);
|
||||
NotebookStorage.setDefaultNotebookPage(page);
|
||||
|
||||
const defaultNotebook = NotebookStorage.getDefaultNotebook();
|
||||
const newPage = defaultNotebook.page;
|
||||
expect(JSON.stringify(page)).toBe(JSON.stringify(newPage));
|
||||
});
|
||||
});
|
||||
79
src/plugins/notebook/utils/painterroInstance.js
Normal file
79
src/plugins/notebook/utils/painterroInstance.js
Normal file
@@ -0,0 +1,79 @@
|
||||
import Painterro from 'painterro';
|
||||
|
||||
const DEFAULT_CONFIG = {
|
||||
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'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default class PainterroInstance {
|
||||
constructor(element, saveCallback) {
|
||||
this.elementId = element.id;
|
||||
this.isSave = false;
|
||||
this.painterroInstance = null;
|
||||
this.saveCallback = saveCallback;
|
||||
}
|
||||
|
||||
dismiss() {
|
||||
this.isSave = false;
|
||||
this.painterroInstance.save();
|
||||
}
|
||||
|
||||
intialize() {
|
||||
this.config = Object.assign({}, DEFAULT_CONFIG);
|
||||
|
||||
this.config.id = this.elementId;
|
||||
this.config.saveHandler = this.saveHandler.bind(this);
|
||||
|
||||
this.painterro = Painterro(this.config);
|
||||
}
|
||||
|
||||
save() {
|
||||
this.isSave = true;
|
||||
this.painterroInstance.save();
|
||||
}
|
||||
|
||||
saveHandler(image, done) {
|
||||
if (this.isSave) {
|
||||
const self = this;
|
||||
const url = image.asBlob();
|
||||
const reader = new window.FileReader();
|
||||
reader.readAsDataURL(url);
|
||||
reader.onloadend = () => {
|
||||
const snapshot = reader.result;
|
||||
const snapshotObject = {
|
||||
src: snapshot,
|
||||
type: url.type,
|
||||
size: url.size,
|
||||
modified: Date.now()
|
||||
};
|
||||
|
||||
self.saveCallback(snapshotObject);
|
||||
};
|
||||
}
|
||||
|
||||
done(true);
|
||||
}
|
||||
|
||||
show(src) {
|
||||
this.painterroInstance = this.painterro.show(src);
|
||||
}
|
||||
}
|
||||
@@ -86,7 +86,10 @@ export default class CouchObjectProvider {
|
||||
this.objectQueue[key] = new CouchObjectQueue(undefined, response[REV]);
|
||||
}
|
||||
|
||||
this.objectQueue[key].updateRevision(response[REV]);
|
||||
//Sometimes CouchDB returns the old rev which fetching the object if there is a document update in progress
|
||||
if (!this.objectQueue[key].pending) {
|
||||
this.objectQueue[key].updateRevision(response[REV]);
|
||||
}
|
||||
|
||||
return object;
|
||||
} else {
|
||||
|
||||
@@ -22,9 +22,10 @@
|
||||
|
||||
import CouchObjectProvider from './CouchObjectProvider';
|
||||
const NAMESPACE = '';
|
||||
const PERSISTENCE_SPACE = 'mct';
|
||||
|
||||
export default function CouchPlugin(url) {
|
||||
return function install(openmct) {
|
||||
openmct.objects.addProvider(NAMESPACE, new CouchObjectProvider(openmct, url, NAMESPACE));
|
||||
openmct.objects.addProvider(PERSISTENCE_SPACE, new CouchObjectProvider(openmct, url, NAMESPACE));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -31,19 +31,18 @@ describe('the plugin', () => {
|
||||
let element;
|
||||
let child;
|
||||
let provider;
|
||||
let testSpace = 'testSpace';
|
||||
let testPath = '/test/db';
|
||||
let mockDomainObject;
|
||||
|
||||
beforeEach((done) => {
|
||||
mockDomainObject = {
|
||||
identifier: {
|
||||
namespace: '',
|
||||
namespace: 'mct',
|
||||
key: 'some-value'
|
||||
}
|
||||
};
|
||||
openmct = createOpenMct(false);
|
||||
openmct.install(new CouchPlugin(testSpace, testPath));
|
||||
openmct.install(new CouchPlugin(testPath));
|
||||
|
||||
element = document.createElement('div');
|
||||
child = document.createElement('div');
|
||||
|
||||
@@ -188,15 +188,19 @@
|
||||
ng-style="{
|
||||
right: (100 * (max - tick.value) / interval) + '%',
|
||||
height: '100%'
|
||||
}">
|
||||
</div>
|
||||
}"
|
||||
ng-show="plot.gridLines"
|
||||
>
|
||||
</div>
|
||||
</mct-ticks>
|
||||
|
||||
<mct-ticks axis="yAxis">
|
||||
<div class="gl-plot-hash hash-h"
|
||||
<div class="gl-plot-hash hash-h"
|
||||
ng-repeat="tick in ticks track by tick.value"
|
||||
ng-style="{ bottom: (100 * (tick.value - min) / interval) + '%', width: '100%' }">
|
||||
</div>
|
||||
ng-style="{ bottom: (100 * (tick.value - min) / interval) + '%', width: '100%' }"
|
||||
ng-show="plot.gridLines"
|
||||
>
|
||||
</div>
|
||||
</mct-ticks>
|
||||
|
||||
<mct-chart config="config"
|
||||
|
||||
@@ -22,16 +22,16 @@
|
||||
<div ng-controller="PlotController as controller"
|
||||
class="c-plot holder holder-plot has-control-bar">
|
||||
<div class="c-control-bar" ng-show="!controller.hideExportButtons">
|
||||
<span class="c-button-set c-button-set--strip-h">
|
||||
<span class="c-button-set c-button-set--strip-h">
|
||||
<button class="c-button icon-download"
|
||||
ng-click="controller.exportPNG()"
|
||||
title="Export This View's Data as PNG">
|
||||
<span class="c-button__label">PNG</span>
|
||||
ng-click="controller.exportPNG()"
|
||||
title="Export This View's Data as PNG">
|
||||
<span class="c-button__label">PNG</span>
|
||||
</button>
|
||||
<button class="c-button"
|
||||
ng-click="controller.exportJPG()"
|
||||
title="Export This View's Data as JPG">
|
||||
<span class="c-button__label">JPG</span>
|
||||
ng-click="controller.exportJPG()"
|
||||
title="Export This View's Data as JPG">
|
||||
<span class="c-button__label">JPG</span>
|
||||
</button>
|
||||
</span>
|
||||
<button class="c-button icon-crosshair"
|
||||
@@ -39,9 +39,14 @@
|
||||
ng-click="controller.toggleCursorGuide($event)"
|
||||
title="Toggle cursor guides">
|
||||
</button>
|
||||
<button class="c-button"
|
||||
ng-class="{ 'icon-grid-on': controller.gridLines, 'icon-grid-off': !controller.gridLines }"
|
||||
ng-click="controller.toggleGridLines($event)"
|
||||
title="Toggle grid lines">
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="l-view-section">
|
||||
<div class="l-view-section u-style-receiver js-style-receiver">
|
||||
<div class="c-loading--overlay loading"
|
||||
ng-show="!!pending"></div>
|
||||
<mct-plot config="controller.config"
|
||||
|
||||
@@ -22,25 +22,30 @@
|
||||
<div ng-controller="StackedPlotController as stackedPlot"
|
||||
class="c-plot c-plot--stacked holder holder-plot has-control-bar">
|
||||
<div class="c-control-bar" ng-show="!stackedPlot.hideExportButtons">
|
||||
<span class="c-button-set c-button-set--strip-h">
|
||||
<button class="c-button icon-download"
|
||||
ng-click="stackedPlot.exportPNG()"
|
||||
title="Export This View's Data as PNG">
|
||||
<span class="c-button__label">PNG</span>
|
||||
</button>
|
||||
<button class="c-button"
|
||||
ng-click="stackedPlot.exportJPG()"
|
||||
title="Export This View's Data as JPG">
|
||||
<span class="c-button__label">JPG</span>
|
||||
</button>
|
||||
<span class="c-button-set c-button-set--strip-h">
|
||||
<button class="c-button icon-download"
|
||||
ng-click="stackedPlot.exportPNG()"
|
||||
title="Export This View's Data as PNG">
|
||||
<span class="c-button__label">PNG</span>
|
||||
</button>
|
||||
<button class="c-button"
|
||||
ng-click="stackedPlot.exportJPG()"
|
||||
title="Export This View's Data as JPG">
|
||||
<span class="c-button__label">JPG</span>
|
||||
</button>
|
||||
</span>
|
||||
<button class="c-button icon-crosshair"
|
||||
ng-class="{ 'is-active': stackedPlot.cursorGuide }"
|
||||
ng-click="stackedPlot.toggleCursorGuide($event)"
|
||||
title="Toggle cursor guides">
|
||||
</button>
|
||||
<button class="c-button"
|
||||
ng-class="{ 'icon-grid-on': stackedPlot.gridLines, 'icon-grid-off': !stackedPlot.gridLines }"
|
||||
ng-click="stackedPlot.toggleGridLines($event)"
|
||||
title="Toggle grid lines">
|
||||
</button>
|
||||
</div>
|
||||
<div class="l-view-section">
|
||||
<div class="l-view-section u-style-receiver js-style-receiver">
|
||||
<div class="c-loading--overlay loading"
|
||||
ng-show="!!currentRequest.pending"></div>
|
||||
<div class="gl-plot child-frame u-inspectable"
|
||||
|
||||
@@ -96,7 +96,10 @@ define([
|
||||
this.cursorGuideHorizontal = this.$element[0].querySelector('.js-cursor-guide--h');
|
||||
this.cursorGuide = false;
|
||||
|
||||
this.gridLines = true;
|
||||
|
||||
this.listenTo(this.$scope, 'cursorguide', this.toggleCursorGuide, this);
|
||||
this.listenTo(this.$scope, 'toggleGridLines', this.toggleGridLines, this);
|
||||
|
||||
this.listenTo(this.$scope, '$destroy', this.destroy, this);
|
||||
this.listenTo(this.$scope, 'plot:tickWidth', this.onTickWidthChange, this);
|
||||
@@ -554,6 +557,10 @@ define([
|
||||
this.cursorGuide = !this.cursorGuide;
|
||||
};
|
||||
|
||||
MCTPlotController.prototype.toggleGridLines = function ($event) {
|
||||
this.gridLines = !this.gridLines;
|
||||
};
|
||||
|
||||
MCTPlotController.prototype.getXKeyOption = function (key) {
|
||||
return this.$scope.xKeyOptions.find(option => option.key === key);
|
||||
};
|
||||
|
||||
@@ -60,6 +60,7 @@ define([
|
||||
this.objectService = objectService;
|
||||
this.exportImageService = exportImageService;
|
||||
this.cursorGuide = false;
|
||||
this.gridLines = true;
|
||||
|
||||
$scope.pending = 0;
|
||||
|
||||
@@ -331,6 +332,11 @@ define([
|
||||
this.$scope.$broadcast('cursorguide', $event);
|
||||
};
|
||||
|
||||
PlotController.prototype.toggleGridLines = function ($event) {
|
||||
this.gridLines = !this.gridLines;
|
||||
this.$scope.$broadcast('toggleGridLines', $event);
|
||||
};
|
||||
|
||||
return PlotController;
|
||||
|
||||
});
|
||||
|
||||
@@ -160,5 +160,10 @@ define([], function () {
|
||||
this.$scope.$broadcast('cursorguide', $event);
|
||||
};
|
||||
|
||||
StackedPlotController.prototype.toggleGridLines = function ($event) {
|
||||
this.gridLines = !this.gridLines;
|
||||
this.$scope.$broadcast('toggleGridLines', $event);
|
||||
};
|
||||
|
||||
return StackedPlotController;
|
||||
});
|
||||
|
||||
@@ -57,7 +57,8 @@ define([
|
||||
'./notificationIndicator/plugin',
|
||||
'./newFolderAction/plugin',
|
||||
'./persistence/couch/plugin',
|
||||
'./defaultRootName/plugin'
|
||||
'./defaultRootName/plugin',
|
||||
'./timeline/plugin'
|
||||
], function (
|
||||
_,
|
||||
UTCTimeSystem,
|
||||
@@ -95,7 +96,8 @@ define([
|
||||
NotificationIndicator,
|
||||
NewFolderAction,
|
||||
CouchDBPlugin,
|
||||
DefaultRootName
|
||||
DefaultRootName,
|
||||
Timeline
|
||||
) {
|
||||
const bundleMap = {
|
||||
LocalStorage: 'platform/persistence/local',
|
||||
@@ -188,6 +190,7 @@ define([
|
||||
plugins.NewFolderAction = NewFolderAction.default;
|
||||
plugins.ISOTimeFormat = ISOTimeFormat.default;
|
||||
plugins.DefaultRootName = DefaultRootName.default;
|
||||
plugins.Timeline = Timeline.default;
|
||||
|
||||
return plugins;
|
||||
});
|
||||
|
||||
@@ -13,14 +13,7 @@
|
||||
}
|
||||
|
||||
&__tab {
|
||||
&:before {
|
||||
margin-right: $interiorMarginSm;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
&__label {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
justify-content: space-between; // Places remove button to far side of tab
|
||||
|
||||
&__close-btn {
|
||||
flex: 0 0 auto;
|
||||
|
||||
@@ -28,7 +28,18 @@
|
||||
}"
|
||||
@click="showTab(tab, index)"
|
||||
>
|
||||
<span class="c-button__label c-tabs-view__tab__label">{{ tab.domainObject.name }}</span>
|
||||
<div class="c-tabs-view__tab__label c-object-label"
|
||||
:class="{'is-missing': tab.domainObject.status === 'missing'}"
|
||||
>
|
||||
<div class="c-object-label__type-icon"
|
||||
:class="tab.type.definition.cssClass"
|
||||
>
|
||||
<span class="is-missing__indicator"
|
||||
title="This item is missing"
|
||||
></span>
|
||||
</div>
|
||||
<span class="c-button__label c-object-label__name">{{ tab.domainObject.name }}</span>
|
||||
</div>
|
||||
<button v-if="isEditing"
|
||||
class="icon-x c-click-icon c-tabs-view__tab__close-btn"
|
||||
@click="showRemoveDialog(index)"
|
||||
|
||||
@@ -25,6 +25,7 @@ define([
|
||||
'lodash',
|
||||
'./collections/BoundedTableRowCollection',
|
||||
'./collections/FilteredTableRowCollection',
|
||||
'./TelemetryTableNameColumn',
|
||||
'./TelemetryTableRow',
|
||||
'./TelemetryTableColumn',
|
||||
'./TelemetryTableUnitColumn',
|
||||
@@ -34,6 +35,7 @@ define([
|
||||
_,
|
||||
BoundedTableRowCollection,
|
||||
FilteredTableRowCollection,
|
||||
TelemetryTableNameColumn,
|
||||
TelemetryTableRow,
|
||||
TelemetryTableColumn,
|
||||
TelemetryTableUnitColumn,
|
||||
@@ -71,6 +73,24 @@ define([
|
||||
openmct.time.on('timeSystem', this.refreshData);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
addNameColumn(telemetryObject, metadataValues) {
|
||||
let metadatum = metadataValues.find(m => m.key === 'name');
|
||||
if (!metadatum) {
|
||||
metadatum = {
|
||||
format: 'string',
|
||||
key: 'name',
|
||||
name: 'Name'
|
||||
};
|
||||
}
|
||||
|
||||
const column = new TelemetryTableNameColumn(this.openmct, telemetryObject, metadatum);
|
||||
|
||||
this.configuration.addSingleColumnForObject(telemetryObject, column);
|
||||
}
|
||||
|
||||
initialize() {
|
||||
if (this.domainObject.type === 'table') {
|
||||
this.filterObserver = this.openmct.objects.observe(this.domainObject, 'configuration.filters', this.updateFilters);
|
||||
@@ -212,7 +232,13 @@ define([
|
||||
|
||||
addColumnsForObject(telemetryObject) {
|
||||
let metadataValues = this.openmct.telemetry.getMetadata(telemetryObject).values();
|
||||
|
||||
this.addNameColumn(telemetryObject, metadataValues);
|
||||
metadataValues.forEach(metadatum => {
|
||||
if (metadatum.key === 'name') {
|
||||
return;
|
||||
}
|
||||
|
||||
let column = this.createColumn(metadatum);
|
||||
this.configuration.addSingleColumnForObject(telemetryObject, column);
|
||||
// add units column if available
|
||||
|
||||
44
src/plugins/telemetryTable/TelemetryTableNameColumn.js
Normal file
44
src/plugins/telemetryTable/TelemetryTableNameColumn.js
Normal file
@@ -0,0 +1,44 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2018, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
define([
|
||||
'./TelemetryTableColumn.js'
|
||||
], function (
|
||||
TelemetryTableColumn
|
||||
) {
|
||||
class TelemetryTableNameColumn extends TelemetryTableColumn {
|
||||
constructor(openmct, telemetryObject, metadatum) {
|
||||
super(openmct, metadatum);
|
||||
|
||||
this.telemetryObject = telemetryObject;
|
||||
}
|
||||
|
||||
getRawValue() {
|
||||
return this.telemetryObject.name;
|
||||
}
|
||||
|
||||
getFormattedValue() {
|
||||
return this.telemetryObject.name;
|
||||
}
|
||||
}
|
||||
|
||||
return TelemetryTableNameColumn;
|
||||
});
|
||||
54
src/plugins/telemetryTable/components/sizing-row.vue
Normal file
54
src/plugins/telemetryTable/components/sizing-row.vue
Normal file
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<tr class="c-telemetry-table__sizing-tr"><td>SIZING ROW</td></tr>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
isEditing: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
isEditing: function (isEditing) {
|
||||
if (isEditing) {
|
||||
this.pollForRowHeight();
|
||||
} else {
|
||||
this.clearPoll();
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick().then(() => {
|
||||
this.height = this.$el.offsetHeight;
|
||||
this.$emit('change-height', this.height);
|
||||
});
|
||||
if (this.isEditing) {
|
||||
this.pollForRowHeight();
|
||||
}
|
||||
},
|
||||
destroyed() {
|
||||
this.clearPoll();
|
||||
},
|
||||
methods: {
|
||||
pollForRowHeight() {
|
||||
this.clearPoll();
|
||||
this.pollID = window.setInterval(this.heightPoll, 300);
|
||||
},
|
||||
clearPoll() {
|
||||
if (this.pollID) {
|
||||
window.clearInterval(this.pollID);
|
||||
this.pollID = undefined;
|
||||
}
|
||||
},
|
||||
heightPoll() {
|
||||
let height = this.$el.offsetHeight;
|
||||
if (height !== this.height) {
|
||||
this.$emit('change-height', height);
|
||||
this.height = height;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,29 @@
|
||||
.c-table-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.9em;
|
||||
overflow: hidden;
|
||||
|
||||
&__elem {
|
||||
@include ellipsize();
|
||||
flex: 0 1 auto;
|
||||
padding: 2px;
|
||||
text-transform: uppercase;
|
||||
|
||||
> * {
|
||||
//display: contents;
|
||||
}
|
||||
}
|
||||
|
||||
&__counts {
|
||||
//background: rgba(deeppink, 0.1);
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
justify-content: flex-end;
|
||||
overflow: hidden;
|
||||
|
||||
> * {
|
||||
margin-left: $interiorMargin;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,41 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="filterNames.length > 0"
|
||||
:title="title"
|
||||
class="c-filter-indication"
|
||||
:class="{ 'c-filter-indication--mixed': hasMixedFilters }"
|
||||
class="c-table-indicator"
|
||||
:class="{ 'is-filtering': filterNames.length > 0 }"
|
||||
>
|
||||
<span class="c-filter-indication__mixed">{{ label }}</span>
|
||||
<span
|
||||
v-for="(name, index) in filterNames"
|
||||
:key="index"
|
||||
class="c-filter-indication__label"
|
||||
<div
|
||||
v-if="filterNames.length > 0"
|
||||
class="c-table-indicator__filter c-table-indicator__elem c-filter-indication"
|
||||
:class="{ 'c-filter-indication--mixed': hasMixedFilters }"
|
||||
:title="title"
|
||||
>
|
||||
{{ name }}
|
||||
</span>
|
||||
<span class="c-filter-indication__mixed">{{ label }}</span>
|
||||
<span
|
||||
v-for="(name, index) in filterNames"
|
||||
:key="index"
|
||||
class="c-filter-indication__label"
|
||||
>
|
||||
{{ name }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="c-table-indicator__counts">
|
||||
<span
|
||||
:title="totalRows + ' rows visible after any filtering'"
|
||||
class="c-table-indicator__elem c-table-indicator__row-count"
|
||||
>
|
||||
{{ totalRows }} Rows
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-if="markedRows"
|
||||
class="c-table-indicator__elem c-table-indicator__marked-count"
|
||||
:title="markedRows + ' rows selected'"
|
||||
>
|
||||
{{ markedRows }} Marked
|
||||
</span>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -27,6 +50,16 @@ const USE_GLOBAL = 'useGlobal';
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'table'],
|
||||
props: {
|
||||
markedRows: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
totalRows: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
filterNames: [],
|
||||
@@ -9,6 +9,9 @@
|
||||
|
||||
.c-telemetry-table {
|
||||
// Table that displays telemetry in a scrolling body area
|
||||
|
||||
@include fontAndSize();
|
||||
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: flex-start;
|
||||
@@ -96,10 +99,6 @@
|
||||
height: 0; // Fixes Chrome 73 overflow bug
|
||||
overflow-x: auto;
|
||||
overflow-y: scroll;
|
||||
|
||||
.is-editing & {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
/******************************* TABLES */
|
||||
@@ -112,7 +111,11 @@
|
||||
display: flex; // flex-flow defaults to row nowrap (which is what we want) so no need to define
|
||||
align-items: stretch;
|
||||
position: absolute;
|
||||
height: 18px; // Needed when a row has empty values in its cells
|
||||
min-height: 18px; // Needed when a row has empty values in its cells
|
||||
|
||||
.is-editing .l-layout__frame & {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.is-selected {
|
||||
background-color: $colorSelectedBg !important;
|
||||
@@ -150,6 +153,41 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
&__sizing-tr {
|
||||
// A row element used to determine sizing of rows based on font size
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
$pt: 2px;
|
||||
border-top: 1px solid $colorInteriorBorder;
|
||||
margin-top: $interiorMargin;
|
||||
padding: $pt 0;
|
||||
overflow: hidden;
|
||||
transition: all 250ms;
|
||||
|
||||
&:not(.is-filtering) {
|
||||
.c-frame & {
|
||||
height: 0;
|
||||
padding: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.c-frame & {
|
||||
// target .c-frame .c-telemetry-table {}
|
||||
$pt: 2px;
|
||||
&:hover {
|
||||
.c-telemetry-table__footer:not(.is-filtering) {
|
||||
height: $pt + 16px;
|
||||
padding: initial;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/******************************* SPECIFIC CASE WRAPPERS */
|
||||
|
||||
@@ -125,7 +125,7 @@
|
||||
<!-- alternate controlbar end -->
|
||||
|
||||
<div
|
||||
class="c-table c-telemetry-table c-table--filterable c-table--sortable has-control-bar"
|
||||
class="c-table c-telemetry-table c-table--filterable c-table--sortable has-control-bar u-style-receiver js-style-receiver"
|
||||
:class="{
|
||||
'loading': loading,
|
||||
'is-paused' : paused
|
||||
@@ -234,6 +234,10 @@
|
||||
class="c-telemetry-table__sizing js-telemetry-table__sizing"
|
||||
:style="sizingTableWidth"
|
||||
>
|
||||
<sizing-row
|
||||
:is-editing="isEditing"
|
||||
@change-height="setRowHeight"
|
||||
/>
|
||||
<tr>
|
||||
<template v-for="(title, key) in headers">
|
||||
<th
|
||||
@@ -253,7 +257,11 @@
|
||||
:object-path="objectPath"
|
||||
/>
|
||||
</table>
|
||||
<telemetry-filter-indicator />
|
||||
<table-footer-indicator
|
||||
class="c-telemetry-table__footer"
|
||||
:marked-rows="markedRows.length"
|
||||
:total-rows="totalNumberOfRows"
|
||||
/>
|
||||
</div>
|
||||
</div><!-- closes c-table-wrapper -->
|
||||
</template>
|
||||
@@ -262,10 +270,11 @@
|
||||
import TelemetryTableRow from './table-row.vue';
|
||||
import search from '../../../ui/components/search.vue';
|
||||
import TableColumnHeader from './table-column-header.vue';
|
||||
import TelemetryFilterIndicator from './TelemetryFilterIndicator.vue';
|
||||
import TableFooterIndicator from './table-footer-indicator.vue';
|
||||
import CSVExporter from '../../../exporters/CSVExporter.js';
|
||||
import _ from 'lodash';
|
||||
import ToggleSwitch from '../../../ui/components/ToggleSwitch.vue';
|
||||
import SizingRow from './sizing-row.vue';
|
||||
|
||||
const VISIBLE_ROW_COUNT = 100;
|
||||
const ROW_HEIGHT = 17;
|
||||
@@ -277,8 +286,9 @@ export default {
|
||||
TelemetryTableRow,
|
||||
TableColumnHeader,
|
||||
search,
|
||||
TelemetryFilterIndicator,
|
||||
ToggleSwitch
|
||||
TableFooterIndicator,
|
||||
ToggleSwitch,
|
||||
SizingRow
|
||||
},
|
||||
inject: ['table', 'openmct', 'objectPath'],
|
||||
props: {
|
||||
@@ -342,7 +352,8 @@ export default {
|
||||
paused: false,
|
||||
markedRows: [],
|
||||
isShowingMarkedRowsOnly: false,
|
||||
hideHeaders: configuration.hideHeaders
|
||||
hideHeaders: configuration.hideHeaders,
|
||||
totalNumberOfRows: 0
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -451,6 +462,8 @@ export default {
|
||||
let filteredRows = this.table.filteredRows.getRows();
|
||||
let filteredRowsLength = filteredRows.length;
|
||||
|
||||
this.totalNumberOfRows = filteredRowsLength;
|
||||
|
||||
if (filteredRowsLength < VISIBLE_ROW_COUNT) {
|
||||
end = filteredRowsLength;
|
||||
} else {
|
||||
@@ -499,7 +512,7 @@ export default {
|
||||
let columnWidths = {};
|
||||
let totalWidth = 0;
|
||||
let headerKeys = Object.keys(this.headers);
|
||||
let sizingTableRow = this.sizingTable.children[0];
|
||||
let sizingTableRow = this.sizingTable.children[1];
|
||||
let sizingCells = sizingTableRow.children;
|
||||
|
||||
headerKeys.forEach((headerKey, headerIndex, array) => {
|
||||
@@ -894,6 +907,12 @@ export default {
|
||||
this.isAutosizeEnabled = true;
|
||||
|
||||
this.$nextTick().then(this.calculateColumnWidths);
|
||||
},
|
||||
setRowHeight(height) {
|
||||
this.rowHeight = height;
|
||||
this.setHeight();
|
||||
this.calculateTableSize();
|
||||
this.clearRowsAndRerender();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
.c-filter-indication {
|
||||
@include userSelectNone();
|
||||
background: $colorFilterBg;
|
||||
color: $colorFilterFg;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.9em;
|
||||
margin-top: $interiorMarginSm;
|
||||
padding: 2px;
|
||||
text-transform: uppercase;
|
||||
|
||||
&:before {
|
||||
font-family: symbolsfont-12px;
|
||||
content: $glyph-icon-filter;
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
margin-right: $interiorMarginSm;
|
||||
}
|
||||
|
||||
&__mixed {
|
||||
margin-right: $interiorMarginSm;
|
||||
}
|
||||
|
||||
&--mixed {
|
||||
.c-filter-indication__mixed {
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
&__label {
|
||||
+ .c-filter-indication__label {
|
||||
&:before {
|
||||
content: ',';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -183,10 +183,11 @@ describe("the plugin", () => {
|
||||
|
||||
it("Renders a column for every item in telemetry metadata", () => {
|
||||
let headers = element.querySelectorAll('span.c-telemetry-table__headers__label');
|
||||
expect(headers.length).toBe(3);
|
||||
expect(headers[0].innerText).toBe('Time');
|
||||
expect(headers[1].innerText).toBe('Some attribute');
|
||||
expect(headers[2].innerText).toBe('Another attribute');
|
||||
expect(headers.length).toBe(4);
|
||||
expect(headers[0].innerText).toBe('Name');
|
||||
expect(headers[1].innerText).toBe('Time');
|
||||
expect(headers[2].innerText).toBe('Some attribute');
|
||||
expect(headers[3].innerText).toBe('Another attribute');
|
||||
});
|
||||
|
||||
it("Supports column reordering via drag and drop", () => {
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
@import "~styles/vendor/normalize-min";
|
||||
@import "~styles/constants";
|
||||
@import "~styles/constants-mobile.scss";
|
||||
@import "../../styles/vendor/normalize-min";
|
||||
@import "../../styles/constants";
|
||||
@import "../../styles/constants-mobile.scss";
|
||||
|
||||
@import "~styles/constants-espresso";
|
||||
@import "../../styles/constants-espresso";
|
||||
|
||||
@import "~styles/mixins";
|
||||
@import "~styles/animations";
|
||||
@import "~styles/about";
|
||||
@import "~styles/glyphs";
|
||||
@import "~styles/global";
|
||||
@import "~styles/status";
|
||||
@import "~styles/controls";
|
||||
@import "~styles/forms";
|
||||
@import "~styles/table";
|
||||
@import "~styles/legacy";
|
||||
@import "~styles/legacy-plots";
|
||||
@import "~styles/plotly";
|
||||
@import "~styles/legacy-messages";
|
||||
@import "../../styles/mixins";
|
||||
@import "../../styles/animations";
|
||||
@import "../../styles/about";
|
||||
@import "../../styles/glyphs";
|
||||
@import "../../styles/global";
|
||||
@import "../../styles/status";
|
||||
@import "../../styles/controls";
|
||||
@import "../../styles/forms";
|
||||
@import "../../styles/table";
|
||||
@import "../../styles/legacy";
|
||||
@import "../../styles/legacy-plots";
|
||||
@import "../../styles/plotly";
|
||||
@import "../../styles/legacy-messages";
|
||||
|
||||
@import "~styles/vue-styles.scss";
|
||||
@import "../../styles/vue-styles.scss";
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
@import "~styles/vendor/normalize-min";
|
||||
@import "~styles/constants";
|
||||
@import "~styles/constants-mobile.scss";
|
||||
@import "../../styles/vendor/normalize-min";
|
||||
@import "../../styles/constants";
|
||||
@import "../../styles/constants-mobile.scss";
|
||||
|
||||
@import "~styles/constants-maelstrom";
|
||||
@import "../../styles/constants-maelstrom";
|
||||
|
||||
@import "~styles/mixins";
|
||||
@import "~styles/animations";
|
||||
@import "~styles/about";
|
||||
@import "~styles/glyphs";
|
||||
@import "~styles/global";
|
||||
@import "~styles/status";
|
||||
@import "~styles/controls";
|
||||
@import "~styles/forms";
|
||||
@import "~styles/table";
|
||||
@import "~styles/legacy";
|
||||
@import "~styles/legacy-plots";
|
||||
@import "~styles/plotly";
|
||||
@import "~styles/legacy-messages";
|
||||
@import "../../styles/mixins";
|
||||
@import "../../styles/animations";
|
||||
@import "../../styles/about";
|
||||
@import "../../styles/glyphs";
|
||||
@import "../../styles/global";
|
||||
@import "../../styles/status";
|
||||
@import "../../styles/controls";
|
||||
@import "../../styles/forms";
|
||||
@import "../../styles/table";
|
||||
@import "../../styles/legacy";
|
||||
@import "../../styles/legacy-plots";
|
||||
@import "../../styles/plotly";
|
||||
@import "../../styles/legacy-messages";
|
||||
|
||||
@import "~styles/vue-styles.scss";
|
||||
@import "../../styles/vue-styles.scss";
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
@import "~styles/vendor/normalize-min";
|
||||
@import "~styles/constants";
|
||||
@import "~styles/constants-mobile.scss";
|
||||
@import "../../styles/vendor/normalize-min";
|
||||
@import "../../styles/constants";
|
||||
@import "../../styles/constants-mobile.scss";
|
||||
|
||||
@import "~styles/constants-snow";
|
||||
@import "../../styles/constants-snow";
|
||||
|
||||
@import "~styles/mixins";
|
||||
@import "~styles/animations";
|
||||
@import "~styles/about";
|
||||
@import "~styles/glyphs";
|
||||
@import "~styles/global";
|
||||
@import "~styles/status";
|
||||
@import "~styles/controls";
|
||||
@import "~styles/forms";
|
||||
@import "~styles/table";
|
||||
@import "~styles/legacy";
|
||||
@import "~styles/legacy-plots";
|
||||
@import "~styles/plotly";
|
||||
@import "~styles/legacy-messages";
|
||||
@import "../../styles/mixins";
|
||||
@import "../../styles/animations";
|
||||
@import "../../styles/about";
|
||||
@import "../../styles/glyphs";
|
||||
@import "../../styles/global";
|
||||
@import "../../styles/status";
|
||||
@import "../../styles/controls";
|
||||
@import "../../styles/forms";
|
||||
@import "../../styles/table";
|
||||
@import "../../styles/legacy";
|
||||
@import "../../styles/legacy-plots";
|
||||
@import "../../styles/plotly";
|
||||
@import "../../styles/legacy-messages";
|
||||
|
||||
@import "~styles/vue-styles.scss";
|
||||
@import "../../styles/vue-styles.scss";
|
||||
|
||||
@@ -141,10 +141,11 @@
|
||||
<ConductorMode class="c-conductor__mode-select" />
|
||||
<ConductorTimeSystem class="c-conductor__time-system-select" />
|
||||
<ConductorHistory
|
||||
v-if="isFixed"
|
||||
class="c-conductor__history-select"
|
||||
:offsets="openmct.time.clockOffsets()"
|
||||
:bounds="bounds"
|
||||
:time-system="timeSystem"
|
||||
:mode="timeMode"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
@@ -210,6 +211,11 @@ export default {
|
||||
isZooming: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
timeMode() {
|
||||
return this.isFixed ? 'fixed' : 'realtime';
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
document.addEventListener('keydown', this.handleKeyDown);
|
||||
document.addEventListener('keyup', this.handleKeyUp);
|
||||
|
||||
@@ -66,7 +66,9 @@
|
||||
<script>
|
||||
import toggleMixin from '../../ui/mixins/toggle-mixin';
|
||||
|
||||
const LOCAL_STORAGE_HISTORY_KEY = 'tcHistory';
|
||||
const DEFAULT_DURATION_FORMATTER = 'duration';
|
||||
const LOCAL_STORAGE_HISTORY_KEY_FIXED = 'tcHistory';
|
||||
const LOCAL_STORAGE_HISTORY_KEY_REALTIME = 'tcHistoryRealtime';
|
||||
const DEFAULT_RECORDS = 10;
|
||||
|
||||
export default {
|
||||
@@ -77,72 +79,115 @@ export default {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
offsets: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => {}
|
||||
},
|
||||
timeSystem: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
/**
|
||||
* previous bounds entries available for easy re-use
|
||||
* @history array of timespans
|
||||
* @realtimeHistory array of timespans
|
||||
* @timespans {start, end} number representing timestamp
|
||||
*/
|
||||
history: this.getHistoryFromLocalStorage(),
|
||||
realtimeHistory: {},
|
||||
/**
|
||||
* previous bounds entries available for easy re-use
|
||||
* @fixedHistory array of timespans
|
||||
* @timespans {start, end} number representing timestamp
|
||||
*/
|
||||
fixedHistory: {},
|
||||
presets: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
currentHistory() {
|
||||
return this.mode + 'History';
|
||||
},
|
||||
isFixed() {
|
||||
return this.openmct.time.clock() === undefined;
|
||||
},
|
||||
hasHistoryPresets() {
|
||||
return this.timeSystem.isUTCBased && this.presets.length;
|
||||
},
|
||||
historyForCurrentTimeSystem() {
|
||||
const history = this.history[this.timeSystem.key];
|
||||
const history = this[this.currentHistory][this.timeSystem.key];
|
||||
|
||||
return history;
|
||||
},
|
||||
storageKey() {
|
||||
let key = LOCAL_STORAGE_HISTORY_KEY_FIXED;
|
||||
if (this.mode !== 'fixed') {
|
||||
key = LOCAL_STORAGE_HISTORY_KEY_REALTIME;
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
bounds: {
|
||||
handler() {
|
||||
// only for fixed time since we track offsets for realtime
|
||||
if (this.isFixed) {
|
||||
this.addTimespan();
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
offsets: {
|
||||
handler() {
|
||||
this.addTimespan();
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
timeSystem: {
|
||||
handler() {
|
||||
handler(ts) {
|
||||
this.loadConfiguration();
|
||||
this.addTimespan();
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
mode: function () {
|
||||
this.getHistoryFromLocalStorage();
|
||||
this.initializeHistoryIfNoHistory();
|
||||
this.loadConfiguration();
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getHistoryFromLocalStorage();
|
||||
this.initializeHistoryIfNoHistory();
|
||||
},
|
||||
methods: {
|
||||
getHistoryFromLocalStorage() {
|
||||
const localStorageHistory = localStorage.getItem(LOCAL_STORAGE_HISTORY_KEY);
|
||||
const localStorageHistory = localStorage.getItem(this.storageKey);
|
||||
const history = localStorageHistory ? JSON.parse(localStorageHistory) : undefined;
|
||||
|
||||
return history;
|
||||
this[this.currentHistory] = history;
|
||||
},
|
||||
initializeHistoryIfNoHistory() {
|
||||
if (!this.history) {
|
||||
this.history = {};
|
||||
if (!this[this.currentHistory]) {
|
||||
this[this.currentHistory] = {};
|
||||
this.persistHistoryToLocalStorage();
|
||||
}
|
||||
},
|
||||
persistHistoryToLocalStorage() {
|
||||
localStorage.setItem(LOCAL_STORAGE_HISTORY_KEY, JSON.stringify(this.history));
|
||||
localStorage.setItem(this.storageKey, JSON.stringify(this[this.currentHistory]));
|
||||
},
|
||||
addTimespan() {
|
||||
const key = this.timeSystem.key;
|
||||
let [...currentHistory] = this.history[key] || [];
|
||||
let [...currentHistory] = this[this.currentHistory][key] || [];
|
||||
const timespan = {
|
||||
start: this.bounds.start,
|
||||
end: this.bounds.end
|
||||
start: this.isFixed ? this.bounds.start : this.offsets.start,
|
||||
end: this.isFixed ? this.bounds.end : this.offsets.end
|
||||
};
|
||||
let self = this;
|
||||
|
||||
@@ -160,20 +205,24 @@ export default {
|
||||
}
|
||||
|
||||
currentHistory.unshift(timespan);
|
||||
this.history[key] = currentHistory;
|
||||
this.$set(this[this.currentHistory], key, currentHistory);
|
||||
|
||||
this.persistHistoryToLocalStorage();
|
||||
},
|
||||
selectTimespan(timespan) {
|
||||
this.openmct.time.bounds(timespan);
|
||||
if (this.isFixed) {
|
||||
this.openmct.time.bounds(timespan);
|
||||
} else {
|
||||
this.openmct.time.clockOffsets(timespan);
|
||||
}
|
||||
},
|
||||
selectPresetBounds(bounds) {
|
||||
const start = typeof bounds.start === 'function' ? bounds.start() : bounds.start;
|
||||
const end = typeof bounds.end === 'function' ? bounds.end() : bounds.end;
|
||||
|
||||
this.selectTimespan({
|
||||
start: start,
|
||||
end: end
|
||||
start,
|
||||
end
|
||||
});
|
||||
},
|
||||
loadConfiguration() {
|
||||
@@ -184,7 +233,9 @@ export default {
|
||||
this.records = this.loadRecords(configurations);
|
||||
},
|
||||
loadPresets(configurations) {
|
||||
const configuration = configurations.find(option => option.presets);
|
||||
const configuration = configurations.find(option => {
|
||||
return option.presets && option.name.toLowerCase() === this.mode;
|
||||
});
|
||||
const presets = configuration ? configuration.presets : [];
|
||||
|
||||
return presets;
|
||||
@@ -196,11 +247,24 @@ export default {
|
||||
return records;
|
||||
},
|
||||
formatTime(time) {
|
||||
let format = this.timeSystem.timeFormat;
|
||||
let isNegativeOffset = false;
|
||||
|
||||
if (!this.isFixed) {
|
||||
if (time < 0) {
|
||||
isNegativeOffset = true;
|
||||
}
|
||||
|
||||
time = Math.abs(time);
|
||||
|
||||
format = this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER;
|
||||
}
|
||||
|
||||
const formatter = this.openmct.telemetry.getValueFormatter({
|
||||
format: this.timeSystem.timeFormat
|
||||
format: format
|
||||
}).formatter;
|
||||
|
||||
return formatter.format(time);
|
||||
return (isNegativeOffset ? '-' : '') + formatter.format(time);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
437
src/plugins/timeline/Plan.vue
Normal file
437
src/plugins/timeline/Plan.vue
Normal file
@@ -0,0 +1,437 @@
|
||||
<template>
|
||||
<div ref="axisHolder"
|
||||
class="c-timeline-plan"
|
||||
>
|
||||
<div class="nowMarker"><span class="icon-arrow-down"></span></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as d3Selection from 'd3-selection';
|
||||
import * as d3Axis from 'd3-axis';
|
||||
import * as d3Scale from 'd3-scale';
|
||||
import utcMultiTimeFormat from "@/plugins/timeConductor/utcMultiTimeFormat";
|
||||
|
||||
//TODO: UI direction needed for the following property values
|
||||
const PADDING = 1;
|
||||
const OUTER_TEXT_PADDING = 12;
|
||||
const INNER_TEXT_PADDING = 17;
|
||||
const TEXT_LEFT_PADDING = 5;
|
||||
const ROW_PADDING = 12;
|
||||
// const DEFAULT_DURATION_FORMATTER = 'duration';
|
||||
const RESIZE_POLL_INTERVAL = 200;
|
||||
const PIXELS_PER_TICK = 100;
|
||||
const PIXELS_PER_TICK_WIDE = 200;
|
||||
const ROW_HEIGHT = 30;
|
||||
const LINE_HEIGHT = 12;
|
||||
const MAX_TEXT_WIDTH = 300;
|
||||
const TIMELINE_HEIGHT = 30;
|
||||
//This offset needs to be re-considered
|
||||
const TIMELINE_OFFSET_HEIGHT = 70;
|
||||
const GROUP_OFFSET = 100;
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject'],
|
||||
props: {
|
||||
"renderingEngine": {
|
||||
type: String,
|
||||
default() {
|
||||
return 'canvas';
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.validateJSON(this.domainObject.selectFile.body);
|
||||
if (this.renderingEngine === 'svg') {
|
||||
this.useSVG = true;
|
||||
}
|
||||
|
||||
this.container = d3Selection.select(this.$refs.axisHolder);
|
||||
this.svgElement = this.container.append("svg:svg");
|
||||
// draw x axis with labels. CSS is used to position them.
|
||||
this.axisElement = this.svgElement.append("g")
|
||||
.attr("class", "axis");
|
||||
this.xAxis = d3Axis.axisTop();
|
||||
|
||||
this.canvas = this.container.append('canvas').node();
|
||||
this.canvasContext = this.canvas.getContext('2d');
|
||||
|
||||
this.setDimensions();
|
||||
this.updateViewBounds();
|
||||
this.openmct.time.on("timeSystem", this.setScaleAndPlotActivities);
|
||||
this.openmct.time.on("bounds", this.updateViewBounds);
|
||||
this.resizeTimer = setInterval(this.resize, RESIZE_POLL_INTERVAL);
|
||||
},
|
||||
destroyed() {
|
||||
clearInterval(this.resizeTimer);
|
||||
this.openmct.time.off("timeSystem", this.setScaleAndPlotActivities);
|
||||
this.openmct.time.off("bounds", this.updateViewBounds);
|
||||
},
|
||||
methods: {
|
||||
resize() {
|
||||
if (this.$refs.axisHolder.clientWidth !== this.width) {
|
||||
this.setDimensions();
|
||||
this.updateViewBounds();
|
||||
}
|
||||
},
|
||||
validateJSON(jsonString) {
|
||||
try {
|
||||
this.json = JSON.parse(jsonString);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
updateViewBounds() {
|
||||
this.viewBounds = this.openmct.time.bounds();
|
||||
// this.viewBounds.end = this.viewBounds.end + (30 * 60 * 1000);
|
||||
this.setScaleAndPlotActivities();
|
||||
},
|
||||
updateNowMarker() {
|
||||
if (this.openmct.time.clock() === undefined) {
|
||||
let nowMarker = document.querySelector('.nowMarker');
|
||||
if (nowMarker) {
|
||||
nowMarker.parentNode.removeChild(nowMarker);
|
||||
}
|
||||
} else {
|
||||
let nowMarker = document.querySelector('.nowMarker');
|
||||
if (nowMarker) {
|
||||
const svgEl = d3Selection.select(this.svgElement).node();
|
||||
const height = this.useSVG ? svgEl.style('height') : this.canvas.height + 'px';
|
||||
nowMarker.style.height = height;
|
||||
const now = this.xScale(Date.now());
|
||||
nowMarker.style.left = now + GROUP_OFFSET + 'px';
|
||||
}
|
||||
}
|
||||
},
|
||||
setScaleAndPlotActivities() {
|
||||
this.setScale();
|
||||
this.clearPreviousActivities();
|
||||
if (this.xScale) {
|
||||
this.calculatePlanLayout();
|
||||
this.drawPlan();
|
||||
this.updateNowMarker();
|
||||
}
|
||||
},
|
||||
clearPreviousActivities() {
|
||||
if (this.useSVG) {
|
||||
d3Selection.selectAll("svg > :not(g)").remove();
|
||||
} else {
|
||||
this.canvasContext.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
}
|
||||
},
|
||||
setDimensions() {
|
||||
const axisHolder = this.$refs.axisHolder;
|
||||
const rect = axisHolder.getBoundingClientRect();
|
||||
this.left = Math.round(rect.left);
|
||||
this.top = Math.round(rect.top);
|
||||
this.width = axisHolder.clientWidth;
|
||||
this.offsetWidth = this.width - GROUP_OFFSET;
|
||||
|
||||
const axisHolderParent = this.$parent.$refs.planHolder;
|
||||
this.height = Math.round(axisHolderParent.getBoundingClientRect().height);
|
||||
|
||||
if (this.useSVG) {
|
||||
this.svgElement.attr("width", this.width);
|
||||
this.svgElement.attr("height", this.height);
|
||||
} else {
|
||||
this.svgElement.attr("height", 50);
|
||||
this.canvas.width = this.width;
|
||||
this.canvas.height = this.height;
|
||||
}
|
||||
|
||||
this.canvasContext.font = "normal normal 12px sans-serif";
|
||||
},
|
||||
setScale(timeSystem) {
|
||||
if (!this.width) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (timeSystem === undefined) {
|
||||
timeSystem = this.openmct.time.timeSystem();
|
||||
}
|
||||
|
||||
if (timeSystem.isUTCBased) {
|
||||
this.xScale = d3Scale.scaleUtc();
|
||||
this.xScale.domain(
|
||||
[new Date(this.viewBounds.start), new Date(this.viewBounds.end)]
|
||||
);
|
||||
} else {
|
||||
this.xScale = d3Scale.scaleLinear();
|
||||
this.xScale.domain(
|
||||
[this.viewBounds.start, this.viewBounds.end]
|
||||
);
|
||||
}
|
||||
|
||||
this.xScale.range([PADDING, this.offsetWidth - PADDING * 2]);
|
||||
|
||||
this.xAxis.scale(this.xScale);
|
||||
this.xAxis.tickFormat(utcMultiTimeFormat);
|
||||
|
||||
this.axisElement.call(this.xAxis);
|
||||
|
||||
if (this.width > 1800) {
|
||||
this.xAxis.ticks(this.offsetWidth / PIXELS_PER_TICK_WIDE);
|
||||
} else {
|
||||
this.xAxis.ticks(this.offsetWidth / PIXELS_PER_TICK);
|
||||
}
|
||||
},
|
||||
isActivityInBounds(activity) {
|
||||
return (activity.start < this.viewBounds.end) && (activity.end > this.viewBounds.start);
|
||||
},
|
||||
getTextWidth(name) {
|
||||
// canvasContext.font = font;
|
||||
let metrics = this.canvasContext.measureText(name);
|
||||
|
||||
return parseInt(metrics.width, 10);
|
||||
},
|
||||
sortFn(a, b) {
|
||||
const numA = parseInt(a, 10);
|
||||
const numB = parseInt(b, 10);
|
||||
if (numA > numB) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (numA < numB) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
},
|
||||
// Get the row where the next activity will land.
|
||||
getRowForActivity(rectX, width, defaultActivityRow = 0) {
|
||||
let currentRow;
|
||||
let sortedActivityRows = Object.keys(this.activitiesByRow).sort(this.sortFn);
|
||||
|
||||
function getOverlap(rects) {
|
||||
return rects.every(rect => {
|
||||
const { start, end } = rect;
|
||||
const calculatedEnd = rectX + width;
|
||||
const hasOverlap = (rectX >= start && rectX <= end) || (calculatedEnd >= start && calculatedEnd <= end) || (rectX <= start && calculatedEnd >= end);
|
||||
|
||||
return !hasOverlap;
|
||||
});
|
||||
}
|
||||
|
||||
for (let i = 0; i < sortedActivityRows.length; i++) {
|
||||
let row = sortedActivityRows[i];
|
||||
if (getOverlap(this.activitiesByRow[row])) {
|
||||
currentRow = row;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentRow === undefined && sortedActivityRows.length) {
|
||||
currentRow = parseInt(sortedActivityRows[sortedActivityRows.length - 1], 10) + ROW_HEIGHT + ROW_PADDING;
|
||||
}
|
||||
|
||||
return (currentRow || defaultActivityRow);
|
||||
},
|
||||
calculatePlanLayout() {
|
||||
this.activitiesByRow = {};
|
||||
|
||||
let currentRow = 0;
|
||||
|
||||
let groups = Object.keys(this.json);
|
||||
groups.forEach((key, index) => {
|
||||
let activities = this.json[key];
|
||||
//set the currentRow to the beginning of the next logical row
|
||||
currentRow = currentRow + ROW_HEIGHT * index;
|
||||
let newGroup = true;
|
||||
activities.forEach((activity) => {
|
||||
if (this.isActivityInBounds(activity)) {
|
||||
const currentStart = Math.max(this.viewBounds.start, activity.start);
|
||||
const currentEnd = Math.min(this.viewBounds.end, activity.end);
|
||||
const rectX = this.xScale(currentStart);
|
||||
const rectY = this.xScale(currentEnd);
|
||||
const rectWidth = rectY - rectX;
|
||||
|
||||
const activityNameWidth = this.getTextWidth(activity.name) + TEXT_LEFT_PADDING;
|
||||
//TODO: Fix bug for SVG where the rectWidth is not proportional to the canvas measuredWidth of the text
|
||||
const activityNameFitsRect = (rectWidth >= activityNameWidth);
|
||||
const textStart = (activityNameFitsRect ? rectX : (rectX + rectWidth)) + TEXT_LEFT_PADDING;
|
||||
|
||||
let textLines = this.getActivityDisplayText(this.canvasContext, activity.name, activityNameFitsRect);
|
||||
const textWidth = textStart + this.getTextWidth(textLines[0]) + TEXT_LEFT_PADDING;
|
||||
|
||||
if (activityNameFitsRect) {
|
||||
currentRow = this.getRowForActivity(rectX, rectWidth);
|
||||
} else {
|
||||
currentRow = this.getRowForActivity(rectX, textWidth);
|
||||
}
|
||||
|
||||
let textY = parseInt(currentRow, 10) + (activityNameFitsRect ? INNER_TEXT_PADDING : OUTER_TEXT_PADDING);
|
||||
|
||||
if (!this.activitiesByRow[currentRow]) {
|
||||
this.activitiesByRow[currentRow] = [];
|
||||
}
|
||||
|
||||
this.activitiesByRow[currentRow].push({
|
||||
heading: newGroup ? key : '',
|
||||
activity: {
|
||||
color: activity.color,
|
||||
textColor: activity.textColor
|
||||
},
|
||||
textLines: textLines,
|
||||
textStart: textStart,
|
||||
textY: textY,
|
||||
start: rectX,
|
||||
end: activityNameFitsRect ? rectX + rectWidth : textStart + textWidth,
|
||||
rectWidth: rectWidth
|
||||
});
|
||||
newGroup = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
getActivityDisplayText(context, text, activityNameFitsRect) {
|
||||
//TODO: If the activity start is less than viewBounds.start then the text should be cropped on the left/should be off-screen)
|
||||
let words = text.split(' ');
|
||||
let line = '';
|
||||
let activityText = [];
|
||||
let rows = 1;
|
||||
|
||||
for (let n = 0; (n < words.length) && (rows <= 2); n++) {
|
||||
let testLine = line + words[n] + ' ';
|
||||
let metrics = context.measureText(testLine);
|
||||
let testWidth = metrics.width;
|
||||
if (!activityNameFitsRect && (testWidth > MAX_TEXT_WIDTH && n > 0)) {
|
||||
activityText.push(line);
|
||||
line = words[n] + ' ';
|
||||
testLine = line + words[n] + ' ';
|
||||
rows = rows + 1;
|
||||
}
|
||||
|
||||
line = testLine;
|
||||
}
|
||||
|
||||
return activityText.length ? activityText : [line];
|
||||
},
|
||||
getGroupHeading(row) {
|
||||
let groupHeadingRow;
|
||||
let groupHeadingBorder;
|
||||
|
||||
if (row) {
|
||||
groupHeadingBorder = row + ROW_PADDING + OUTER_TEXT_PADDING;
|
||||
groupHeadingRow = groupHeadingBorder + OUTER_TEXT_PADDING;
|
||||
} else {
|
||||
groupHeadingRow = TIMELINE_HEIGHT + OUTER_TEXT_PADDING;
|
||||
}
|
||||
|
||||
return {
|
||||
groupHeadingRow,
|
||||
groupHeadingBorder
|
||||
};
|
||||
},
|
||||
getPlanHeight(activityRows) {
|
||||
return parseInt(activityRows[activityRows.length - 1], 10) + TIMELINE_OFFSET_HEIGHT;
|
||||
},
|
||||
drawPlan() {
|
||||
const activityRows = Object.keys(this.activitiesByRow);
|
||||
if (activityRows.length) {
|
||||
|
||||
let planHeight = this.getPlanHeight(activityRows);
|
||||
planHeight = Math.max(this.height, planHeight);
|
||||
if (this.useSVG) {
|
||||
this.svgElement.attr("height", planHeight);
|
||||
} else {
|
||||
// This needs to happen before we draw on the canvas or the canvas will get wiped out when height is set
|
||||
this.canvas.height = planHeight;
|
||||
}
|
||||
|
||||
activityRows.forEach((key) => {
|
||||
const items = this.activitiesByRow[key];
|
||||
const row = parseInt(key, 10);
|
||||
items.forEach((item) => {
|
||||
|
||||
//TODO: Don't draw the left-border of the rectangle if the activity started before viewBounds.start
|
||||
if (this.useSVG) {
|
||||
this.plotSVG(item, row);
|
||||
} else {
|
||||
this.plotCanvas(item, row);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
},
|
||||
plotSVG(item, row) {
|
||||
const headingText = item.heading;
|
||||
const { groupHeadingRow, groupHeadingBorder } = this.getGroupHeading(row);
|
||||
|
||||
if (headingText) {
|
||||
if (groupHeadingBorder) {
|
||||
this.svgElement.append("line")
|
||||
.attr("class", "activity")
|
||||
.attr("x1", 0)
|
||||
.attr("y1", groupHeadingBorder)
|
||||
.attr("x2", this.width)
|
||||
.attr("y2", groupHeadingBorder)
|
||||
.attr('stroke', "white");
|
||||
}
|
||||
|
||||
this.svgElement.append("text").text(headingText)
|
||||
.attr("class", "activity")
|
||||
.attr("x", 0)
|
||||
.attr("y", groupHeadingRow)
|
||||
.attr('fill', "white");
|
||||
}
|
||||
|
||||
const activity = item.activity;
|
||||
const rectY = row + TIMELINE_HEIGHT;
|
||||
this.svgElement.append("rect")
|
||||
.attr("class", "activity")
|
||||
.attr("x", item.start + GROUP_OFFSET)
|
||||
.attr("y", rectY + TIMELINE_HEIGHT)
|
||||
.attr("width", item.rectWidth)
|
||||
.attr("height", ROW_HEIGHT)
|
||||
.attr('fill', activity.color)
|
||||
.attr('stroke', "lightgray");
|
||||
|
||||
item.textLines.forEach((line, index) => {
|
||||
this.svgElement.append("text").text(line)
|
||||
.attr("class", "activity")
|
||||
.attr("x", item.textStart + GROUP_OFFSET)
|
||||
.attr("y", item.textY + TIMELINE_HEIGHT + (index * LINE_HEIGHT))
|
||||
.attr('fill', activity.textColor);
|
||||
});
|
||||
//TODO: Ending border
|
||||
},
|
||||
plotCanvas(item, row) {
|
||||
const headingText = item.heading;
|
||||
const { groupHeadingRow, groupHeadingBorder } = this.getGroupHeading(row);
|
||||
|
||||
if (headingText) {
|
||||
if (groupHeadingBorder) {
|
||||
this.canvasContext.strokeStyle = "white";
|
||||
this.canvasContext.beginPath();
|
||||
this.canvasContext.moveTo(0, groupHeadingBorder);
|
||||
this.canvasContext.lineTo(this.width, groupHeadingBorder);
|
||||
this.canvasContext.stroke();
|
||||
}
|
||||
|
||||
this.canvasContext.fillStyle = "white";
|
||||
this.canvasContext.fillText(headingText, 0, groupHeadingRow);
|
||||
}
|
||||
|
||||
const activity = item.activity;
|
||||
const rectX = item.start;
|
||||
const rectY = row + TIMELINE_HEIGHT;
|
||||
const rectWidth = item.rectWidth;
|
||||
this.canvasContext.fillStyle = activity.color;
|
||||
this.canvasContext.strokeStyle = "lightgray";
|
||||
this.canvasContext.fillRect(rectX + GROUP_OFFSET, rectY, rectWidth, ROW_HEIGHT);
|
||||
this.canvasContext.strokeRect(rectX + GROUP_OFFSET, rectY, rectWidth, ROW_HEIGHT);
|
||||
|
||||
this.canvasContext.fillStyle = activity.textColor;
|
||||
|
||||
item.textLines.forEach((line, index) => {
|
||||
this.canvasContext.fillText(line, item.textStart + GROUP_OFFSET, item.textY + TIMELINE_HEIGHT + (index * LINE_HEIGHT));
|
||||
});
|
||||
//TODO: Ending border
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
45
src/plugins/timeline/TimelineViewLayout.vue
Normal file
45
src/plugins/timeline/TimelineViewLayout.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
/*****************************************************************************
|
||||
* 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 ref="planHolder"
|
||||
class="c-timeline"
|
||||
>
|
||||
<plan :rendering-engine="'canvas'" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Plan from './Plan.vue';
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject'],
|
||||
components: {
|
||||
Plan
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
plans: []
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
64
src/plugins/timeline/TimelineViewProvider.js
Normal file
64
src/plugins/timeline/TimelineViewProvider.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 TimelineViewLayout from './TimelineViewLayout.vue';
|
||||
import Vue from 'vue';
|
||||
|
||||
export default function TimelineViewProvider(openmct) {
|
||||
|
||||
return {
|
||||
key: 'timeline.view',
|
||||
name: 'Timeline',
|
||||
cssClass: 'icon-clock',
|
||||
canView(domainObject) {
|
||||
return domainObject.type === 'plan';
|
||||
},
|
||||
|
||||
canEdit(domainObject) {
|
||||
return domainObject.type === 'plan';
|
||||
},
|
||||
|
||||
view: function (domainObject) {
|
||||
let component;
|
||||
|
||||
return {
|
||||
show: function (element) {
|
||||
component = new Vue({
|
||||
el: element,
|
||||
components: {
|
||||
TimelineViewLayout
|
||||
},
|
||||
provide: {
|
||||
openmct,
|
||||
domainObject
|
||||
},
|
||||
template: '<timeline-view-layout></timeline-view-layout>'
|
||||
});
|
||||
},
|
||||
destroy: function () {
|
||||
component.$destroy();
|
||||
component = undefined;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
38
src/plugins/timeline/activities.json
Normal file
38
src/plugins/timeline/activities.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"ROVER": [
|
||||
{
|
||||
"name": "Activity 1",
|
||||
"start": 1597170002854,
|
||||
"end": 1597171032854,
|
||||
"type": "ROVER",
|
||||
"color": "fuchsia",
|
||||
"textColor": "black"
|
||||
},
|
||||
{
|
||||
"name": "Activity 2",
|
||||
"start": 1597171132854,
|
||||
"end": 1597171232854,
|
||||
"type": "ROVER",
|
||||
"color": "fuchsia",
|
||||
"textColor": "black"
|
||||
},
|
||||
{
|
||||
"name": "Activity 4",
|
||||
"start": 1597171132854,
|
||||
"end": 1597171232854,
|
||||
"type": "ROVER",
|
||||
"color": "fuchsia",
|
||||
"textColor": "black"
|
||||
}
|
||||
],
|
||||
"VIPER": [
|
||||
{
|
||||
"name": "Activity 3",
|
||||
"start": 1597170132854,
|
||||
"end": 1597171202854,
|
||||
"type": "VIPER",
|
||||
"color": "fuchsia",
|
||||
"textColor": "black"
|
||||
}
|
||||
]
|
||||
}
|
||||
49
src/plugins/timeline/plugin.js
Normal file
49
src/plugins/timeline/plugin.js
Normal file
@@ -0,0 +1,49 @@
|
||||
/*****************************************************************************
|
||||
* 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 TimelineViewProvider from './TimelineViewProvider';
|
||||
|
||||
export default function () {
|
||||
return function install(openmct) {
|
||||
openmct.types.addType('plan', {
|
||||
name: 'Plan',
|
||||
key: 'plan',
|
||||
description: 'An activity timeline',
|
||||
creatable: true,
|
||||
cssClass: 'icon-timeline',
|
||||
form: [
|
||||
{
|
||||
name: 'Upload Plan (JSON File)',
|
||||
key: 'selectFile',
|
||||
control: 'file-input',
|
||||
required: true,
|
||||
text: 'Select File',
|
||||
type: 'application/json'
|
||||
}
|
||||
],
|
||||
initialize: function (domainObject) {
|
||||
}
|
||||
});
|
||||
openmct.objectViews.addProvider(new TimelineViewProvider(openmct));
|
||||
};
|
||||
}
|
||||
|
||||
205
src/plugins/timeline/pluginSpec.js
Normal file
205
src/plugins/timeline/pluginSpec.js
Normal file
@@ -0,0 +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.
|
||||
*****************************************************************************/
|
||||
|
||||
import { createOpenMct, resetApplicationState } from "utils/testing";
|
||||
import TimelinePlugin from "./plugin";
|
||||
import Vue from 'vue';
|
||||
import TimelineViewLayout from "./TimelineViewLayout.vue";
|
||||
|
||||
describe('the plugin', function () {
|
||||
let planDefinition;
|
||||
let element;
|
||||
let child;
|
||||
let openmct;
|
||||
|
||||
beforeEach((done) => {
|
||||
const appHolder = document.createElement('div');
|
||||
appHolder.style.width = '640px';
|
||||
appHolder.style.height = '480px';
|
||||
|
||||
openmct = createOpenMct();
|
||||
openmct.install(new TimelinePlugin());
|
||||
|
||||
planDefinition = openmct.types.get('plan').definition;
|
||||
|
||||
element = document.createElement('div');
|
||||
element.style.width = '640px';
|
||||
element.style.height = '480px';
|
||||
child = document.createElement('div');
|
||||
child.style.width = '640px';
|
||||
child.style.height = '480px';
|
||||
element.appendChild(child);
|
||||
|
||||
openmct.time.bounds({
|
||||
start: 1597160002854,
|
||||
end: 1597181232854
|
||||
});
|
||||
|
||||
openmct.on('start', done);
|
||||
openmct.startHeadless(appHolder);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
return resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
let mockPlanObject = {
|
||||
name: 'Plan',
|
||||
key: 'plan',
|
||||
creatable: true
|
||||
};
|
||||
|
||||
it('defines a plan object type with the correct key', () => {
|
||||
expect(planDefinition.key).toEqual(mockPlanObject.key);
|
||||
});
|
||||
|
||||
describe('the plan object', () => {
|
||||
|
||||
it('is creatable', () => {
|
||||
expect(planDefinition.creatable).toEqual(mockPlanObject.creatable);
|
||||
});
|
||||
|
||||
it('provides a timeline view', () => {
|
||||
const testViewObject = {
|
||||
id: "test-object",
|
||||
type: "plan"
|
||||
};
|
||||
|
||||
const applicableViews = openmct.objectViews.get(testViewObject);
|
||||
let timelineView = applicableViews.find((viewProvider) => viewProvider.key === 'timeline.view');
|
||||
expect(timelineView).toBeDefined();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('the timeline view displays activities', () => {
|
||||
let planDomainObject;
|
||||
let component;
|
||||
let planViewComponent;
|
||||
|
||||
beforeEach((done) => {
|
||||
planDomainObject = {
|
||||
type: 'plan',
|
||||
id: "test-object",
|
||||
selectFile: {
|
||||
body: JSON.stringify({
|
||||
"TEST-GROUP": [
|
||||
{
|
||||
"name": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua",
|
||||
"start": 1597170002854,
|
||||
"end": 1597171032854,
|
||||
"type": "TEST-GROUP",
|
||||
"color": "fuchsia",
|
||||
"textColor": "black"
|
||||
},
|
||||
{
|
||||
"name": "Sed ut perspiciatis",
|
||||
"start": 1597171132854,
|
||||
"end": 1597171232854,
|
||||
"type": "TEST-GROUP",
|
||||
"color": "fuchsia",
|
||||
"textColor": "black"
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
let viewContainer = document.createElement('div');
|
||||
child.append(viewContainer);
|
||||
component = new Vue({
|
||||
provide: {
|
||||
openmct: openmct,
|
||||
domainObject: planDomainObject
|
||||
},
|
||||
el: viewContainer,
|
||||
components: {
|
||||
TimelineViewLayout
|
||||
},
|
||||
template: '<timeline-view-layout/>'
|
||||
});
|
||||
|
||||
return Vue.nextTick().then(() => {
|
||||
planViewComponent = component.$root.$children[0].$children[0];
|
||||
setTimeout(() => {
|
||||
clearInterval(planViewComponent.resizeTimer);
|
||||
//TODO: this is a hack to ensure the canvas has a width - maybe there's a better way to set the width of the plan div
|
||||
planViewComponent.width = 1200;
|
||||
planViewComponent.setScaleAndPlotActivities();
|
||||
done();
|
||||
}, 300);
|
||||
});
|
||||
});
|
||||
|
||||
it('loads activities into the view', () => {
|
||||
expect(planViewComponent.json).toBeDefined();
|
||||
expect(planViewComponent.json["TEST-GROUP"].length).toEqual(2);
|
||||
});
|
||||
|
||||
it('loads a time axis into the view', () => {
|
||||
let ticks = planViewComponent.axisElement.node().querySelectorAll('g.tick');
|
||||
expect(ticks.length).toEqual(11);
|
||||
});
|
||||
|
||||
it('calculates the activity layout', () => {
|
||||
const expectedActivitiesByRow = {
|
||||
"0": [
|
||||
{
|
||||
"heading": "TEST-GROUP",
|
||||
"activity": {
|
||||
"color": "fuchsia",
|
||||
"textColor": "black"
|
||||
},
|
||||
"textLines": [
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, ",
|
||||
"sed sed do eiusmod tempor incididunt ut labore et "
|
||||
],
|
||||
"textStart": -47.51342439943476,
|
||||
"textY": 12,
|
||||
"start": -47.51625058878945,
|
||||
"end": 204.97315120113046,
|
||||
"rectWidth": -4.9971738106453145
|
||||
}
|
||||
],
|
||||
"42": [
|
||||
{
|
||||
"heading": "",
|
||||
"activity": {
|
||||
"color": "fuchsia",
|
||||
"textColor": "black"
|
||||
},
|
||||
"textLines": [
|
||||
"Sed ut perspiciatis "
|
||||
],
|
||||
"textStart": -48.483749411210546,
|
||||
"textY": 54,
|
||||
"start": -52.99858690532266,
|
||||
"end": 9.032501177578908,
|
||||
"rectWidth": -0.48516250588788523
|
||||
}
|
||||
]
|
||||
};
|
||||
expect(Object.keys(planViewComponent.activitiesByRow)).toEqual(Object.keys(expectedActivitiesByRow));
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
57
src/plugins/timeline/timeline-axis.scss
Normal file
57
src/plugins/timeline/timeline-axis.scss
Normal file
@@ -0,0 +1,57 @@
|
||||
.c-timeline {
|
||||
$h: 18px;
|
||||
$tickYPos: ($h / 2) + 12px + 10px;
|
||||
$tickXPos: 100px;
|
||||
|
||||
height: 100%;
|
||||
|
||||
svg {
|
||||
text-rendering: geometricPrecision;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
> g.axis {
|
||||
// Overall Tick holder
|
||||
transform: translateY($tickYPos) translateX($tickXPos);
|
||||
|
||||
g {
|
||||
//Each tick. These move on drag.
|
||||
line {
|
||||
// Line beneath ticks
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
text:not(.activity) {
|
||||
// Tick labels
|
||||
fill: $colorBodyFg;
|
||||
font-size: 1em;
|
||||
paint-order: stroke;
|
||||
font-weight: bold;
|
||||
stroke: $colorBodyBg;
|
||||
stroke-linecap: butt;
|
||||
stroke-linejoin: bevel;
|
||||
stroke-width: 6px;
|
||||
}
|
||||
|
||||
text.activity {
|
||||
stroke: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.nowMarker {
|
||||
width: 2px;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
background: gray;
|
||||
|
||||
& .icon-arrow-down {
|
||||
font-size: large;
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
left: -8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -173,6 +173,7 @@ $editUIAreaShdw: $editUIAreaBaseColor 0 0 0 2px; // Edit area s-selected-parent
|
||||
$editUIAreaShdwSelected: $editUIAreaBaseColor 0 0 0 3px; // Edit area s-selected
|
||||
$editUIGridColorBg: rgba($editUIBaseColor, 0.2); // Background of layout editing area
|
||||
$editUIGridColorFg: rgba(#000, 0.1); // Grid lines in layout editing area
|
||||
$editDimensionsColor: #6a5ea6;
|
||||
$editFrameColor: $browseFrameColor; // Solid or dotted border applied to non-selected frames in a layout; move-bar on frame hover
|
||||
$editFrameBorder: 1px dotted $editFrameColor;
|
||||
$editFrameColorHov: $editUIColor; // Solid border hover on frames; hover should not be applied to selected objects
|
||||
|
||||
@@ -177,6 +177,7 @@ $editUIAreaShdw: $editUIAreaBaseColor 0 0 0 2px; // Edit area s-selected-parent
|
||||
$editUIAreaShdwSelected: $editUIAreaBaseColor 0 0 0 3px; // Edit area s-selected
|
||||
$editUIGridColorBg: rgba($editUIBaseColor, 0.2); // Background of layout editing area
|
||||
$editUIGridColorFg: rgba(#000, 0.1); // Grid lines in layout editing area
|
||||
$editDimensionsColor: #6a5ea6;
|
||||
$editFrameColor: $browseFrameColor; // Solid or dotted border applied to non-selected frames in a layout; move-bar on frame hover
|
||||
$editFrameBorder: 1px dotted $editFrameColor;
|
||||
$editFrameColorHov: $editUIColor; // Solid border hover on frames; hover should not be applied to selected objects
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user