Merge branch 'master' of https://github.com/nasa/openmct into fix-telemetryview-styles

This commit is contained in:
Joshi 2020-04-01 10:18:30 -07:00
commit dd70bb470f
92 changed files with 4210 additions and 1313 deletions

View File

@ -1,5 +1,5 @@
<!-- <!--
Open MCT, Copyright (c) 2014-2018, United States Government Open MCT, Copyright (c) 2014-2020, United States Government
as represented by the Administrator of the National Aeronautics and Space as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved. Administration. All rights reserved.

View File

@ -1,5 +1,5 @@
/***************************************************************************** /*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government * Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space * as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved. * Administration. All rights reserved.
* *

View File

@ -1,5 +1,5 @@
/***************************************************************************** /*****************************************************************************
* Open MCT, Copyright (c) 2014-2019, United States Government * Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space * as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved. * Administration. All rights reserved.
* *

View File

@ -25,6 +25,7 @@ import uuid from 'uuid';
import TelemetryCriterion from "./criterion/TelemetryCriterion"; import TelemetryCriterion from "./criterion/TelemetryCriterion";
import { TRIGGER } from "./utils/constants"; import { TRIGGER } from "./utils/constants";
import {computeCondition, computeConditionByLimit} from "./utils/evaluator"; import {computeCondition, computeConditionByLimit} from "./utils/evaluator";
import AllTelemetryCriterion from "./criterion/AllTelemetryCriterion";
/* /*
* conditionConfiguration = { * conditionConfiguration = {
@ -72,7 +73,11 @@ export default class ConditionClass extends EventEmitter {
return; return;
} }
this.criteria.forEach(criterion => { this.criteria.forEach(criterion => {
if (criterion.telemetry && (criterion.telemetry === 'all' || criterion.telemetry === 'any')) {
criterion.handleSubscription(datum, this.conditionManager.telemetryObjects);
} else {
criterion.emit(`subscription:${datum.id}`, datum); criterion.emit(`subscription:${datum.id}`, datum);
}
}); });
} }
@ -120,8 +125,13 @@ export default class ConditionClass extends EventEmitter {
* adds criterion to the condition. * adds criterion to the condition.
*/ */
addCriterion(criterionConfiguration) { addCriterion(criterionConfiguration) {
let criterion;
let criterionConfigurationWithId = this.generateCriterion(criterionConfiguration || null); let criterionConfigurationWithId = this.generateCriterion(criterionConfiguration || null);
let criterion = new TelemetryCriterion(criterionConfigurationWithId, this.openmct); if (criterionConfiguration.telemetry && (criterionConfiguration.telemetry === 'any' || criterionConfiguration.telemetry === 'all')) {
criterion = new AllTelemetryCriterion(criterionConfigurationWithId, this.openmct);
} else {
criterion = new TelemetryCriterion(criterionConfigurationWithId, this.openmct);
}
criterion.on('criterionUpdated', (obj) => this.handleCriterionUpdated(obj)); criterion.on('criterionUpdated', (obj) => this.handleCriterionUpdated(obj));
criterion.on('criterionResultUpdated', (obj) => this.handleCriterionResult(obj)); criterion.on('criterionResultUpdated', (obj) => this.handleCriterionResult(obj));
if (!this.criteria) { if (!this.criteria) {

View File

@ -86,7 +86,6 @@
<select v-model="selectedOutputSelection" <select v-model="selectedOutputSelection"
@change="setOutputValue" @change="setOutputValue"
> >
<option value="">- Select Output -</option>
<option v-for="option in outputOptions" <option v-for="option in outputOptions"
:key="option" :key="option"
:value="option" :value="option"

View File

@ -110,10 +110,14 @@ export default {
conditions: [], conditions: [],
telemetryObjs: [], telemetryObjs: [],
moveIndex: Number, moveIndex: Number,
isDragging: false isDragging: false,
defaultOutput: undefined
}; };
}, },
watch: { watch: {
defaultOutput(newOutput, oldOutput) {
this.$emit('updateDefaultOutput', newOutput);
},
testData: { testData: {
handler() { handler() {
this.updateTestData(); this.updateTestData();
@ -141,6 +145,7 @@ export default {
this.observeForChanges(); this.observeForChanges();
this.conditionManager = new ConditionManager(this.domainObject, this.openmct); this.conditionManager = new ConditionManager(this.domainObject, this.openmct);
this.conditionManager.on('conditionSetResultUpdated', this.handleConditionSetResultUpdated); this.conditionManager.on('conditionSetResultUpdated', this.handleConditionSetResultUpdated);
this.updateDefaultCondition();
}, },
methods: { methods: {
handleConditionSetResultUpdated(data) { handleConditionSetResultUpdated(data) {
@ -149,8 +154,14 @@ export default {
observeForChanges() { observeForChanges() {
this.stopObservingForChanges = this.openmct.objects.observe(this.domainObject, 'configuration.conditionCollection', (newConditionCollection) => { this.stopObservingForChanges = this.openmct.objects.observe(this.domainObject, 'configuration.conditionCollection', (newConditionCollection) => {
this.conditionCollection = newConditionCollection; this.conditionCollection = newConditionCollection;
this.updateDefaultCondition();
}); });
}, },
updateDefaultCondition() {
const defaultCondition = this.domainObject.configuration.conditionCollection
.find(conditionConfiguration => conditionConfiguration.isDefault);
this.defaultOutput = defaultCondition.configuration.output;
},
setMoveIndex(index) { setMoveIndex(index) {
this.moveIndex = index; this.moveIndex = index;
this.isDragging = true; this.isDragging = true;

View File

@ -30,7 +30,9 @@
<template v-if="currentConditionOutput"> <template v-if="currentConditionOutput">
{{ currentConditionOutput }} {{ currentConditionOutput }}
</template> </template>
<template v-else>No output selected</template> <template v-else>
{{ defaultConditionOutput }}
</template>
</div> </div>
</section> </section>
<TestData :is-editing="isEditing" <TestData :is-editing="isEditing"
@ -42,6 +44,7 @@
:is-editing="isEditing" :is-editing="isEditing"
:test-data="testData" :test-data="testData"
@conditionSetResultUpdated="updateCurrentOutput" @conditionSetResultUpdated="updateCurrentOutput"
@updateDefaultOutput="updateDefaultOutput"
@telemetryUpdated="updateTelemetry" @telemetryUpdated="updateTelemetry"
/> />
</div> </div>
@ -63,6 +66,7 @@ export default {
data() { data() {
return { return {
currentConditionOutput: '', currentConditionOutput: '',
defaultConditionOutput: '',
telemetryObjs: [], telemetryObjs: [],
testData: {} testData: {}
} }
@ -78,6 +82,9 @@ export default {
updateCurrentOutput(currentConditionResult) { updateCurrentOutput(currentConditionResult) {
this.currentConditionOutput = currentConditionResult.output; this.currentConditionOutput = currentConditionResult.output;
}, },
updateDefaultOutput(output) {
this.currentConditionOutput = output;
},
updateTelemetry(telemetryObjs) { updateTelemetry(telemetryObjs) {
this.telemetryObjs = telemetryObjs; this.telemetryObjs = telemetryObjs;
}, },

View File

@ -1,3 +1,25 @@
/*****************************************************************************
* 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> <template>
<div class="u-contents"> <div class="u-contents">
<div class="c-cdef__separator c-row-separator"></div> <div class="c-cdef__separator c-row-separator"></div>
@ -9,6 +31,8 @@
@change="updateMetadataOptions" @change="updateMetadataOptions"
> >
<option value="">- Select Telemetry -</option> <option value="">- Select Telemetry -</option>
<option value="all">all telemetry</option>
<option value="any">any telemetry</option>
<option v-for="telemetryOption in telemetry" <option v-for="telemetryOption in telemetry"
:key="telemetryOption.identifier.key" :key="telemetryOption.identifier.key"
:value="telemetryOption.identifier" :value="telemetryOption.identifier"
@ -20,7 +44,8 @@
<span v-if="criterion.telemetry" <span v-if="criterion.telemetry"
class="c-cdef__control" class="c-cdef__control"
> >
<select v-model="criterion.metadata" <select ref="metadataSelect"
v-model="criterion.metadata"
@change="updateOperations" @change="updateOperations"
> >
<option value="">- Select Field -</option> <option value="">- Select Field -</option>
@ -36,7 +61,7 @@
class="c-cdef__control" class="c-cdef__control"
> >
<select v-model="criterion.operation" <select v-model="criterion.operation"
@change="updateOperationInputVisibility" @change="updateInputVisibilityAndValues"
> >
<option value="">- Select Comparison -</option> <option value="">- Select Comparison -</option>
<option v-for="option in filteredOps" <option v-for="option in filteredOps"
@ -107,8 +132,7 @@ export default {
}, },
data() { data() {
return { return {
telemetryMetadata: {}, telemetryMetadataOptions: [],
telemetryMetadataOptions: {},
operations: OPERATIONS, operations: OPERATIONS,
inputCount: 0, inputCount: 0,
rowLabel: '', rowLabel: '',
@ -123,13 +147,13 @@ export default {
return (this.index !== 0 ? operator : '') + 'when'; return (this.index !== 0 ? operator : '') + 'when';
}, },
filteredOps: function () { filteredOps: function () {
return [...this.operations.filter(op => op.appliesTo.indexOf(this.operationFormat) !== -1)]; return this.operations.filter(op => op.appliesTo.indexOf(this.operationFormat) !== -1);
}, },
setInputType: function () { setInputType: function () {
let type = ''; let type = '';
for (let i = 0; i < this.filteredOps.length; i++) { for (let i = 0; i < this.filteredOps.length; i++) {
if (this.criterion.operation === this.filteredOps[i].name) { if (this.criterion.operation === this.filteredOps[i].name) {
if (this.filteredOps[i].appliesTo.length === 1) { if (this.filteredOps[i].appliesTo.length) {
type = this.inputTypes[this.filteredOps[i].appliesTo[0]]; type = this.inputTypes[this.filteredOps[i].appliesTo[0]];
} else { } else {
type = 'text' type = 'text'
@ -153,8 +177,11 @@ export default {
}, },
methods: { methods: {
checkTelemetry() { checkTelemetry() {
if(this.criterion.telemetry && if(this.criterion.telemetry) {
!this.telemetry.find((telemetryObj) => this.openmct.objects.areIdsEqual(this.criterion.telemetry, telemetryObj.identifier))) { if (this.criterion.telemetry === 'any' || this.criterion.telemetry === 'all') {
this.updateMetadataOptions();
} else {
if (!this.telemetry.find((telemetryObj) => this.openmct.objects.areIdsEqual(this.criterion.telemetry, telemetryObj.identifier))) {
//telemetry being used was removed. So reset this criterion. //telemetry being used was removed. So reset this criterion.
this.criterion.telemetry = ''; this.criterion.telemetry = '';
this.criterion.metadata = ''; this.criterion.metadata = '';
@ -162,70 +189,103 @@ export default {
this.criterion.operation = ''; this.criterion.operation = '';
this.persist(); this.persist();
} }
}
}
}, },
getOperationFormat() { updateOperationFormat() {
this.enumerations = []; this.enumerations = [];
this.telemetryMetadata.valueMetadatas.forEach((value, index) => { let foundMetadata = this.telemetryMetadataOptions.find((value) => {
if (value.key === this.criterion.metadata) { return value.key === this.criterion.metadata;
let valueMetadata = this.telemetryMetadataOptions[index]; });
if (valueMetadata.enumerations !== undefined) { if (foundMetadata) {
if (foundMetadata.enumerations !== undefined) {
this.operationFormat = 'enum'; this.operationFormat = 'enum';
this.enumerations = valueMetadata.enumerations; this.enumerations = foundMetadata.enumerations;
} else if (valueMetadata.hints.hasOwnProperty('range')) { } else if (foundMetadata.format === 'string' || foundMetadata.format === 'number') {
this.operationFormat = foundMetadata.format;
} else if (foundMetadata.hints.hasOwnProperty('range')) {
this.operationFormat = 'number'; this.operationFormat = 'number';
} else if (valueMetadata.hints.hasOwnProperty('domain')) { } else if (foundMetadata.hints.hasOwnProperty('domain')) {
this.operationFormat = 'number'; this.operationFormat = 'number';
} else if (valueMetadata.key === 'name') { } else if (foundMetadata.key === 'name') {
this.operationFormat = 'string'; this.operationFormat = 'string';
} else { } else {
this.operationFormat = 'string'; this.operationFormat = 'number';
} }
} }
}); this.updateInputVisibilityAndValues();
}, },
updateMetadataOptions(ev) { updateMetadataOptions(ev) {
if (ev) {
this.clearDependentFields(ev.target)
}
if (this.criterion.telemetry) {
this.openmct.objects.get(this.criterion.telemetry).then((telemetryObject) => {
this.telemetryMetadata = this.openmct.telemetry.getMetadata(telemetryObject);
this.telemetryMetadataOptions = this.telemetryMetadata.values();
this.updateOperations(ev);
this.updateOperationInputVisibility();
});
} else {
this.criterion.metadata = '';
}
},
updateOperations(ev) {
if (ev) { if (ev) {
this.clearDependentFields(ev.target); this.clearDependentFields(ev.target);
this.persist(); this.persist();
} }
this.getOperationFormat(); if (this.criterion.telemetry) {
}, const telemetry = (this.criterion.telemetry === 'all' || this.criterion.telemetry === 'any') ? this.telemetry : [{
updateOperationInputVisibility(ev) { identifier: this.criterion.telemetry
if (ev) { }];
if (this.enumerations.length) {
this.criterion.input = [this.enumerations[0].value.toString()]; let telemetryPromises = telemetry.map((telemetryObject) => this.openmct.objects.get(telemetryObject.identifier));
Promise.all(telemetryPromises).then(telemetryObjects => {
this.telemetryMetadataOptions = [];
telemetryObjects.forEach(telemetryObject => {
let telemetryMetadata = this.openmct.telemetry.getMetadata(telemetryObject);
this.addMetaDataOptions(telemetryMetadata.values());
});
this.updateOperations();
});
} }
},
addMetaDataOptions(options) {
if (!this.telemetryMetadataOptions) {
this.telemetryMetadataOptions = options;
}
options.forEach((option) => {
const found = this.telemetryMetadataOptions.find((metadataOption) => {
return (metadataOption.key && (metadataOption.key === option.key)) && (metadataOption.name && (metadataOption.name === option.name))
});
if (!found) {
this.telemetryMetadataOptions.push(option);
}
});
},
updateOperations(ev) {
this.updateOperationFormat();
if (ev) {
this.clearDependentFields(ev.target);
this.persist(); this.persist();
} }
},
updateInputVisibilityAndValues(ev) {
if (ev) {
this.clearDependentFields();
this.persist();
}
for (let i = 0; i < this.filteredOps.length; i++) { for (let i = 0; i < this.filteredOps.length; i++) {
if (this.criterion.operation === this.filteredOps[i].name) { if (this.criterion.operation === this.filteredOps[i].name) {
this.inputCount = this.filteredOps[i].inputCount; this.inputCount = this.filteredOps[i].inputCount;
if (!this.inputCount) {this.criterion.input = []}
} }
} }
if (!this.inputCount) {
this.criterion.input = [];
}
}, },
clearDependentFields(el) { clearDependentFields(el) {
if (el === this.$refs.telemetrySelect) { if (el === this.$refs.telemetrySelect) {
this.criterion.metadata = ''; this.criterion.metadata = '';
} } else if (el === this.$refs.metadataSelect) {
if (!this.filteredOps.find(operation => operation.name === this.criterion.operation)) {
this.criterion.operation = ''; this.criterion.operation = '';
this.criterion.input = []; this.criterion.input = this.enumerations.length ? [this.enumerations[0].value.toString()] : [];
this.inputCount = 0; this.inputCount = 0;
}
} else {
if (this.enumerations.length && !this.criterion.input.length) {
this.criterion.input = [this.enumerations[0].value.toString()];
}
this.inputCount = 0;
}
}, },
persist() { persist() {
this.$emit('persist', this.criterion); this.$emit('persist', this.criterion);

View File

@ -1,3 +1,25 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
.c-cs { .c-cs {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -7,7 +29,7 @@
&__content { &__content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1 1 auto; flex: 0 1 auto;
overflow: hidden; overflow: hidden;
> * { > * {
@ -65,7 +87,7 @@
/***************************** TEST DATA */ /***************************** TEST DATA */
.c-cs-tests { .c-cs-tests {
flex: 1 1 auto; flex: 0 1 auto;
overflow: auto; overflow: auto;
padding-right: $interiorMarginSm; padding-right: $interiorMarginSm;

View File

@ -1,3 +1,25 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
.c-condition, .c-condition,
.c-test-datum { .c-test-datum {
@include discreteItem(); @include discreteItem();
@ -86,7 +108,6 @@
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
align-items: flex-start; align-items: flex-start;
//line-height: 200%;
grid-column: 2; grid-column: 2;
> * > * { > * > * {
@ -103,16 +124,8 @@
width: 100%; width: 100%;
min-height: $interiorMarginSm; min-height: $interiorMarginSm;
//&:before {
// @include test();
// content: '';
// display: block;
// z-index: 2;
//}
&.dragging { &.dragging {
min-height: 5em; min-height: 5em;
//border: solid 1px blue;
background-color: lightblue; background-color: lightblue;
border-radius: 2px; border-radius: 2px;
} }

View File

@ -1,3 +1,25 @@
/*****************************************************************************
* 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> <template>
<li class="c-tree__item-h"> <li class="c-tree__item-h">
<div <div

View File

@ -1,3 +1,25 @@
/*****************************************************************************
* 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> <template>
<div class="u-contents"> <div class="u-contents">
<div class="c-overlay__top-bar"> <div class="c-overlay__top-bar">

View File

@ -1,3 +1,25 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/********************************************* INSPECTOR STYLES TAB */ /********************************************* INSPECTOR STYLES TAB */
.c-inspect-styles { .c-inspect-styles {
> * + * { > * + * {

View File

@ -0,0 +1,172 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import EventEmitter from 'EventEmitter';
import {OPERATIONS} from '../utils/operations';
import {computeCondition} from "@/plugins/condition/utils/evaluator";
export default class TelemetryCriterion extends EventEmitter {
/**
* Subscribes/Unsubscribes to telemetry and emits the result
* of operations performed on the telemetry data returned and a given input value.
* @constructor
* @param telemetryDomainObjectDefinition {id: uuid, operation: enum, input: Array, metadata: string, key: {domainObject.identifier} }
* @param openmct
*/
constructor(telemetryDomainObjectDefinition, openmct) {
super();
this.openmct = openmct;
this.objectAPI = this.openmct.objects;
this.telemetryAPI = this.openmct.telemetry;
this.timeAPI = this.openmct.time;
this.id = telemetryDomainObjectDefinition.id;
this.telemetry = telemetryDomainObjectDefinition.telemetry;
this.operation = telemetryDomainObjectDefinition.operation;
this.telemetryObjects = Object.assign({}, telemetryDomainObjectDefinition.telemetryObjects);
this.input = telemetryDomainObjectDefinition.input;
this.metadata = telemetryDomainObjectDefinition.metadata;
this.telemetryDataCache = {};
}
updateTelemetry(telemetryObjects) {
this.telemetryObjects = Object.assign({}, telemetryObjects);
}
formatData(data, telemetryObjects) {
if (data) {
this.telemetryDataCache[data.id] = this.computeResult(data);
}
let keys = Object.keys(telemetryObjects);
keys.forEach((key) => {
let telemetryObject = telemetryObjects[key];
const id = this.openmct.objects.makeKeyString(telemetryObject.identifier);
if (this.telemetryDataCache[id] === undefined) {
this.telemetryDataCache[id] = false;
}
});
const datum = {
result: computeCondition(this.telemetryDataCache, this.telemetry === 'all')
};
if (data) {
// TODO check back to see if we should format times here
this.timeAPI.getAllTimeSystems().forEach(timeSystem => {
datum[timeSystem.key] = data[timeSystem.key]
});
}
return datum;
}
handleSubscription(data, telemetryObjects) {
if(this.isValid()) {
this.emitEvent('criterionResultUpdated', this.formatData(data, telemetryObjects));
} else {
this.emitEvent('criterionResultUpdated', this.formatData({}, telemetryObjects));
}
}
findOperation(operation) {
for (let i=0; i < OPERATIONS.length; i++) {
if (operation === OPERATIONS[i].name) {
return OPERATIONS[i].operation;
}
}
return null;
}
computeResult(data) {
let result = false;
if (data) {
let comparator = this.findOperation(this.operation);
let params = [];
params.push(data[this.metadata]);
if (this.input instanceof Array && this.input.length) {
this.input.forEach(input => params.push(input));
}
if (typeof comparator === 'function') {
result = comparator(params);
}
}
return result;
}
emitEvent(eventName, data) {
this.emit(eventName, {
id: this.id,
data: data
});
}
isValid() {
return (this.telemetry === 'any' || this.telemetry === 'all') && this.metadata && this.operation;
}
requestLAD(options) {
options = Object.assign({},
options,
{
strategy: 'latest',
size: 1
}
);
if (!this.isValid()) {
return this.formatData({}, options.telemetryObjects);
}
const telemetryRequests = options.telemetryObjects
.map(telemetryObject => this.telemetryAPI.request(
telemetryObject,
options
));
return Promise.all(telemetryRequests)
.then(telemetryRequestsResults => {
telemetryRequestsResults.forEach((results, index) => {
const latestDatum = results.length ? results[results.length - 1] : {};
if (index === telemetryRequestsResults.length-1) {
//when the last result is computed, we return the result
return {
id: this.id,
data: this.formatData(latestDatum, options.telemetryObjects)
};
} else {
if (latestDatum) {
this.telemetryDataCache[latestDatum.id] = this.computeResult(latestDatum);
}
}
});
});
}
destroy() {
this.emitEvent('criterionRemoved');
delete this.telemetryObjects;
delete this.telemetryDataCache;
delete this.telemetryObjectIdAsString;
delete this.telemetryObject;
}
}

View File

@ -71,6 +71,8 @@ export default class TelemetryCriterion extends EventEmitter {
handleSubscription(data) { handleSubscription(data) {
if(this.isValid()) { if(this.isValid()) {
this.emitEvent('criterionResultUpdated', this.formatData(data)); this.emitEvent('criterionResultUpdated', this.formatData(data));
} else {
this.emitEvent('criterionResultUpdated', this.formatData({}));
} }
} }

View File

@ -44,7 +44,7 @@ export default function ConditionPlugin() {
id: uuid(), id: uuid(),
configuration: { configuration: {
name: 'Default', name: 'Default',
output: 'false', output: 'Default',
trigger: 'all', trigger: 'all',
criteria: [] criteria: []
}, },

View File

@ -1,3 +1,25 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
export const TRIGGER = { export const TRIGGER = {
ANY: 'any', ANY: 'any',
ALL: 'all', ALL: 'all',

View File

@ -1,3 +1,25 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
export const computeCondition = (resultMap, allMustBeTrue) => { export const computeCondition = (resultMap, allMustBeTrue) => {
let result = false; let result = false;
for (let key in resultMap) { for (let key in resultMap) {

View File

@ -1,3 +1,25 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import _ from 'lodash'; import _ from 'lodash';
export const OPERATIONS = [ export const OPERATIONS = [

View File

@ -1,3 +1,25 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
export const getStyleProp = (key, defaultValue) => { export const getStyleProp = (key, defaultValue) => {
let styleProp = undefined; let styleProp = undefined;
switch(key) { switch(key) {

View File

@ -1,5 +1,5 @@
/***************************************************************************** /*****************************************************************************
* Open MCT, Copyright (c) 2014-2019, United States Government * Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space * as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved. * Administration. All rights reserved.
* *

View File

@ -1,3 +1,25 @@
/*****************************************************************************
* 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> <template>
<a class="c-condition-widget" <a class="c-condition-widget"
:href="internalDomainObject.url" :href="internalDomainObject.url"

View File

@ -1,3 +1,25 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
.c-condition-widget { .c-condition-widget {
$shdwSize: 3px; $shdwSize: 3px;
@include userSelectNone(); @include userSelectNone();
@ -6,6 +28,10 @@
border: 1px solid transparent; border: 1px solid transparent;
display: inline-block; display: inline-block;
padding: $interiorMarginLg $interiorMarginLg * 2; padding: $interiorMarginLg $interiorMarginLg * 2;
cursor: inherit !important;
&[href] {
cursor: pointer !important;
}
} }
// Make Condition Widget expand when in a hidden frame Layout context // Make Condition Widget expand when in a hidden frame Layout context

View File

@ -1,5 +1,5 @@
/***************************************************************************** /*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government * Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space * as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved. * Administration. All rights reserved.
* *

View File

@ -347,22 +347,6 @@ define(['lodash'], function (_) {
}; };
} }
function getURLButton(selectedParent, selection) {
return {
control: "button",
domainObject: selectedParent,
applicableSelectedItems: selection.filter(selectionPath => {
return selectionPath[0].context.layoutItem.type === 'image-view';
}),
property: function (selectionPath) {
return getPath(selectionPath);
},
icon: "icon-image",
title: "Edit image properties",
dialog: DIALOG_FORM.image
};
}
function getTextButton(selectedParent, selection) { function getTextButton(selectedParent, selection) {
return { return {
control: "button", control: "button",
@ -555,9 +539,6 @@ define(['lodash'], function (_) {
getWidthInput(selectedParent, selectedObjects) getWidthInput(selectedParent, selectedObjects)
]; ];
} }
if (toolbar.url.length === 0) {
toolbar.url = [getURLButton(selectedParent, selectedObjects)];
}
if (toolbar.remove.length === 0) { if (toolbar.remove.length === 0) {
toolbar.remove = [getRemoveButton(selectedParent, selectionPath, selectedObjects)]; toolbar.remove = [getRemoveButton(selectedParent, selectionPath, selectedObjects)];
} }

View File

@ -37,7 +37,7 @@
<script> <script>
import LayoutFrame from './LayoutFrame.vue' import LayoutFrame from './LayoutFrame.vue'
import conditionalStylesMixin from '../mixins/objectlStyles-mixin'; import conditionalStylesMixin from '../mixins/objectStyles-mixin';
export default { export default {
makeDefinition() { makeDefinition() {

View File

@ -37,7 +37,7 @@
<script> <script>
import LayoutFrame from './LayoutFrame.vue' import LayoutFrame from './LayoutFrame.vue'
import conditionalStylesMixin from "../mixins/objectlStyles-mixin"; import conditionalStylesMixin from "../mixins/objectStyles-mixin";
export default { export default {
makeDefinition(openmct, gridSize, element) { makeDefinition(openmct, gridSize, element) {

View File

@ -61,7 +61,7 @@
<script> <script>
import conditionalStylesMixin from "../mixins/objectlStyles-mixin"; import conditionalStylesMixin from "../mixins/objectStyles-mixin";
const START_HANDLE_QUADRANTS = { const START_HANDLE_QUADRANTS = {
1: 'c-frame-edit__handle--sw', 1: 'c-frame-edit__handle--sw',

View File

@ -37,7 +37,7 @@
v-if="showLabel" v-if="showLabel"
class="c-telemetry-view__label" class="c-telemetry-view__label"
:class="[styleClass]" :class="[styleClass]"
:style="telemetryObjectStyle" :style="objectStyle"
> >
<div class="c-telemetry-view__label-text"> <div class="c-telemetry-view__label-text">
{{ domainObject.name }} {{ domainObject.name }}
@ -49,7 +49,7 @@
:title="fieldName" :title="fieldName"
class="c-telemetry-view__value" class="c-telemetry-view__value"
:class="[telemetryClass, !telemetryClass && styleClass]" :class="[telemetryClass, !telemetryClass && styleClass]"
:style="!telemetryClass && telemetryObjectStyle" :style="!telemetryClass && objectStyle"
> >
<div class="c-telemetry-view__value-text"> <div class="c-telemetry-view__value-text">
{{ telemetryValue }} {{ telemetryValue }}
@ -62,7 +62,7 @@
<script> <script>
import LayoutFrame from './LayoutFrame.vue' import LayoutFrame from './LayoutFrame.vue'
import printj from 'printj' import printj from 'printj'
import conditionalStylesMixin from "@/plugins/displayLayout/mixins/objectlStyles-mixin"; import StyleRuleManager from "../../condition/StyleRuleManager";
const DEFAULT_TELEMETRY_DIMENSIONS = [10, 5], const DEFAULT_TELEMETRY_DIMENSIONS = [10, 5],
DEFAULT_POSITION = [1, 1], DEFAULT_POSITION = [1, 1],
@ -91,7 +91,6 @@ export default {
components: { components: {
LayoutFrame LayoutFrame
}, },
mixins: [conditionalStylesMixin],
props: { props: {
item: { item: {
type: Object, type: Object,
@ -114,7 +113,8 @@ export default {
datum: undefined, datum: undefined,
formats: undefined, formats: undefined,
domainObject: undefined, domainObject: undefined,
currentObjectPath: undefined currentObjectPath: undefined,
objectStyle: ''
} }
}, },
computed: { computed: {
@ -135,19 +135,7 @@ export default {
} }
}, },
styleClass() { styleClass() {
return this.telemetryObjectStyle && this.telemetryObjectStyle.isStyleInvisible; return this.objectStyle && this.objectStyle.isStyleInvisible;
},
telemetryObjectStyle() {
let styleObj = Object.assign({}, this.itemStyle);
let keys = Object.keys(styleObj);
keys.forEach(key => {
if ((typeof styleObj[key] === 'string') && (styleObj[key].indexOf('transparent') > -1)) {
if (styleObj[key]) {
styleObj[key] = '';
}
}
});
return styleObj;
}, },
fieldName() { fieldName() {
return this.valueMetadata && this.valueMetadata.name; return this.valueMetadata && this.valueMetadata.name;
@ -202,6 +190,15 @@ export default {
this.removeSelectable(); this.removeSelectable();
} }
if (this.unlistenStyles) {
this.unlistenStyles();
}
if (this.styleRuleManager) {
this.styleRuleManager.destroy();
delete this.styleRuleManager;
}
this.openmct.time.off("bounds", this.refreshData); this.openmct.time.off("bounds", this.refreshData);
}, },
methods: { methods: {
@ -244,6 +241,7 @@ export default {
}, },
setObject(domainObject) { setObject(domainObject) {
this.domainObject = domainObject; this.domainObject = domainObject;
this.initObjectStyles();
this.keyString = this.openmct.objects.makeKeyString(domainObject.identifier); this.keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject); this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
this.limitEvaluator = this.openmct.telemetry.limitEvaluator(this.domainObject); this.limitEvaluator = this.openmct.telemetry.limitEvaluator(this.domainObject);
@ -268,6 +266,30 @@ export default {
}, },
showContextMenu(event) { showContextMenu(event) {
this.openmct.contextMenu._showContextMenuForObjectPath(this.currentObjectPath, event.x, event.y, CONTEXT_MENU_ACTIONS); this.openmct.contextMenu._showContextMenuForObjectPath(this.currentObjectPath, event.x, event.y, CONTEXT_MENU_ACTIONS);
},
initObjectStyles() {
if (this.domainObject.configuration) {
this.styleRuleManager = new StyleRuleManager(this.domainObject.configuration.objectStyles, this.openmct, this.updateStyle.bind(this));
if (this.unlistenStyles) {
this.unlistenStyles();
}
this.unlistenStyles = this.openmct.objects.observe(this.domainObject, 'configuration.objectStyles', (newObjectStyle) => {
//Updating object styles in the inspector view will trigger this so that the changes are reflected immediately
this.styleRuleManager.updateObjectStyleConfig(newObjectStyle);
});
}
},
updateStyle(styleObj) {
let keys = Object.keys(styleObj);
keys.forEach(key => {
if ((typeof styleObj[key] === 'string') && (styleObj[key].indexOf('transparent') > -1)) {
if (styleObj[key]) {
styleObj[key] = '';
}
}
});
this.objectStyle = styleObj;
} }
} }
} }

View File

@ -39,7 +39,7 @@
<script> <script>
import LayoutFrame from './LayoutFrame.vue' import LayoutFrame from './LayoutFrame.vue'
import conditionalStylesMixin from "../mixins/objectlStyles-mixin"; import conditionalStylesMixin from "../mixins/objectStyles-mixin";
export default { export default {
makeDefinition(openmct, gridSize, element) { makeDefinition(openmct, gridSize, element) {

View File

@ -0,0 +1,81 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import StyleRuleManager from "@/plugins/condition/StyleRuleManager";
export default {
inject: ['openmct'],
data() {
return {
itemStyle: this.itemStyle
}
},
mounted() {
this.domainObject = this.$parent.domainObject;
this.itemId = this.item.id;
this.objectStyle = this.getObjectStyleForItem(this.domainObject.configuration.objectStyles);
this.initObjectStyles();
},
destroyed() {
if (this.stopListeningObjectStyles) {
this.stopListeningObjectStyles();
}
},
methods: {
getObjectStyleForItem(objectStyle) {
if (objectStyle) {
return objectStyle[this.itemId] ? Object.assign({}, objectStyle[this.itemId]) : undefined;
} else {
return undefined;
}
},
initObjectStyles() {
if (!this.styleRuleManager) {
this.styleRuleManager = new StyleRuleManager(this.objectStyle, this.openmct, this.updateStyle.bind(this));
} else {
this.styleRuleManager.updateObjectStyleConfig(this.objectStyle);
}
if (this.stopListeningObjectStyles) {
this.stopListeningObjectStyles();
}
this.stopListeningObjectStyles = this.openmct.objects.observe(this.domainObject, 'configuration.objectStyles', (newObjectStyle) => {
//Updating object styles in the inspector view will trigger this so that the changes are reflected immediately
let newItemObjectStyle = this.getObjectStyleForItem(newObjectStyle);
if (this.objectStyle !== newItemObjectStyle) {
this.objectStyle = newItemObjectStyle;
this.styleRuleManager.updateObjectStyleConfig(this.objectStyle);
}
});
},
updateStyle(style) {
this.itemStyle = style;
let keys = Object.keys(this.itemStyle);
keys.forEach((key) => {
if ((typeof this.itemStyle[key] === 'string') && (this.itemStyle[key].indexOf('transparent') > -1)) {
delete this.itemStyle[key];
}
});
}
}
};

View File

@ -0,0 +1,270 @@
<template>
<div class="c-snapshot c-ne__embed">
<div v-if="embed.snapshot"
class="c-ne__embed__snap-thumb"
@click="openSnapshot()"
>
<img :src="embed.snapshot.src">
</div>
<div class="c-ne__embed__info">
<div class="c-ne__embed__name">
<a class="c-ne__embed__link"
:class="embed.cssClass"
@click="changeLocation"
>{{ embed.name }}</a>
<a class="c-ne__embed__context-available icon-arrow-down"
@click="toggleActionMenu"
></a>
</div>
<div class="hide-menu hidden">
<div class="menu-element context-menu-wrapper mobile-disable-select">
<div class="c-menu">
<ul>
<li v-for="action in actions"
:key="action.name"
:class="action.cssClass"
@click="action.perform(embed)"
>
{{ action.name }}
</li>
</ul>
</div>
</div>
</div>
<div v-if="embed.snapshot"
class="c-ne__embed__time"
>
{{ formatTime(embed.createdOn, 'YYYY-MM-DD HH:mm:ss') }}
</div>
</div>
</div>
</template>
<script>
import Moment from 'moment';
import PreviewAction from '../../../ui/preview/PreviewAction';
import Painterro from 'painterro';
import SnapshotTemplate from './snapshot-template.html';
import { togglePopupMenu } from '../utils/popup-menu';
import Vue from 'vue';
export default {
inject: ['openmct'],
components: {
},
props: {
embed: {
type: Object,
default() {
return {};
}
},
removeActionString: {
type: String,
default() {
return 'Remove Embed';
}
}
},
data() {
return {
actions: [this.removeEmbedAction()],
agentService: this.openmct.$injector.get('agentService'),
popupService: this.openmct.$injector.get('popupService')
}
},
watch: {
},
beforeMount() {
this.populateActionMenu();
},
methods: {
annotateSnapshot() {
const self = this;
let save = false;
let painterroInstance = {};
const annotateVue = new Vue({
template: '<div id="snap-annotation"></div>'
});
let annotateOverlay = self.openmct.overlays.overlay({
element: annotateVue.$mount().$el,
size: 'large',
dismissable: false,
buttons: [
{
label: 'Cancel',
callback: function () {
save = false;
painterroInstance.save();
annotateOverlay.dismiss();
}
},
{
label: 'Save',
callback: function () {
save = true;
painterroInstance.save();
annotateOverlay.dismiss();
}
}
],
onDestroy: function () {
annotateVue.$destroy(true);
}
});
painterroInstance = Painterro({
id: 'snap-annotation',
activeColor: '#ff0000',
activeColorAlpha: 1.0,
activeFillColor: '#fff',
activeFillColorAlpha: 0.0,
backgroundFillColor: '#000',
backgroundFillColorAlpha: 0.0,
defaultFontSize: 16,
defaultLineWidth: 2,
defaultTool: 'ellipse',
hiddenTools: ['save', 'open', 'close', 'eraser', 'pixelize', 'rotate', 'settings', 'resize'],
translation: {
name: 'en',
strings: {
lineColor: 'Line',
fillColor: 'Fill',
lineWidth: 'Size',
textColor: 'Color',
fontSize: 'Size',
fontStyle: 'Style'
}
},
saveHandler: function (image, done) {
if (save) {
const url = image.asBlob();
const reader = new window.FileReader();
reader.readAsDataURL(url);
reader.onloadend = function () {
const snapshot = reader.result;
const snapshotObject = {
src: snapshot,
type: url.type,
size: url.size,
modified: Date.now()
};
self.embed.snapshot = snapshotObject;
self.updateEmbed(self.embed);
};
} else {
console.log('You cancelled the annotation!!!');
}
done(true);
}
}).show(this.embed.snapshot.src);
},
changeLocation() {
this.openmct.time.stopClock();
this.openmct.time.bounds({
start: this.embed.bounds.start,
end: this.embed.bounds.end
});
const link = this.embed.historicLink;
if (!link) {
return;
}
window.location.href = link;
const message = 'Time bounds changed to fixed timespan mode';
this.openmct.notifications.alert(message);
},
formatTime(unixTime, timeFormat) {
return Moment.utc(unixTime).format(timeFormat);
},
openSnapshot() {
const self = this;
const snapshot = new Vue({
data: () => {
return {
embed: self.embed
};
},
methods: {
formatTime: self.formatTime,
annotateSnapshot: self.annotateSnapshot
},
template: SnapshotTemplate
});
const snapshotOverlay = this.openmct.overlays.overlay({
element: snapshot.$mount().$el,
onDestroy: () => { snapshot.$destroy(true) },
size: 'large',
dismissable: true,
buttons: [
{
label: 'Done',
emphasis: true,
callback: () => {
snapshotOverlay.dismiss();
}
}
]
});
},
populateActionMenu() {
const self = this;
const actions = [new PreviewAction(self.openmct)];
actions.forEach((action) => {
self.actions.push({
cssClass: action.cssClass,
name: action.name,
perform: () => {
action.invoke(JSON.parse(self.embed.objectPath));
}
});
});
},
removeEmbed(id) {
this.$emit('removeEmbed', id);
},
removeEmbedAction() {
const self = this;
return {
name: self.removeActionString,
cssClass: 'icon-trash',
perform: function (embed) {
const dialog = self.openmct.overlays.dialog({
iconClass: "error",
message: `This action will permanently ${self.removeActionString.toLowerCase()}. Do you wish to continue?`,
buttons: [{
label: "No",
callback: function () {
dialog.dismiss();
}
},
{
label: "Yes",
emphasis: true,
callback: function () {
dialog.dismiss();
self.removeEmbed(embed.id);
}
}]
});
}
};
},
toggleActionMenu(event) {
togglePopupMenu(event, this.openmct);
},
updateEmbed(embed) {
this.$emit('updateEmbed', embed);
}
}
}
</script>

View File

@ -0,0 +1,316 @@
<template>
<div class="c-notebook__entry c-ne has-local-controls"
@dragover="dragover"
@drop.capture="dropCapture"
@drop.prevent="dropOnEntry(entry.id, $event)"
>
<div class="c-ne__time-and-content">
<div class="c-ne__time">
<span>{{ formatTime(entry.createdOn, 'YYYY-MM-DD') }}</span>
<span>{{ formatTime(entry.createdOn, 'HH:mm:ss') }}</span>
</div>
<div class="c-ne__content">
<div :id="entry.id"
class="c-ne__text"
:class="{'c-input-inline' : !readOnly }"
:contenteditable="!readOnly"
:style="!entry.text.length ? defaultEntryStyle : ''"
@blur="textBlur($event, entry.id)"
@focus="textFocus($event, entry.id)"
>{{ entry.text.length ? entry.text : defaultText }}</div>
<div class="c-snapshots c-ne__embeds">
<NotebookEmbed v-for="embed in entry.embeds"
:key="embed.id"
:embed="embed"
:entry="entry"
@removeEmbed="removeEmbed"
@updateEmbed="updateEmbed"
/>
</div>
</div>
</div>
<div v-if="!readOnly"
class="c-ne__local-controls--hidden"
>
<button class="c-icon-button c-icon-button--major icon-trash"
title="Delete this entry"
@click="deleteEntry"
>
</button>
</div>
<div v-if="readOnly"
class="c-ne__section-and-page"
>
<a class="c-click-link"
@click="navigateToSection()"
>
{{ result.section.name }}
</a>
<span class="icon-arrow-right"></span>
<a class="c-click-link"
@click="navigateToPage()"
>
{{ result.page.name }}
</a>
</div>
</div>
</template>
<script>
import NotebookEmbed from './notebook-embed.vue';
import { createNewEmbed, getEntryPosById, getNotebookEntries } from '../utils/notebook-entries';
import Moment from 'moment';
export default {
inject: ['openmct', 'snapshotContainer'],
components: {
NotebookEmbed
},
props: {
domainObject: {
type: Object,
default() {
return {};
}
},
entry: {
type: Object,
default() {
return {};
}
},
result: {
type: Object,
default() {
return {};
}
},
selectedPage: {
type: Object,
default() {
return {};
}
},
selectedSection: {
type: Object,
default() {
return {};
}
},
readOnly: {
type: Boolean,
default() {
return true;
}
}
},
data() {
return {
currentEntryValue: '',
defaultEntryStyle: {
fontStyle: 'italic',
color: '#6e6e6e'
},
defaultText: 'add description'
}
},
watch: {
entry() {
},
readOnly(readOnly) {
},
selectedSection(selectedSection) {
},
selectedPage(selectedSection) {
}
},
mounted() {
this.updateEntries = this.updateEntries.bind(this);
},
beforeDestory() {
},
methods: {
deleteEntry() {
const self = this;
if (!self.domainObject || !self.selectedSection || !self.selectedPage || !self.entry.id) {
return;
}
const entryPosById = this.entryPosById(this.entry.id);
if (entryPosById === -1) {
return;
}
const dialog = this.openmct.overlays.dialog({
iconClass: 'alert',
message: 'This action will permanently delete this entry. Do you wish to continue?',
buttons: [
{
label: "Ok",
emphasis: true,
callback: () => {
const entries = getNotebookEntries(self.domainObject, self.selectedSection, self.selectedPage);
entries.splice(entryPosById, 1);
this.updateEntries(entries);
dialog.dismiss();
}
},
{
label: "Cancel",
callback: () => {
dialog.dismiss();
}
}
]
});
},
dragover() {
event.preventDefault();
event.dataTransfer.dropEffect = "copy";
},
dropCapture(event) {
const isEditing = this.openmct.editor.isEditing();
if (isEditing) {
this.openmct.editor.cancel();
}
},
dropOnEntry(entryId, $event) {
event.stopImmediatePropagation();
if (!this.domainObject || !this.selectedSection || !this.selectedPage) {
return;
}
const snapshotId = $event.dataTransfer.getData('snapshot/id');
if (snapshotId.length) {
this.moveSnapshot(snapshotId);
return;
}
const data = $event.dataTransfer.getData('openmct/domain-object-path');
const objectPath = JSON.parse(data);
const entryPos = this.entryPosById(entryId);
const bounds = this.openmct.time.bounds();
const snapshotMeta = {
bounds,
link: null,
objectPath,
openmct: this.openmct
}
const newEmbed = createNewEmbed(snapshotMeta);
const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
const currentEntryEmbeds = entries[entryPos].embeds;
currentEntryEmbeds.push(newEmbed);
this.updateEntries(entries);
},
entryPosById(entryId) {
return getEntryPosById(entryId, this.domainObject, this.selectedSection, this.selectedPage);
},
findPositionInArray(array, id) {
let position = -1;
array.some((item, index) => {
const found = item.id === id;
if (found) {
position = index;
}
return found;
});
return position;
},
formatTime(unixTime, timeFormat) {
return Moment(unixTime).format(timeFormat);
},
moveSnapshot(snapshotId) {
const snapshot = this.snapshotContainer.getSnapshot(snapshotId);
this.entry.embeds.push(snapshot);
this.updateEntry(this.entry);
this.snapshotContainer.removeSnapshot(snapshotId);
},
navigateToPage() {
this.$emit('changeSectionPage', {
sectionId: this.result.section.id,
pageId: this.result.page.id
});
},
navigateToSection() {
this.$emit('changeSectionPage', {
sectionId: this.result.section.id,
pageId: null
});
},
removeEmbed(id) {
const embedPosition = this.findPositionInArray(this.entry.embeds, id);
this.entry.embeds.splice(embedPosition, 1);
this.updateEntry(this.entry);
},
selectTextInsideElement(element) {
const range = document.createRange();
range.selectNodeContents(element);
var selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
},
textBlur($event, entryId) {
if (!this.domainObject || !this.selectedSection || !this.selectedPage) {
return;
}
const target = $event.target;
if (!target) {
return;
}
const entryPos = this.entryPosById(entryId);
const value = target.textContent.trim();
if (this.currentEntryValue !== value) {
const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
entries[entryPos].text = value;
this.updateEntries(entries);
}
},
textFocus($event) {
if (this.readOnly || !this.domainObject || !this.selectedSection || !this.selectedPage) {
return;
}
const target = $event.target
this.currentEntryValue = target ? target.innerText : '';
if (!this.entry.text.length) {
this.selectTextInsideElement(target);
}
},
updateEmbed(newEmbed) {
let embed = this.entry.embeds.find(e => e.id === newEmbed.id);
if (!embed) {
return;
}
embed = newEmbed;
this.updateEntry(this.entry);
},
updateEntry(newEntry) {
if (!this.domainObject || !this.selectedSection || !this.selectedPage) {
return;
}
const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
entries.forEach(entry => {
if (entry.id === newEntry.id) {
entry = newEntry;
}
});
this.updateEntries(entries);
},
updateEntries(entries) {
this.$emit('updateEntries', entries);
}
}
}
</script>

View File

@ -0,0 +1,114 @@
<template>
<div class="l-browse-bar__view-switcher c-ctrl-wrapper c-ctrl-wrapper--menus-left">
<button
class="c-button--menu icon-notebook"
title="Switch view type"
@click="setNotebookTypes"
@click.stop="toggleMenu"
>
<span class="c-button__label"></span>
</button>
<div
v-show="showMenu"
class="c-menu"
>
<ul>
<li
v-for="(type, index) in notebookTypes"
:key="index"
:class="type.cssClass"
:title="type.name"
@click="snapshot(type)"
>
{{ type.name }}
</li>
</ul>
</div>
</div>
</template>
<script>
import Snapshot from '../snapshot';
import { clearDefaultNotebook, getDefaultNotebook } from '../utils/notebook-storage';
import { NOTEBOOK_DEFAULT, NOTEBOOK_SNAPSHOT } from '../notebook-constants';
export default {
inject: ['openmct'],
props: {
domainObject: {
type: Object,
default() {
return {};
}
}
},
data() {
return {
notebookSnapshot: null,
notebookTypes: [],
showMenu: false
}
},
mounted() {
this.notebookSnapshot = new Snapshot(this.openmct);
document.addEventListener('click', this.hideMenu);
},
destroyed() {
document.removeEventListener('click', this.hideMenu);
},
methods: {
async setNotebookTypes() {
const notebookTypes = [];
let defaultPath = '';
const defaultNotebook = getDefaultNotebook();
if (defaultNotebook) {
const domainObject = await this.openmct.objects.get(defaultNotebook.notebookMeta.identifier)
.then(d => d);
if (!domainObject.location) {
clearDefaultNotebook();
} else {
defaultPath = `${domainObject.name} - ${defaultNotebook.section.name} - ${defaultNotebook.page.name}`;
}
}
if (defaultPath.length !== 0) {
notebookTypes.push({
cssClass: 'icon-notebook',
name: `Save to Notebook ${defaultPath}`,
type: NOTEBOOK_DEFAULT
});
}
notebookTypes.push({
cssClass: 'icon-notebook',
name: 'Save to Notebook Snapshots',
type: NOTEBOOK_SNAPSHOT
});
this.notebookTypes = notebookTypes;
},
toggleMenu() {
this.showMenu = !this.showMenu;
},
hideMenu() {
this.showMenu = false;
},
snapshot(notebook) {
let element = document.getElementsByClassName("l-shell__main-container")[0];
const bounds = this.openmct.time.bounds();
const objectPath = this.openmct.router.path;
const snapshotMeta = {
bounds,
link: window.location.href,
objectPath,
openmct: this.openmct
};
this.notebookSnapshot.capture(snapshotMeta, notebook.type, element);
}
}
}
</script>

View File

@ -0,0 +1,152 @@
<template>
<div class="c-snapshots-h">
<div class="l-browse-bar">
<div class="l-browse-bar__start">
<div class="l-browse-bar__object-name--w icon-notebook">
<div class="l-browse-bar__object-name">
Notebook Snapshots
<span v-if="snapshots.length"
class="l-browse-bar__object-details"
>&nbsp;{{ snapshots.length }} of {{ getNotebookSnapshotMaxCount() }}
</span>
</div>
<a class="l-browse-bar__context-actions c-disclosure-button"
@click="toggleActionMenu"
></a>
<div class="hide-menu hidden">
<div class="menu-element context-menu-wrapper mobile-disable-select">
<div class="c-menu">
<ul>
<li v-for="action in actions"
:key="action.name"
:class="action.cssClass"
@click="action.perform()"
>
{{ action.name }}
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<div class="l-browse-bar__end">
<button class="c-click-icon c-click-icon--major icon-x"
@click="close"
></button>
</div>
</div><!-- closes l-browse-bar -->
<div class="c-snapshots">
<span v-for="snapshot in snapshots"
:key="snapshot.id"
draggable="true"
@dragstart="startEmbedDrag(snapshot, $event)"
>
<NotebookEmbed ref="notebookEmbed"
:key="snapshot.id"
:embed="snapshot"
:remove-action-string="'Delete Snapshot'"
@updateEmbed="updateSnapshot"
@removeEmbed="removeSnapshot"
/>
</span>
<div v-if="!snapshots.length > 0"
class="hint"
>
There are no Notebook Snapshots currently.
</div>
</div>
</div>
</template>
<script>
import NotebookEmbed from './notebook-embed.vue';
import { NOTEBOOK_SNAPSHOT_MAX_COUNT } from '../snapshot-container';
import { EVENT_SNAPSHOTS_UPDATED } from '../notebook-constants';
import { togglePopupMenu } from '../utils/popup-menu';
export default {
inject: ['openmct', 'snapshotContainer'],
components: {
NotebookEmbed
},
props: {
toggleSnapshot: {
type: Function,
default() {
return () => {};
}
}
},
data() {
return {
actions: [this.removeAllSnapshotAction()],
snapshots: []
}
},
mounted() {
this.snapshotContainer.on(EVENT_SNAPSHOTS_UPDATED, this.snapshotsUpdated);
this.snapshots = this.snapshotContainer.getSnapshots();
},
beforeDestory() {
},
methods: {
close() {
this.toggleSnapshot();
},
getNotebookSnapshotMaxCount() {
return NOTEBOOK_SNAPSHOT_MAX_COUNT;
},
removeAllSnapshotAction() {
const self = this;
return {
name: 'Delete All Snapshots',
cssClass: 'icon-trash',
perform: function (embed) {
const dialog = self.openmct.overlays.dialog({
iconClass: "error",
message: 'This action will delete all notebook snapshots. Do you want to continue?',
buttons: [
{
label: "No",
callback: () => {
dialog.dismiss();
}
},
{
label: "Yes",
emphasis: true,
callback: () => {
self.removeAllSnapshots();
dialog.dismiss();
}
}
]
});
}
};
},
removeAllSnapshots() {
this.snapshotContainer.removeAllSnapshots();
},
removeSnapshot(id) {
this.snapshotContainer.removeSnapshot(id);
},
snapshotsUpdated() {
this.snapshots = this.snapshotContainer.getSnapshots();
},
startEmbedDrag(snapshot, event) {
event.dataTransfer.setData('text/plain', snapshot.id);
event.dataTransfer.setData('snapshot/id', snapshot.id);
},
toggleActionMenu(event) {
togglePopupMenu(event, this.openmct);
},
updateSnapshot(snapshot) {
this.snapshotContainer.updateSnapshot(snapshot);
}
}
}
</script>

View File

@ -0,0 +1,97 @@
<template>
<div class="c-indicator c-indicator--clickable icon-notebook"
:class="[
{ 's-status-off': snapshotCount === 0 },
{ 's-status-on': snapshotCount > 0 },
{ 's-status-caution': snapshotCount === snapshotMaxCount },
{ 'has-new-snapshot': flashIndicator }
]"
>
<span class="label c-indicator__label">
{{ indicatorTitle }}
<button @click="toggleSnapshot">
{{ expanded ? 'Hide' : 'Show' }}
</button>
</span>
<span class="c-indicator__count">{{ snapshotCount }}</span>
</div>
</template>
<script>
import SnapshotContainerComponent from './notebook-snapshot-container.vue';
import { EVENT_SNAPSHOTS_UPDATED } from '../notebook-constants';
import { NOTEBOOK_SNAPSHOT_MAX_COUNT } from '../snapshot-container';
import Vue from 'vue';
export default {
inject: ['openmct','snapshotContainer'],
data() {
return {
expanded: false,
indicatorTitle: '',
snapshotCount: 0,
snapshotMaxCount: NOTEBOOK_SNAPSHOT_MAX_COUNT,
flashIndicator: false
}
},
mounted() {
this.snapshotContainer.on(EVENT_SNAPSHOTS_UPDATED, this.snapshotsUpdated);
this.updateSnapshotIndicatorTitle();
},
methods: {
notifyNewSnapshot() {
this.flashIndicator = true;
setTimeout(this.removeNotify, 15000);
},
removeNotify() {
this.flashIndicator = false;
},
snapshotsUpdated() {
if (this.snapshotContainer.getSnapshots().length > this.snapshotCount) {
this.notifyNewSnapshot();
}
this.updateSnapshotIndicatorTitle();
},
toggleSnapshot() {
this.expanded = !this.expanded;
const drawerElement = document.querySelector('.l-shell__drawer');
drawerElement.classList.toggle('is-expanded');
this.updateSnapshotContainer();
},
updateSnapshotContainer() {
const { openmct, snapshotContainer } = this;
const toggleSnapshot = this.toggleSnapshot.bind(this);
const drawerElement = document.querySelector('.l-shell__drawer');
drawerElement.innerHTML = '<div></div>';
const divElement = document.querySelector('.l-shell__drawer div');
this.component = new Vue({
provide: {
openmct,
snapshotContainer
},
el: divElement,
components: {
SnapshotContainerComponent
},
data() {
return {
toggleSnapshot
};
},
template: '<SnapshotContainerComponent :toggleSnapshot="toggleSnapshot"></SnapshotContainerComponent>'
}).$mount();
},
updateSnapshotIndicatorTitle() {
const snapshotCount = this.snapshotContainer.getSnapshots().length;
this.snapshotCount = snapshotCount;
const snapshotTitleSuffix = snapshotCount === 1
? 'Snapshot'
: 'Snapshots';
this.indicatorTitle = `${snapshotCount} ${snapshotTitleSuffix}`;
}
}
}
</script>

View File

@ -0,0 +1,528 @@
<template>
<div class="c-notebook">
<div class="c-notebook__head">
<Search class="c-notebook__search"
:value="search"
@input="throttledSearchItem"
@clear="throttledSearchItem"
/>
</div>
<SearchResults v-if="search.length"
ref="searchResults"
:results="getSearchResults()"
@changeSectionPage="changeSelectedSection"
/>
<div v-if="!search.length"
class="c-notebook__body"
>
<Sidebar ref="sidebar"
class="c-notebook__nav c-sidebar c-drawer c-drawer--align-left"
:class="[{'is-expanded': showNav}, {'c-drawer--push': !sidebarCoversEntries}, {'c-drawer--overlays': sidebarCoversEntries}]"
:default-page-id="defaultPageId"
:default-section-id="defaultSectionId"
:domain-object="internalDomainObject"
:page-title="internalDomainObject.configuration.pageTitle"
:pages="pages"
:section-title="internalDomainObject.configuration.sectionTitle"
:sections="sections"
:sidebar-covers-entries="sidebarCoversEntries"
@updatePage="updatePage"
@updateSection="updateSection"
@toggleNav="toggleNav"
/>
<div class="c-notebook__page-view">
<div class="c-notebook__page-view__header">
<button class="c-notebook__toggle-nav-button c-icon-button c-icon-button--major icon-menu-hamburger"
@click="toggleNav"
></button>
<div class="c-notebook__page-view__path c-path">
<span class="c-notebook__path__section c-path__item">
{{ getSelectedSection() ? getSelectedSection().name : '' }}
</span>
<span class="c-notebook__path__page c-path__item">
{{ getSelectedPage() ? getSelectedPage().name : '' }}
</span>
</div>
<div class="c-notebook__page-view__controls">
<select v-model="showTime"
class="c-notebook__controls__time"
>
<option value="0"
selected="selected"
>
Show all
</option>
<option value="1">Last hour</option>
<option value="8">Last 8 hours</option>
<option value="24">Last 24 hours</option>
</select>
<select v-model="defaultSort"
class="c-notebook__controls__time"
>
<option value="newest"
:selected="defaultSort === 'newest'"
>Newest first</option>
<option value="oldest"
:selected="defaultSort === 'oldest'"
>Oldest first</option>
</select>
</div>
</div>
<div class="c-notebook__drag-area icon-plus"
@click="newEntry()"
@dragover="dragOver"
@drop.capture="dropCapture"
@drop="dropOnEntry($event)"
>
<span class="c-notebook__drag-area__label">
To start a new entry, click here or drag and drop any object
</span>
</div>
<div v-if="selectedSection && selectedPage"
class="c-notebook__entries"
>
<NotebookEntry v-for="entry in filteredAndSortedEntries"
ref="notebookEntry"
:key="entry.id"
:entry="entry"
:domain-object="internalDomainObject"
:selected-page="getSelectedPage()"
:selected-section="getSelectedSection()"
:read-only="false"
@updateEntries="updateEntries"
/>
</div>
</div>
</div>
</div>
</template>
<script>
import NotebookEntry from './notebook-entry.vue';
import Search from '@/ui/components/search.vue';
import SearchResults from './search-results.vue';
import Sidebar from './sidebar.vue';
import { clearDefaultNotebook, getDefaultNotebook, setDefaultNotebook, setDefaultNotebookSection, setDefaultNotebookPage } from '../utils/notebook-storage';
import { addNotebookEntry, createNewEmbed, getNotebookEntries } from '../utils/notebook-entries';
import { throttle } from 'lodash';
const DEFAULT_CLASS = 'is-notebook-default';
export default {
inject: ['openmct', 'domainObject', 'snapshotContainer'],
components: {
NotebookEntry,
Search,
SearchResults,
Sidebar
},
data() {
return {
defaultPageId: getDefaultNotebook() ? getDefaultNotebook().page.id : '',
defaultSectionId: getDefaultNotebook() ? getDefaultNotebook().section.id : '',
defaultSort: this.domainObject.configuration.defaultSort,
internalDomainObject: this.domainObject,
search: '',
showTime: 0,
showNav: false,
sidebarCoversEntries: false
}
},
computed: {
filteredAndSortedEntries() {
const pageEntries = getNotebookEntries(this.internalDomainObject, this.selectedSection, this.selectedPage) || [];
return pageEntries.sort(this.sortEntries);
},
pages() {
return this.getPages() || [];
},
sections() {
return this.internalDomainObject.configuration.sections || [];
},
selectedPage() {
const pages = this.getPages();
if (!pages) {
return null;
}
return pages.find(page => page.isSelected);
},
selectedSection() {
if (!this.sections.length) {
return null;
}
return this.sections.find(section => section.isSelected);
}
},
watch: {
},
beforeMount() {
this.throttledSearchItem = throttle(this.searchItem, 500);
},
mounted() {
this.unlisten = this.openmct.objects.observe(this.internalDomainObject, '*', this.updateInternalDomainObject);
this.formatSidebar();
window.addEventListener('orientationchange', this.formatSidebar);
this.navigateToSectionPage();
},
beforeDestroy() {
if (this.unlisten) {
this.unlisten();
}
},
methods: {
addDefaultClass() {
const classList = this.internalDomainObject.classList || [];
if (classList.includes(DEFAULT_CLASS)) {
return;
}
classList.push(DEFAULT_CLASS);
this.mutateObject('classList', classList);
},
changeSelectedSection({ sectionId, pageId }) {
const sections = this.sections.map(s => {
s.isSelected = false;
if (s.id === sectionId) {
s.isSelected = true;
}
s.pages.forEach((p, i) => {
p.isSelected = false;
if (pageId && pageId === p.id) {
p.isSelected = true;
}
if (!pageId && i === 0) {
p.isSelected = true;
}
});
return s;
});
this.updateSection({ sections });
this.throttledSearchItem('');
},
dragOver(event) {
event.preventDefault();
event.dataTransfer.dropEffect = "copy";
},
dropCapture(event) {
const isEditing = this.openmct.editor.isEditing();
if (isEditing) {
this.openmct.editor.cancel();
}
},
dropOnEntry(event) {
event.preventDefault();
event.stopImmediatePropagation();
const snapshotId = event.dataTransfer.getData('snapshot/id');
if (snapshotId.length) {
const snapshot = this.snapshotContainer.getSnapshot(snapshotId);
this.newEntry(snapshot);
this.snapshotContainer.removeSnapshot(snapshotId);
return;
}
const data = event.dataTransfer.getData('openmct/domain-object-path');
const objectPath = JSON.parse(data);
const bounds = this.openmct.time.bounds();
const snapshotMeta = {
bounds,
link: null,
objectPath,
openmct: this.openmct
};
const embed = createNewEmbed(snapshotMeta);
this.newEntry(embed);
},
formatSidebar() {
/*
Determine if the sidebar should slide over content, or compress it
Slide over checks:
- phone (all orientations)
- tablet portrait
- in a layout frame (within .c-so-view)
*/
const classList = document.querySelector('body').classList;
const isPhone = Array.from(classList).includes('phone');
const isTablet = Array.from(classList).includes('tablet');
const isPortrait = window.screen.orientation.type.includes('portrait');
const isInLayout = !!this.$el.closest('.c-so-view');
const sidebarCoversEntries = (isPhone || (isTablet && isPortrait) || isInLayout);
this.sidebarCoversEntries = sidebarCoversEntries;
},
getDefaultNotebookObject() {
const oldNotebookStorage = getDefaultNotebook();
if (!oldNotebookStorage) {
return null;
}
return this.openmct.objects.get(oldNotebookStorage.notebookMeta.identifier).then(d => d);
},
getPage(section, id) {
return section.pages.find(p => p.id === id);
},
getSection(id) {
return this.sections.find(s => s.id === id);
},
getSearchResults() {
if (!this.search.length) {
return [];
}
const output = [];
const entries = this.internalDomainObject.configuration.entries;
const sectionKeys = Object.keys(entries);
sectionKeys.forEach(sectionKey => {
const pages = entries[sectionKey];
const pageKeys = Object.keys(pages);
pageKeys.forEach(pageKey => {
const pageEntries = entries[sectionKey][pageKey];
pageEntries.forEach(entry => {
if (entry.text && entry.text.toLowerCase().includes(this.search.toLowerCase())) {
const section = this.getSection(sectionKey);
output.push({
section,
page: this.getPage(section, pageKey),
entry
});
}
});
});
});
return output;
},
getPages() {
const selectedSection = this.getSelectedSection();
if (!selectedSection || !selectedSection.pages.length) {
return [];
}
return selectedSection.pages;
},
getSelectedPage() {
const pages = this.getPages();
if (!pages) {
return null;
}
const selectedPage = pages.find(page => page.isSelected);
if (selectedPage) {
return selectedPage;
}
if (!selectedPage && !pages.length) {
return null;
}
pages[0].isSelected = true;
return pages[0];
},
getSelectedSection() {
if (!this.sections.length) {
return null;
}
return this.sections.find(section => section.isSelected);
},
mutateObject(key, value) {
this.openmct.objects.mutate(this.internalDomainObject, key, value);
},
navigateToSectionPage() {
const { pageId, sectionId } = this.openmct.router.getParams();
if(!pageId || !sectionId) {
return;
}
const sections = this.sections.map(s => {
s.isSelected = false;
if (s.id === sectionId) {
s.isSelected = true;
s.pages.forEach(p => p.isSelected = (p.id === pageId));
}
return s;
});
this.updateSection({ sections });
},
newEntry(embed = null) {
const selectedSection = this.getSelectedSection();
const selectedPage = this.getSelectedPage();
this.search = '';
this.updateDefaultNotebook(selectedSection, selectedPage);
const notebookStorage = getDefaultNotebook();
const id = addNotebookEntry(this.openmct, this.internalDomainObject, notebookStorage, embed);
this.$nextTick(() => {
const element = this.$el.querySelector(`#${id}`);
element.focus();
});
return id;
},
orientationChange() {
this.formatSidebar();
},
removeDefaultClass(domainObject) {
if (!domainObject) {
return;
}
const classList = domainObject.classList || [];
const index = classList.indexOf(DEFAULT_CLASS);
if (!classList.length || index < 0) {
return;
}
classList.splice(index, 1);
this.openmct.objects.mutate(domainObject, 'classList', classList);
},
searchItem(input) {
this.search = input;
},
sortEntries(right, left) {
return this.defaultSort === 'newest'
? left.createdOn - right.createdOn
: right.createdOn - left.createdOn;
},
toggleNav() {
this.showNav = !this.showNav;
},
async updateDefaultNotebook(selectedSection, selectedPage) {
const defaultNotebookObject = await this.getDefaultNotebookObject();
this.removeDefaultClass(defaultNotebookObject);
setDefaultNotebook(this.internalDomainObject, selectedSection, selectedPage);
this.addDefaultClass();
this.defaultSectionId = selectedSection.id;
this.defaultPageId = selectedPage.id;
},
updateDefaultNotebookPage(pages, id) {
if (!id) {
return;
}
const notebookStorage = getDefaultNotebook();
if (!notebookStorage
|| notebookStorage.notebookMeta.identifier.key !== this.internalDomainObject.identifier.key) {
return;
}
const defaultNotebookPage = notebookStorage.page;
const page = pages.find(p => p.id === id);
if (!page && defaultNotebookPage.id === id) {
this.defaultSectionId = null;
this.defaultPageId = null
this.removeDefaultClass(this.internalDomainObject);
clearDefaultNotebook();
return;
}
if (id !== defaultNotebookPage.id) {
return;
}
setDefaultNotebookPage(page);
},
updateDefaultNotebookSection(sections, id) {
if (!id) {
return;
}
const notebookStorage = getDefaultNotebook();
if (!notebookStorage
|| notebookStorage.notebookMeta.identifier.key !== this.internalDomainObject.identifier.key) {
return;
}
const defaultNotebookSection = notebookStorage.section;
const section = sections.find(s => s.id === id);
if (!section && defaultNotebookSection.id === id) {
this.defaultSectionId = null;
this.defaultPageId = null
this.removeDefaultClass(this.internalDomainObject);
clearDefaultNotebook();
return;
}
if (section.id !== defaultNotebookSection.id) {
return;
}
setDefaultNotebookSection(section);
},
updateEntries(entries) {
const configuration = this.internalDomainObject.configuration;
const notebookEntries = configuration.entries || {};
notebookEntries[this.selectedSection.id][this.selectedPage.id] = entries;
this.mutateObject('configuration.entries', notebookEntries);
},
updateInternalDomainObject(domainObject) {
this.internalDomainObject = domainObject;
},
updatePage({ pages = [], id = null}) {
const selectedSection = this.getSelectedSection();
if (!selectedSection) {
return;
}
selectedSection.pages = pages;
const sections = this.sections.map(section => {
if (section.id === selectedSection.id) {
section = selectedSection;
}
return section;
});
this.updateSection({ sections });
this.updateDefaultNotebookPage(pages, id);
},
updateParams(sections) {
const selectedSection = sections.find(s => s.isSelected);
if (!selectedSection) {
return;
}
const selectedPage = selectedSection.pages.find(p => p.isSelected);
if (!selectedPage) {
return;
}
const sectionId = selectedSection.id;
const pageId = selectedPage.id;
if (!sectionId || !pageId) {
return;
}
this.openmct.router.updateParams({
sectionId,
pageId
});
},
updateSection({ sections, id = null }) {
this.mutateObject('configuration.sections', sections);
this.updateParams(sections);
this.updateDefaultNotebookSection(sections, id);
}
}
}
</script>

View File

@ -0,0 +1,132 @@
<template>
<ul class="c-list">
<li v-for="page in pages"
:key="page.id"
class="c-list__item-h"
>
<Page ref="pageComponent"
:default-page-id="defaultPageId"
:page="page"
:page-title="pageTitle"
@deletePage="deletePage"
@renamePage="updatePage"
@selectPage="selectPage"
/>
</li>
</ul>
</template>
<script>
import { deleteNotebookEntries } from '../utils/notebook-entries';
import { getDefaultNotebook } from '../utils/notebook-storage';
import Page from './page-component.vue';
export default {
inject: ['openmct'],
components: {
Page
},
props: {
defaultPageId: {
type: String,
default() {
return '';
}
},
domainObject: {
type: Object,
default() {
return {};
}
},
pages: {
type: Array,
required: true,
default() {
return [];
}
},
sections: {
type: Array,
required: true,
default() {
return [];
}
},
pageTitle: {
type: String,
default() {
return '';
}
},
sidebarCoversEntries: {
type: Boolean,
default() {
return false;
}
}
},
data() {
return {
}
},
watch: {
},
mounted() {
},
destroyed() {
},
methods: {
deletePage(id) {
const selectedSection = this.sections.find(s => s.isSelected);
const page = this.pages.filter(p => p.id !== id);
deleteNotebookEntries(this.openmct, this.domainObject, selectedSection, page);
const selectedPage = this.pages.find(p => p.isSelected);
const defaultNotebook = getDefaultNotebook();
const defaultpage = defaultNotebook && defaultNotebook.page;
const isPageSelected = selectedPage && selectedPage.id === id;
const isPageDefault = defaultpage && defaultpage.id === id;
const pages = this.pages.filter(s => s.id !== id);
if (isPageSelected && defaultpage) {
pages.forEach(s => {
s.isSelected = false;
if (defaultpage && defaultpage.id === s.id) {
s.isSelected = true;
}
});
}
if (pages.length && isPageSelected && (!defaultpage || isPageDefault)) {
pages[0].isSelected = true;
}
this.$emit('updatePage', { pages, id });
},
selectPage(id) {
const pages = this.pages.map(page => {
const isSelected = page.id === id;
page.isSelected = isSelected;
return page;
});
this.$emit('updatePage', { pages, id });
// Add test here for whether or not to toggle the nav
if (this.sidebarCoversEntries) {
this.$emit('toggleNav');
}
},
updatePage(newPage) {
const id = newPage.id;
const pages = this.pages.map(page =>
page.id === id
? newPage
: page);
this.$emit('updatePage', { pages, id });
}
}
}
</script>

View File

@ -0,0 +1,146 @@
<template>
<div class="c-list__item js-list__item"
:class="[{ 'is-selected': page.isSelected, 'is-notebook-default' : (defaultPageId === page.id) }]"
:data-id="page.id"
@click="selectPage"
>
<span class="c-list__item__name js-list__item__name"
:data-id="page.id"
@keydown.enter="updateName"
@blur="updateName"
>{{ page.name.length ? page.name : `Unnamed ${pageTitle}` }}</span>
<a class="c-list__item__menu-indicator icon-arrow-down"
@click="toggleActionMenu"
></a>
<div class="hide-menu hidden">
<div class="menu-element context-menu-wrapper mobile-disable-select">
<div class="c-menu">
<ul>
<li v-for="action in actions"
:key="action.name"
:class="action.cssClass"
@click="action.perform(page.id)"
>
{{ action.name }}
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<script>
import { togglePopupMenu } from '../utils/popup-menu';
export default {
inject: ['openmct'],
props: {
defaultPageId: {
type: String,
default() {
return '';
}
},
page: {
type: Object,
required: true
},
pageTitle: {
type: String,
default() {
return '';
}
}
},
data() {
return {
actions: [this.deletePage()]
}
},
watch: {
page(newPage) {
this.toggleContentEditable(newPage);
}
},
mounted() {
this.toggleContentEditable();
},
destroyed() {
},
methods: {
deletePage() {
const self = this;
return {
name: `Delete ${this.pageTitle}`,
cssClass: 'icon-trash',
perform: function (id) {
const dialog = self.openmct.overlays.dialog({
iconClass: "error",
message: 'This action will delete this page and all of its entries. Do you want to continue?',
buttons: [
{
label: "No",
callback: () => {
dialog.dismiss();
}
},
{
label: "Yes",
emphasis: true,
callback: () => {
self.$emit('deletePage', id);
dialog.dismiss();
}
}
]
});
}
};
},
selectPage(event) {
const target = event.target;
const page = target.closest('.js-list__item');
const input = page.querySelector('.js-list__item__name');
if (page.className.indexOf('is-selected') > -1) {
input.contentEditable = true;
input.classList.add('c-input-inline');
return;
}
const id = target.dataset.id;
if (!id) {
return;
}
this.$emit('selectPage', id);
},
toggleActionMenu(event) {
event.preventDefault();
togglePopupMenu(event, this.openmct);
},
toggleContentEditable(page = this.page) {
const pageTitle = this.$el.querySelector('span');
pageTitle.contentEditable = page.isSelected;
},
updateName(event) {
const target = event.target;
const name = target.textContent.toString();
target.contentEditable = false;
target.classList.remove('c-input-inline');
if (this.page.name === name) {
return;
}
if (name === '') {
return;
}
this.$emit('renamePage', Object.assign(this.page, { name }));
}
}
}
</script>

View File

@ -0,0 +1,50 @@
<template>
<div class="c-notebook__search-results">
<div class="c-notebook__search-results__header">Search Results</div>
<div class="c-notebook__entries">
<NotebookEntry v-for="(result, index) in results"
:key="index"
:result="result"
:entry="result.entry"
:read-only="true"
:selected-page="null"
:selected-section="null"
@changeSectionPage="changeSectionPage"
/>
</div>
</div>
</template>
<script>
import NotebookEntry from './notebook-entry.vue';
export default {
inject: ['openmct', 'domainObject'],
components: {
NotebookEntry
},
props:{
results: {
type: Array,
default() {
return [];
}
}
},
data() {
return {}
},
watch: {
results(newResults) {}
},
destroyed() {
},
mounted() {
},
methods: {
changeSectionPage(data) {
this.$emit('changeSectionPage', data);
}
}
}
</script>

View File

@ -0,0 +1,113 @@
<template>
<ul class="c-list">
<li v-for="section in sections"
:key="section.id"
class="c-list__item-h"
>
<sectionComponent ref="sectionComponent"
:default-section-id="defaultSectionId"
:section="section"
:section-title="sectionTitle"
@deleteSection="deleteSection"
@renameSection="updateSection"
@selectSection="selectSection"
/>
</li>
</ul>
</template>
<script>
import { deleteNotebookEntries } from '../utils/notebook-entries';
import { getDefaultNotebook } from '../utils/notebook-storage';
import sectionComponent from './section-component.vue';
export default {
inject: ['openmct'],
components: {
sectionComponent
},
props: {
defaultSectionId: {
type: String,
default() {
return '';
}
},
domainObject: {
type: Object,
default() {
return {};
}
},
sections: {
type: Array,
required: true,
default() {
return [];
}
},
sectionTitle: {
type: String,
default() {
return '';
}
}
},
data() {
return {
}
},
watch: {
},
mounted() {
},
destroyed() {
},
methods: {
deleteSection(id) {
const section = this.sections.find(s => s.id === id);
deleteNotebookEntries(this.openmct, this.domainObject, section);
const selectedSection = this.sections.find(s => s.isSelected);
const defaultNotebook = getDefaultNotebook();
const defaultSection = defaultNotebook && defaultNotebook.section;
const isSectionSelected = selectedSection && selectedSection.id === id;
const isSectionDefault = defaultSection && defaultSection.id === id;
const sections = this.sections.filter(s => s.id !== id);
if (isSectionSelected && defaultSection) {
sections.forEach(s => {
s.isSelected = false;
if (defaultSection && defaultSection.id === s.id) {
s.isSelected = true;
}
});
}
if (sections.length && isSectionSelected && (!defaultSection || isSectionDefault)) {
sections[0].isSelected = true;
}
this.$emit('updateSection', { sections, id });
},
selectSection(id, newSections) {
const currentSections = newSections || this.sections;
const sections = currentSections.map(section => {
const isSelected = section.id === id;
section.isSelected = isSelected;
return section;
});
this.$emit('updateSection', { sections, id });
},
updateSection(newSection) {
const id = newSection.id;
const sections = this.sections.map(section =>
section.id === id
? newSection
: section);
this.$emit('updateSection', { sections, id });
}
}
}
</script>

View File

@ -0,0 +1,149 @@
<template>
<div class="c-list__item js-list__item"
:class="[{ 'is-selected': section.isSelected, 'is-notebook-default' : (defaultSectionId === section.id) }]"
:data-id="section.id"
@click="selectSection"
>
<span class="c-list__item__name js-list__item__name"
:data-id="section.id"
@keydown.enter="updateName"
@blur="updateName"
>{{ section.name.length ? section.name : `Unnamed ${sectionTitle}` }}</span>
<a class="c-list__item__menu-indicator icon-arrow-down"
@click="toggleActionMenu"
></a>
<div class="hide-menu hidden">
<div class="menu-element context-menu-wrapper mobile-disable-select">
<div class="c-menu">
<ul>
<li v-for="action in actions"
:key="action.name"
:class="action.cssClass"
@click="action.perform(section.id)"
>
{{ action.name }}
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<style lang="scss">
</style>
<script>
import { togglePopupMenu } from '../utils/popup-menu';
export default {
inject: ['openmct'],
props: {
defaultSectionId: {
type: String,
default() {
return '';
}
},
section: {
type: Object,
required: true
},
sectionTitle: {
type: String,
default() {
return '';
}
}
},
data() {
return {
actions: [this.deleteSectionAction()]
}
},
watch: {
section(newSection) {
this.toggleContentEditable(newSection);
}
},
mounted() {
this.toggleContentEditable();
},
destroyed() {
},
methods: {
deleteSectionAction() {
const self = this;
return {
name: `Delete ${this.sectionTitle}`,
cssClass: 'icon-trash',
perform: function (id) {
const dialog = self.openmct.overlays.dialog({
iconClass: "error",
message: 'This action will delete this section and all of its pages and entries. Do you want to continue?',
buttons: [
{
label: "No",
callback: () => {
dialog.dismiss();
}
},
{
label: "Yes",
emphasis: true,
callback: () => {
self.$emit('deleteSection', id);
dialog.dismiss();
}
}
]
});
}
};
},
selectSection(event) {
const target = event.target;
const section = target.closest('.js-list__item');
const input = section.querySelector('.js-list__item__name');
if (section.className.indexOf('is-selected') > -1) {
input.contentEditable = true;
input.classList.add('c-input-inline');
return;
}
const id = target.dataset.id;
if (!id) {
return;
}
this.$emit('selectSection', id);
},
toggleActionMenu(event) {
togglePopupMenu(event, this.openmct);
},
toggleContentEditable(section = this.section) {
const sectionTitle = this.$el.querySelector('span');
sectionTitle.contentEditable = section.isSelected;
},
updateName(event) {
const target = event.target;
target.contentEditable = false;
target.classList.remove('c-input-inline');
const name = target.textContent.trim();
if (this.section.name === name) {
return;
}
if (name === '') {
return;
}
this.$emit('renameSection', Object.assign(this.section, { name }));
}
}
}
</script>

View File

@ -0,0 +1,119 @@
.c-sidebar {
@include userSelectNone();
background: $sideBarBg;
display: flex;
justify-content: stretch;
max-width: 300px;
&.c-drawer--push.is-expanded {
margin-right: $interiorMargin;
width: 50%;
}
&.c-drawer--overlays.is-expanded {
width: 95%;
}
> * {
// Hardcoded for two columns
background: $sideBarBg;
display: flex;
flex: 1 1 50%;
flex-direction: column;
+ * {
margin-left: $interiorMarginSm;
}
> * + * {
// Add margin-top to first and second level children
margin-top: $interiorMargin;
}
}
&__pane {
> * + * { margin-top: $interiorMargin; }
}
&__header-w {
// Wraps header, used for page pane with collapse button
display: flex;
flex: 0 0 auto;
background: $sideBarHeaderBg;
align-items: center;
.c-icon-button {
font-size: 0.8em;
color: $colorBodyFg;
}
}
&__header {
color: $sideBarHeaderFg;
display: flex;
flex: 1 1 auto;
padding: $interiorMargin;
text-transform: uppercase;
> * {
@include ellipsize();
}
}
&__contents-and-controls {
// Encloses pane buttons and contents elements
display: flex;
flex-direction: column;
flex: 1 1 auto;
> * {
margin: auto $interiorMargin $interiorMargin $interiorMargin;
&:first-child {
border-bottom: 1px solid $colorInteriorBorder;
flex: 0 0 auto;
}
+ * {
margin-top: $interiorMargin;
}
}
}
&__contents {
flex: 1 1 auto;
overflow-x: hidden;
overflow-y: auto;
padding: auto $interiorMargin;
}
.c-list-button {
.c-button {
font-size: 0.8em;
}
}
.c-list__item {
@include hover() {
[class*="__menu-indicator"] {
opacity: 0.7;
transition: $transIn;
}
}
> * + * {
margin-left: $interiorMargin;
}
&__name {
flex: 0 1 auto;
}
&__menu-indicator {
flex: 0 0 auto;
font-size: 0.8em;
opacity: 0;
transition: $transOut;
}
}
}

View File

@ -0,0 +1,189 @@
<template>
<div class="c-sidebar c-drawer c-drawer--align-left">
<div class="c-sidebar__pane">
<div class="c-sidebar__header-w">
<div class="c-sidebar__header">
<span class="c-sidebar__header-label">{{ sectionTitle }}</span>
</div>
</div>
<div class="c-sidebar__contents-and-controls">
<button class="c-list-button"
@click="addSection"
>
<span class="c-button c-list-button__button icon-plus"></span>
<span class="c-list-button__label">Add {{ sectionTitle }}</span>
</button>
<SectionCollection class="c-sidebar__contents"
:default-section-id="defaultSectionId"
:domain-object="domainObject"
:sections="sections"
:section-title="sectionTitle"
@updateSection="updateSection"
/>
</div>
</div>
<div class="c-sidebar__pane">
<div class="c-sidebar__header-w">
<div class="c-sidebar__header">
<span class="c-sidebar__header-label">{{ pageTitle }}</span>
</div>
<button class="c-click-icon c-click-icon--major icon-x-in-circle"
@click="toggleNav"
></button>
</div>
<div class="c-sidebar__contents-and-controls">
<button class="c-list-button"
@click="addPage"
>
<span class="c-button c-list-button__button icon-plus"></span>
<span class="c-list-button__label">Add {{ pageTitle }}</span>
</button>
<PageCollection ref="pageCollection"
class="c-sidebar__contents"
:default-page-id="defaultPageId"
:domain-object="domainObject"
:pages="pages"
:sections="sections"
:sidebar-covers-entries="sidebarCoversEntries"
:page-title="pageTitle"
@toggleNav="toggleNav"
@updatePage="updatePage"
/>
</div>
</div>
</div>
</template>
<script>
import SectionCollection from './section-collection.vue';
import PageCollection from './page-collection.vue';
import uuid from 'uuid';
export default {
inject: ['openmct'],
components: {
SectionCollection,
PageCollection
},
props: {
defaultPageId: {
type: String,
default() {
return '';
}
},
defaultSectionId: {
type: String,
default() {
return '';
}
},
domainObject: {
type: Object,
default() {
return {};
}
},
pages: {
type: Array,
required: true,
default() {
return [];
}
},
pageTitle: {
type: String,
default() {
return '';
}
},
sections: {
type: Array,
required: true,
default() {
return [];
}
},
sectionTitle: {
type: String,
default() {
return '';
}
},
sidebarCoversEntries: {
type: Boolean,
default() {
return false;
}
}
},
data() {
return {
}
},
watch: {
pages(newpages) {
if (!newpages.length) {
this.addPage();
}
},
sections(newSections) {
if (!newSections.length) {
this.addSection();
}
}
},
mounted() {
if (!this.sections.length) {
this.addSection();
}
},
destroyed() {
},
methods: {
addPage() {
const pageTitle = this.pageTitle;
const id = uuid();
const page = {
id,
isDefault : false,
isSelected: true,
name : `Unnamed ${pageTitle}`,
pageTitle
};
this.pages.forEach(p => p.isSelected = false);
const pages = this.pages.concat(page);
this.updatePage({ pages, id });
},
addSection() {
const sectionTitle = this.sectionTitle;
const id = uuid();
const section = {
id,
isDefault : false,
isSelected: true,
name : `Unnamed ${sectionTitle}`,
pages : [],
sectionTitle
};
this.sections.forEach(s => s.isSelected = false);
const sections = this.sections.concat(section);
this.updateSection({ sections, id });
},
toggleNav() {
this.$emit('toggleNav');
},
updatePage({ pages, id }) {
this.$emit('updatePage', { pages, id });
},
updateSection({ sections, id }) {
this.$emit('updateSection', { sections, id });
}
}
}
</script>

View File

@ -3,10 +3,10 @@
<div class="c-notebook-snapshot__header l-browse-bar"> <div class="c-notebook-snapshot__header l-browse-bar">
<div class="l-browse-bar__start"> <div class="l-browse-bar__start">
<div class="l-browse-bar__object-name--w"> <div class="l-browse-bar__object-name--w">
<span class="l-browse-bar__object-name" <span class="c-object-label l-browse-bar__object-name"
v-bind:class="embed.cssClass" v-bind:class="embed.cssClass"
> >
{{embed.name}} <span class="c-object-label__name">{{ embed.name }}</span>
</span> </span>
</div> </div>
</div> </div>
@ -21,9 +21,8 @@
</div> </div>
</div> </div>
<div class="c-notebook-snapshot__image"> <div class="c-notebook-snapshot__image"
<div class="image-main s-image-main"
:style="{ backgroundImage: 'url(' + embed.snapshot.src + ')' }" :style="{ backgroundImage: 'url(' + embed.snapshot.src + ')' }"
></div> >
</div> </div>
</div> </div>

View File

@ -0,0 +1,3 @@
export const EVENT_SNAPSHOTS_UPDATED = 'SNAPSHOTS_UPDATED';
export const NOTEBOOK_DEFAULT = 'DEFAULT';
export const NOTEBOOK_SNAPSHOT = 'SNAPSHOT';

View File

@ -1,33 +1,11 @@
/***************************************************************************** import Notebook from './components/notebook.vue';
* Open MCT, Copyright (c) 2014-2018, United States Government import NotebookSnapshotIndicator from './components/notebook-snapshot-indicator.vue';
* as represented by the Administrator of the National Aeronautics and Space import SnapshotContainer from './snapshot-container';
* Administration. All rights reserved. import Vue from 'vue';
*
* 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([ let installed = false;
"./src/controllers/NotebookController"
], function (
NotebookController
) {
var installed = false;
function NotebookPlugin() { export default function NotebookPlugin() {
return function install(openmct) { return function install(openmct) {
if (installed) { if (installed) {
return; return;
@ -35,25 +13,25 @@ define([
installed = true; installed = true;
openmct.legacyRegistry.register('notebook', { const notebookType = {
name: 'Notebook Plugin',
extensions: {
types: [
{
key: 'notebook',
name: 'Notebook', name: 'Notebook',
cssClass: 'icon-notebook',
description: 'Create and save timestamped notes with embedded object snapshots.', description: 'Create and save timestamped notes with embedded object snapshots.',
features: 'creation', creatable: true,
model: { cssClass: 'icon-notebook',
entries: [], initialize: domainObject => {
entryTypes: [], domainObject.configuration = {
defaultSort: 'oldest' defaultSort: 'oldest',
entries: {},
pageTitle: 'Page',
sections: [],
sectionTitle: 'Section',
type: 'General'
};
}, },
properties: [ form: [
{ {
key: 'defaultSort', key: 'defaultSort',
name: 'Default Sort', name: 'Entry Sorting',
control: 'select', control: 'select',
options: [ options: [
{ {
@ -65,15 +43,62 @@ define([
value: "oldest" value: "oldest"
} }
], ],
cssClass: 'l-inline' cssClass: 'l-inline',
} property: [
"configuration",
"defaultSort"
]
},
{
key: 'type',
name: 'Note book Type',
control: 'textfield',
cssClass: 'l-inline',
property: [
"configuration",
"type"
]
},
{
key: 'sectionTitle',
name: 'Section Title',
control: 'textfield',
cssClass: 'l-inline',
property: [
"configuration",
"sectionTitle"
]
},
{
key: 'pageTitle',
name: 'Page Title',
control: 'textfield',
cssClass: 'l-inline',
property: [
"configuration",
"pageTitle"
] ]
} }
] ]
} };
}); openmct.types.addType('notebook', notebookType);
openmct.legacyRegistry.enable('notebook'); const snapshotContainer = new SnapshotContainer(openmct);
const notebookSnapshotIndicator = new Vue ({
provide: {
openmct,
snapshotContainer
},
components: {
NotebookSnapshotIndicator
},
template: '<NotebookSnapshotIndicator></NotebookSnapshotIndicator>'
});
const indicator = {
element: notebookSnapshotIndicator.$mount().$el
};
openmct.indicators.add(indicator);
openmct.objectViews.addProvider({ openmct.objectViews.addProvider({
key: 'notebook-vue', key: 'notebook-vue',
@ -83,17 +108,27 @@ define([
return domainObject.type === 'notebook'; return domainObject.type === 'notebook';
}, },
view: function (domainObject) { view: function (domainObject) {
var controller = new NotebookController (openmct, domainObject); let component;
return { return {
show: controller.show, show(container) {
destroy: controller.destroy component = new Vue({
el: container,
components: {
Notebook
},
provide: {
openmct,
domainObject,
snapshotContainer
},
template: '<Notebook></Notebook>'
});
},
destroy() {
component.$destroy();
}
}; };
} }
}); });
}; };
} }
return NotebookPlugin;
});

View File

@ -1,33 +0,0 @@
<div class="c-ne__embed">
<div class="c-ne__embed__snap-thumb"
v-if="embed.snapshot"
v-on:click="openSnapshot(domainObject, entry, embed)">
<img v-bind:src="embed.snapshot.src">
</div>
<div class="c-ne__embed__info">
<div class="c-ne__embed__name">
<a class="c-ne__embed__link"
:href="objectLink"
:class="embed.cssClass">{{embed.name}}</a>
<a class="c-ne__embed__context-available icon-arrow-down"
@click="toggleActionMenu"></a>
</div>
<div class="hide-menu hidden">
<div class="menu-element context-menu-wrapper mobile-disable-select">
<div class="c-menu">
<ul>
<li v-for="action in actions"
:key="action.name"
:class="action.cssClass"
@click="action.perform(embed, entry)">
{{ action.name }}
</li>
</ul>
</div>
</div>
</div>
<div class="c-ne__embed__time" v-if="embed.snapshot">
{{formatTime(embed.createdOn, 'YYYY-MM-DD HH:mm:ss')}}
</div>
</div>
</div>

View File

@ -1,34 +0,0 @@
<li class="c-notebook__entry c-ne has-local-controls"
@drop.prevent="dropOnEntry(entry.id, $event)">
<div class="c-ne__time-and-content">
<div class="c-ne__time">
<span>{{formatTime(entry.createdOn, 'YYYY-MM-DD')}}</span>
<span>{{formatTime(entry.createdOn, 'HH:mm:ss')}}</span>
</div>
<div class="c-ne__content">
<div class="c-ne__text c-input-inline"
contenteditable="true"
ref="contenteditable"
@blur="textBlur($event, entry.id)"
@focus="textFocus($event, entry.id)"
v-html="entry.text">
</div>
<div class="c-ne__embeds">
<notebook-embed
v-for="embed in entry.embeds"
:key="embed.id"
:embed="embed"
:objectPath="embed.objectPath"
:entry="entry">
</notebook-embed>
</div>
</div>
</div>
<div class="c-ne__local-controls--hidden">
<button class="c-icon-button c-icon-button--major icon-trash"
title="Delete this entry"
@click="deleteEntry">
</button>
</div>
</li>

View File

@ -1,35 +0,0 @@
<div class="c-notebook">
<div class="c-notebook__head">
<search class="c-notebook__search"
:value="entrySearch"
@input="search"
@clear="search">
</search>
<div class="c-notebook__controls ">
<select class="c-notebook__controls__time" v-model="showTime">
<option value="0" selected="selected">Show all</option>
<option value="1">Last hour</option>
<option value="8">Last 8 hours</option>
<option value="24">Last 24 hours</option>
</select>
<select class="c-notebook__controls__time" v-model="sortEntries">
<option value="newest" :selected="sortEntries === 'newest'">Newest first</option>
<option value="oldest" :selected="sortEntries === 'oldest'">Oldest first</option>
</select>
</div>
</div>
<div class="c-notebook__drag-area icon-plus"
@click="newEntry($event)"
@drop="newEntry($event)">
<span class="c-notebook__drag-area__label">To start a new entry, click here or drag and drop any object</span>
</div>
<div class="c-notebook__entries">
<ul>
<notebook-entry
v-for="(entry,index) in filteredAndSortedEntries"
:key="entry.key"
:entry="entry">
</notebook-entry>
</ul>
</div>
</div>

View File

@ -1,50 +0,0 @@
<div class="abs overlay l-large-view">
<div class="abs blocker" v-on:click="close"></div>
<div class="abs outer-holder">
<a
class="close icon-x-in-circle"
v-on:click="close">
</a>
<div class="abs inner-holder l-flex-col">
<div class="t-contents flex-elem holder grows">
<div class="t-snapshot abs l-view-header">
<div class="abs object-browse-bar l-flex-row">
<div class="left flex-elem l-flex-row grows">
<div class="object-header flex-elem l-flex-row grows">
<div class="type-icon flex-elem embed-icon holder" v-bind:class="embed.cssClass"></div>
<div class="title-label flex-elem holder flex-can-shrink">{{embed.name}}</div>
</div>
</div>
</div>
</div>
<div class="btn-bar right l-flex-row flex-elem flex-justify-end flex-fixed">
<div class="flex-elem holder flex-can-shrink s-snapshot-datetime">
SNAPSHOT {{formatTime(embed.createdOn, 'YYYY-MM-DD HH:mm:ss')}}
</div>
<a class="s-button icon-pencil" title="Annotate">
<span class="title-label">Annotate</span>
</a>
</div>
<div class="abs object-holder t-image-holder s-image-holder">
<div
class="image-main s-image-main"
v-bind:style="{ backgroundImage: 'url(' + embed.snapshot.src + ')' }">
</div>
</div>
</div>
<div
class="bottom-bar flex-elem holder"
v-on:click="close">
<a class="t-done s-button major">Done</a>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,82 @@
import EventEmitter from 'EventEmitter';
import { EVENT_SNAPSHOTS_UPDATED } from './notebook-constants';
const NOTEBOOK_SNAPSHOT_STORAGE = 'notebook-snapshot-storage';
export const NOTEBOOK_SNAPSHOT_MAX_COUNT = 5;
export default class SnapshotContainer extends EventEmitter {
constructor(openmct) {
super();
if (!SnapshotContainer.instance) {
SnapshotContainer.instance = this;
}
this.openmct = openmct;
return SnapshotContainer.instance;
}
addSnapshot(embedObject) {
const snapshots = this.getSnapshots();
if (snapshots.length >= NOTEBOOK_SNAPSHOT_MAX_COUNT) {
snapshots.pop();
}
snapshots.unshift(embedObject);
return this.saveSnapshots(snapshots);
}
getSnapshot(id) {
const snapshots = this.getSnapshots();
return snapshots.find(s => s.id === id);
}
getSnapshots() {
const snapshots = window.localStorage.getItem(NOTEBOOK_SNAPSHOT_STORAGE) || '[]';
return JSON.parse(snapshots);
}
removeSnapshot(id) {
if (!id) {
return;
}
const snapshots = this.getSnapshots();
const filteredsnapshots = snapshots.filter(snapshot => snapshot.id !== id);
return this.saveSnapshots(filteredsnapshots);
}
removeAllSnapshots() {
return this.saveSnapshots([]);
}
saveSnapshots(snapshots) {
try {
window.localStorage.setItem(NOTEBOOK_SNAPSHOT_STORAGE, JSON.stringify(snapshots));
this.emit(EVENT_SNAPSHOTS_UPDATED, true);
return true;
} catch (e) {
const message = 'Insufficient memory in localstorage to store snapshot, please delete some snapshots and try again!';
this.openmct.notifications.error(message);
return false;
}
}
updateSnapshot(snapshot) {
const snapshots = this.getSnapshots();
const updatedSnapshots = snapshots.map(s => {
return s.id === snapshot.id
? snapshot
: s;
});
return this.saveSnapshots(updatedSnapshots);
}
}

View File

@ -0,0 +1,74 @@
import { addNotebookEntry, createNewEmbed } from './utils/notebook-entries';
import { getDefaultNotebook } from './utils/notebook-storage';
import { NOTEBOOK_DEFAULT } from '@/plugins/notebook/notebook-constants';
import SnapshotContainer from './snapshot-container';
export default class Snapshot {
constructor(openmct) {
this.openmct = openmct;
this.snapshotContainer = new SnapshotContainer(openmct);
this.exportImageService = openmct.$injector.get('exportImageService');
this.dialogService = openmct.$injector.get('dialogService');
this.capture = this.capture.bind(this);
this._saveSnapShot = this._saveSnapShot.bind(this);
}
capture(snapshotMeta, notebookType, domElement) {
this.exportImageService.exportPNGtoSRC(domElement, 's-status-taking-snapshot')
.then(function (blob) {
const reader = new window.FileReader();
reader.readAsDataURL(blob);
reader.onloadend = function () {
this._saveSnapShot(notebookType, reader.result, snapshotMeta);
}.bind(this);
}.bind(this));
}
/**
* @private
*/
_saveSnapShot(notebookType, imageUrl, snapshotMeta) {
const snapshot = imageUrl ? { src: imageUrl } : '';
const embed = createNewEmbed(snapshotMeta, snapshot);
if (notebookType === NOTEBOOK_DEFAULT) {
this._saveToDefaultNoteBook(embed);
return;
}
this._saveToNotebookSnapshots(embed);
}
/**
* @private
*/
_saveToDefaultNoteBook(embed) {
const notebookStorage = getDefaultNotebook();
this.openmct.objects.get(notebookStorage.notebookMeta.identifier)
.then(domainObject => {
addNotebookEntry(this.openmct, domainObject, notebookStorage, embed);
const defaultPath = `${domainObject.name} > ${notebookStorage.section.name} > ${notebookStorage.page.name}`;
const msg = `Saved to Notebook ${defaultPath}`;
this._showNotification(msg);
});
}
/**
* @private
*/
_saveToNotebookSnapshots(embed) {
const saved = this.snapshotContainer.addSnapshot(embed);
if (!saved) {
return;
}
const msg = 'Saved to Notebook Snapshots - click to view.';
this._showNotification(msg);
}
_showNotification(msg) {
this.openmct.notifications.info(msg);
}
}

View File

@ -1,302 +0,0 @@
/*****************************************************************************
* 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([
'moment',
'zepto',
'../../res/templates/snapshotTemplate.html',
'vue',
'painterro'
],
function (
Moment,
$,
SnapshotTemplate,
Vue,
Painterro
) {
function EmbedController(openmct, domainObject) {
this.openmct = openmct;
this.domainObject = domainObject;
this.popupService = openmct.$injector.get('popupService');
this.agentService = openmct.$injector.get('agentService');
this.exposedData = this.exposedData.bind(this);
this.exposedMethods = this.exposedMethods.bind(this);
this.toggleActionMenu = this.toggleActionMenu.bind(this);
}
EmbedController.prototype.openSnapshot = function (domainObject, entry, embed) {
function annotateSnapshot(openmct) {
return function () {
var save = false,
painterroInstance = {},
annotateVue = new Vue({
template: '<div id="snap-annotation"></div>'
}),
self = this;
let annotateOverlay = openmct.overlays.overlay({
element: annotateVue.$mount().$el,
size: 'large',
dismissable: false,
buttons: [
{
label: 'Cancel',
callback: function () {
save = false;
painterroInstance.save();
annotateOverlay.dismiss();
}
},
{
label: 'Save',
callback: function () {
save = true;
painterroInstance.save();
annotateOverlay.dismiss();
}
}
],
onDestroy: function () {
annotateVue.$destroy(true);
}
});
painterroInstance = Painterro({
id: 'snap-annotation',
activeColor: '#ff0000',
activeColorAlpha: 1.0,
activeFillColor: '#fff',
activeFillColorAlpha: 0.0,
backgroundFillColor: '#000',
backgroundFillColorAlpha: 0.0,
defaultFontSize: 16,
defaultLineWidth: 2,
defaultTool: 'ellipse',
hiddenTools: ['save', 'open', 'close', 'eraser', 'pixelize', 'rotate', 'settings', 'resize'],
translation: {
name: 'en',
strings: {
lineColor: 'Line',
fillColor: 'Fill',
lineWidth: 'Size',
textColor: 'Color',
fontSize: 'Size',
fontStyle: 'Style'
}
},
saveHandler: function (image, done) {
if (save) {
var entryPos = self.findInArray(domainObject.entries, entry.id),
embedPos = self.findInArray(entry.embeds, embed.id);
if (entryPos !== -1 && embedPos !== -1) {
var url = image.asBlob(),
reader = new window.FileReader();
reader.readAsDataURL(url);
reader.onloadend = function () {
var snapshot = reader.result,
snapshotObject = {
src: snapshot,
type: url.type,
size: url.size,
modified: Date.now()
},
dirString = 'entries[' + entryPos + '].embeds[' + embedPos + '].snapshot';
openmct.objects.mutate(domainObject, dirString, snapshotObject);
};
}
} else {
console.log('You cancelled the annotation!!!');
}
done(true);
}
}).show(embed.snapshot.src);
};
}
var self = this,
snapshot = new Vue({
data: function () {
return {
embed: self.embed
};
},
methods: {
formatTime: self.formatTime,
annotateSnapshot: annotateSnapshot(self.openmct),
findInArray: self.findInArray
},
template: SnapshotTemplate
});
var snapshotOverlay = this.openmct.overlays.overlay({
element: snapshot.$mount().$el,
onDestroy: () => {snapshot.$destroy(true)},
size: 'large',
dismissable: true,
buttons: [
{
label: 'Done',
emphasis: true,
callback: function () {
snapshotOverlay.dismiss();
}
}
]
});
};
EmbedController.prototype.formatTime = function (unixTime, timeFormat) {
return Moment(unixTime).format(timeFormat);
};
EmbedController.prototype.findInArray = function (array, id) {
var foundId = -1;
array.forEach(function (element, index) {
if (element.id === id) {
foundId = index;
return;
}
});
return foundId;
};
EmbedController.prototype.populateActionMenu = function (openmct, actions) {
return function () {
var self = this;
openmct.objects.get(self.embed.type).then(function (domainObject) {
actions.forEach((action) => {
self.actions.push({
cssClass: action.cssClass,
name: action.name,
perform: () => {
action.invoke([domainObject].concat(openmct.router.path));
}
});
});
});
};
};
EmbedController.prototype.removeEmbedAction = function () {
var self = this;
return {
name: 'Remove Embed',
cssClass: 'icon-trash',
perform: function (embed, entry) {
var entryPosition = self.findInArray(self.domainObject.entries, entry.id),
embedPosition = self.findInArray(entry.embeds, embed.id);
var dialog = self.openmct.overlays.dialog({
iconClass: "alert",
message: 'This Action will permanently delete this embed. Do you wish to continue?',
buttons: [{
label: "No",
callback: function () {
dialog.dismiss();
}
},
{
label: "Yes",
emphasis: true,
callback: function () {
entry.embeds.splice(embedPosition, 1);
var dirString = 'entries[' + entryPosition + '].embeds';
self.openmct.objects.mutate(self.domainObject, dirString, entry.embeds);
dialog.dismiss();
}
}]
});
}
};
};
EmbedController.prototype.toggleActionMenu = function (event) {
event.preventDefault();
var body = $(document.body),
container = $(event.target.parentElement.parentElement),
initiatingEvent = this.agentService.isMobile() ?
'touchstart' : 'mousedown',
menu = container.find('.menu-element'),
dismissExistingMenu;
// Remove the context menu
function dismiss() {
container.find('.hide-menu').append(menu);
body.off(initiatingEvent, menuClickHandler);
dismissExistingMenu = undefined;
}
function menuClickHandler(e) {
window.setTimeout(() => {
dismiss();
}, 100);
}
// Dismiss any menu which was already showing
if (dismissExistingMenu) {
dismissExistingMenu();
}
// ...and record the presence of this menu.
dismissExistingMenu = dismiss;
this.popupService.display(menu, [event.pageX,event.pageY], {
marginX: 0,
marginY: -50
});
body.on(initiatingEvent, menuClickHandler);
};
EmbedController.prototype.exposedData = function () {
return {
actions: [this.removeEmbedAction()],
showActionMenu: false
};
};
EmbedController.prototype.exposedMethods = function () {
var self = this;
return {
openSnapshot: self.openSnapshot,
formatTime: self.formatTime,
toggleActionMenu: self.toggleActionMenu,
findInArray: self.findInArray
};
};
return EmbedController;
});

View File

@ -1,151 +0,0 @@
/*****************************************************************************
* 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([
'moment'
],
function (
Moment
) {
function EntryController(openmct, domainObject) {
this.openmct = openmct;
this.domainObject = domainObject;
this.currentEntryValue = '';
this.exposedMethods = this.exposedMethods.bind(this);
this.exposedData = this.exposedData.bind(this);
}
EntryController.prototype.entryPosById = function (entryId) {
var foundId = -1;
this.domainObject.entries.forEach(function (element, index) {
if (element.id === entryId) {
foundId = index;
return;
}
});
return foundId;
};
EntryController.prototype.textFocus = function ($event) {
if ($event.target) {
this.currentEntryValue = $event.target.innerText;
} else {
$event.target.innerText = '';
}
};
EntryController.prototype.textBlur = function ($event, entryId) {
if ($event.target) {
var entryPos = this.entryPosById(entryId);
if (this.currentEntryValue !== $event.target.innerText) {
this.openmct.objects.mutate(this.domainObject, 'entries[' + entryPos + '].text', $event.target.innerText);
}
}
};
EntryController.prototype.formatTime = function (unixTime, timeFormat) {
return Moment(unixTime).format(timeFormat);
};
EntryController.prototype.deleteEntry = function () {
var entryPos = this.entryPosById(this.entry.id),
domainObject = this.domainObject,
openmct = this.openmct;
if (entryPos !== -1) {
var dialog = this.openmct.overlays.dialog({
iconClass: 'alert',
message: 'This action will permanently delete this entry. Do you wish to continue?',
buttons: [
{
label: "Ok",
emphasis: true,
callback: function () {
domainObject.entries.splice(entryPos, 1);
openmct.objects.mutate(domainObject, 'entries', domainObject.entries);
dialog.dismiss();
}
},
{
label: "Cancel",
callback: function () {
dialog.dismiss();
}
}
]
});
}
};
EntryController.prototype.dropOnEntry = function (entryid, event) {
var data = event.dataTransfer.getData('openmct/domain-object-path');
if (data) {
var objectPath = JSON.parse(data),
domainObject = objectPath[0],
domainObjectKey = domainObject.identifier.key,
domainObjectType = this.openmct.types.get(domainObject.type),
cssClass = domainObjectType && domainObjectType.definition ?
domainObjectType.definition.cssClass : 'icon-object-unknown',
entryPos = this.entryPosById(entryid),
currentEntryEmbeds = this.domainObject.entries[entryPos].embeds,
newEmbed = {
id: '' + Date.now(),
domainObject: domainObject,
objectPath: objectPath,
type: domainObjectKey,
cssClass: cssClass,
name: domainObject.name,
snapshot: ''
};
currentEntryEmbeds.push(newEmbed);
this.openmct.objects.mutate(this.domainObject, 'entries[' + entryPos + '].embeds', currentEntryEmbeds);
}
};
EntryController.prototype.exposedData = function () {
return {
openmct: this.openmct,
domainObject: this.domainObject,
currentEntryValue: this.currentEntryValue
};
};
EntryController.prototype.exposedMethods = function () {
return {
entryPosById: this.entryPosById,
textFocus: this.textFocus,
textBlur: this.textBlur,
formatTime: this.formatTime,
deleteEntry: this.deleteEntry,
dropOnEntry: this.dropOnEntry
};
};
return EntryController;
});

View File

@ -1,237 +0,0 @@
/*****************************************************************************
* 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([
'vue',
'./EntryController',
'./EmbedController',
'../../res/templates/notebook.html',
'../../res/templates/entry.html',
'../../res/templates/embed.html',
'../../../../ui/components/search.vue',
'../../../../ui/preview/PreviewAction',
'../../../../ui/mixins/object-link'
],
function (
Vue,
EntryController,
EmbedController,
NotebookTemplate,
EntryTemplate,
EmbedTemplate,
search,
PreviewAction,
objectLinkMixin
) {
function NotebookController(openmct, domainObject) {
this.openmct = openmct;
this.domainObject = domainObject;
this.entrySearch = '';
this.previewAction = new PreviewAction.default(openmct);
this.show = this.show.bind(this);
this.destroy = this.destroy.bind(this);
this.newEntry = this.newEntry.bind(this);
this.entryPosById = this.entryPosById.bind(this);
}
NotebookController.prototype.initializeVue = function (container) {
var self = this,
entryController = new EntryController(this.openmct, this.domainObject),
embedController = new EmbedController(this.openmct, this.domainObject);
this.container = container;
var notebookEmbed = {
inject:['openmct', 'domainObject'],
mixins:[objectLinkMixin.default],
props:['embed', 'entry'],
template: EmbedTemplate,
data: embedController.exposedData,
methods: embedController.exposedMethods(),
beforeMount: embedController.populateActionMenu(self.openmct, [self.previewAction])
};
var entryComponent = {
props:['entry'],
template: EntryTemplate,
components: {
'notebook-embed': notebookEmbed
},
data: entryController.exposedData,
methods: entryController.exposedMethods(),
mounted: self.focusOnEntry
};
var NotebookVue = Vue.extend({
provide: {openmct: self.openmct, domainObject: self.domainObject},
components: {
'notebook-entry': entryComponent,
'search': search.default
},
data: function () {
return {
entrySearch: self.entrySearch,
showTime: '0',
sortEntries: self.domainObject.defaultSort,
entries: self.domainObject.entries,
currentEntryValue: ''
};
},
computed: {
filteredAndSortedEntries() {
return this.sort(this.filterBySearch(this.entries, this.entrySearch), this.sortEntries);
}
},
methods: {
search(value) {
this.entrySearch = value;
},
newEntry: self.newEntry,
filterBySearch: self.filterBySearch,
sort: self.sort
},
template: NotebookTemplate
});
this.NotebookVue = new NotebookVue();
container.appendChild(this.NotebookVue.$mount().$el);
};
NotebookController.prototype.newEntry = function (event) {
this.NotebookVue.search('');
var date = Date.now(),
embed;
if (event.dataTransfer && event.dataTransfer.getData('openmct/domain-object-path')) {
var selectedObject = JSON.parse(event.dataTransfer.getData('openmct/domain-object-path'))[0],
selectedObjectId = selectedObject.identifier.key,
cssClass = this.openmct.types.get(selectedObject.type);
embed = {
type: selectedObjectId,
id: '' + date,
cssClass: cssClass,
name: selectedObject.name,
snapshot: ''
};
}
var entries = this.domainObject.entries,
lastEntryIndex = this.NotebookVue.sortEntries === 'newest' ? 0 : entries.length - 1,
lastEntry = entries[lastEntryIndex];
if (lastEntry === undefined || lastEntry.text || lastEntry.embeds.length) {
var createdEntry = {'id': 'entry-' + date, 'createdOn': date, 'embeds':[]};
if (embed) {
createdEntry.embeds.push(embed);
}
entries.push(createdEntry);
this.openmct.objects.mutate(this.domainObject, 'entries', entries);
} else {
lastEntry.createdOn = date;
if(embed) {
lastEntry.embeds.push(embed);
}
this.openmct.objects.mutate(this.domainObject, 'entries[entries.length-1]', lastEntry);
this.focusOnEntry.bind(this.NotebookVue.$children[lastEntryIndex+1])();
}
};
NotebookController.prototype.entryPosById = function (entryId) {
var foundId = -1;
this.domainObject.entries.forEach(function (element, index) {
if (element.id === entryId) {
foundId = index;
return;
}
});
return foundId;
};
NotebookController.prototype.focusOnEntry = function () {
if (!this.entry.text) {
this.$refs.contenteditable.focus();
}
};
NotebookController.prototype.filterBySearch = function (entryArray, filterString) {
if (filterString) {
var lowerCaseFilterString = filterString.toLowerCase();
return entryArray.filter(function (entry) {
if (entry.text) {
return entry.text.toLowerCase().includes(lowerCaseFilterString);
} else {
return false;
}
});
} else {
return entryArray;
}
};
NotebookController.prototype.sort = function (array, sortDirection) {
let oldest = (a,b) => {
if (a.createdOn < b.createdOn) {
return -1;
} else if (a.createdOn > b.createdOn) {
return 1;
} else {
return 0;
}
},
newest = (a,b) => {
if (a.createdOn < b.createdOn) {
return 1;
} else if (a.createdOn > b.createdOn) {
return -1;
} else {
return 0;
}
};
if (sortDirection === 'newest') {
return array.sort(newest);
} else {
return array.sort(oldest);
}
};
NotebookController.prototype.show = function (container) {
this.initializeVue(container);
};
NotebookController.prototype.destroy = function (container) {
this.NotebookVue.$destroy(true);
};
return NotebookController;
});

View File

@ -0,0 +1,195 @@
import objectLink from '../../../ui/mixins/object-link';
const TIME_BOUNDS = {
START_BOUND: 'tc.startBound',
END_BOUND: 'tc.endBound',
START_DELTA: 'tc.startDelta',
END_DELTA: 'tc.endDelta'
}
export const getHistoricLinkInFixedMode = (openmct, bounds, historicLink) => {
if (historicLink.includes('tc.mode=fixed')) {
return historicLink;
}
openmct.time.getAllClocks().forEach(clock => {
if (historicLink.includes(`tc.mode=${clock.key}`)) {
historicLink.replace(`tc.mode=${clock.key}`, 'tc.mode=fixed');
return;
}
});
const params = historicLink.split('&').map(param => {
if (param.includes(TIME_BOUNDS.START_BOUND)
|| param.includes(TIME_BOUNDS.START_DELTA)) {
param = `${TIME_BOUNDS.START_BOUND}=${bounds.start}`;
}
if (param.includes(TIME_BOUNDS.END_BOUND)
|| param.includes(TIME_BOUNDS.END_DELTA)) {
param = `${TIME_BOUNDS.END_BOUND}=${bounds.end}`;
}
return param;
});
return params.join('&');
}
export const getNotebookDefaultEntries = (notebookStorage, domainObject) => {
if (!notebookStorage || !domainObject) {
return null;
}
const defaultSection = notebookStorage.section;
const defaultPage = notebookStorage.page;
if (!defaultSection || !defaultPage) {
return null;
}
const configuration = domainObject.configuration;
const entries = configuration.entries || {};
let section = entries[defaultSection.id];
if (!section) {
section = {};
entries[defaultSection.id] = section;
}
let page = entries[defaultSection.id][defaultPage.id];
if (!page) {
page = [];
entries[defaultSection.id][defaultPage.id] = [];
}
return entries[defaultSection.id][defaultPage.id];
}
export const createNewEmbed = (snapshotMeta, snapshot = '') => {
const {
bounds,
link,
objectPath,
openmct
} = snapshotMeta;
const domainObject = objectPath[0];
const domainObjectType = openmct.types.get(domainObject.type);
const cssClass = domainObjectType && domainObjectType.definition
? domainObjectType.definition.cssClass
: 'icon-object-unknown';
const date = Date.now();
const historicLink = link
? getHistoricLinkInFixedMode(openmct, bounds, link)
: objectLink.computed.objectLink.call({ objectPath, openmct });
const name = domainObject.name;
const type = domainObject.identifier.key;
return {
bounds,
createdOn: date,
cssClass,
domainObject,
historicLink,
id: 'embed-' + date,
name,
snapshot,
type,
objectPath: JSON.stringify(objectPath)
};
}
export const addNotebookEntry = (openmct, domainObject, notebookStorage, embed = null) => {
if (!openmct || !domainObject || !notebookStorage) {
return;
}
const date = Date.now();
const configuration = domainObject.configuration;
const entries = configuration.entries || {};
if (!entries) {
return;
}
const embeds = embed
? [embed]
: [];
const defaultEntries = getNotebookDefaultEntries(notebookStorage, domainObject);
const id = `entry-${date}`;
defaultEntries.push({
id,
createdOn: date,
text: '',
embeds
});
openmct.objects.mutate(domainObject, 'configuration.entries', entries);
return id;
}
export const getNotebookEntries = (domainObject, selectedSection, selectedPage) => {
if (!domainObject || !selectedSection || !selectedPage) {
return null;
}
const configuration = domainObject.configuration;
const entries = configuration.entries || {};
let section = entries[selectedSection.id];
if (!section) {
return null;
}
let page = entries[selectedSection.id][selectedPage.id];
if (!page) {
return null;
}
return entries[selectedSection.id][selectedPage.id];
}
export const getEntryPosById = (entryId, domainObject, selectedSection, selectedPage) => {
if (!domainObject || !selectedSection || !selectedPage) {
return;
}
const entries = getNotebookEntries(domainObject, selectedSection, selectedPage);
let foundId = -1;
entries.forEach((element, index) => {
if (element.id === entryId) {
foundId = index;
return;
}
});
return foundId;
}
export const deleteNotebookEntries = (openmct, domainObject, selectedSection, selectedPage) => {
if (!domainObject || !selectedSection) {
return;
}
const configuration = domainObject.configuration;
const entries = configuration.entries || {};
// Delete entire section
if (!selectedPage) {
delete entries[selectedSection.id];
return;
}
let section = entries[selectedSection.id];
if (!section) {
return;
}
delete entries[selectedSection.id][selectedPage.id];
openmct.objects.mutate(domainObject, 'configuration.entries', entries);
}

View File

@ -0,0 +1,40 @@
const NOTEBOOK_LOCAL_STORAGE = 'notebook-storage';
export function clearDefaultNotebook() {
window.localStorage.setItem(NOTEBOOK_LOCAL_STORAGE, null);
}
export function getDefaultNotebook() {
const notebookStorage = window.localStorage.getItem(NOTEBOOK_LOCAL_STORAGE);
return JSON.parse(notebookStorage);
}
export function setDefaultNotebook(domainObject, section, page) {
const notebookMeta = {
name: domainObject.name,
identifier: domainObject.identifier
};
const notebookStorage = {
notebookMeta,
section,
page
}
window.localStorage.setItem(NOTEBOOK_LOCAL_STORAGE, JSON.stringify(notebookStorage));
}
export function setDefaultNotebookSection(section) {
const notebookStorage = getDefaultNotebook();
notebookStorage.section = section;
window.localStorage.setItem(NOTEBOOK_LOCAL_STORAGE, JSON.stringify(notebookStorage));
}
export function setDefaultNotebookPage(page) {
const notebookStorage = getDefaultNotebook();
notebookStorage.page = page;
window.localStorage.setItem(NOTEBOOK_LOCAL_STORAGE, JSON.stringify(notebookStorage));
}

View File

@ -0,0 +1,45 @@
import $ from 'zepto';
export const togglePopupMenu = (event, openmct) => {
event.preventDefault();
const body = $(document.body);
const container = $(event.target.parentElement.parentElement);
const classList = document.querySelector('body').classList;
const isPhone = Array.from(classList).includes('phone');
const isTablet = Array.from(classList).includes('tablet');
const initiatingEvent = isPhone || isTablet
? 'touchstart'
: 'mousedown';
const menu = container.find('.menu-element');
let dismissExistingMenu;
function dismiss() {
container.find('.hide-menu').append(menu);
body.off(initiatingEvent, menuClickHandler);
dismissExistingMenu = undefined;
}
function menuClickHandler(e) {
window.setTimeout(() => {
dismiss();
}, 100);
}
// Dismiss any menu which was already showing
if (dismissExistingMenu) {
dismissExistingMenu();
}
// ...and record the presence of this menu.
dismissExistingMenu = dismiss;
const popupService = openmct.$injector.get('popupService');
popupService.display(menu, [event.pageX,event.pageY], {
marginX: 0,
marginY: -50
});
body.on(initiatingEvent, menuClickHandler);
}

View File

@ -1,5 +1,5 @@
/***************************************************************************** /*****************************************************************************
* Open MCT, Copyright (c) 2014-2019, United States Government * Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space * as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved. * Administration. All rights reserved.
* *
@ -175,7 +175,7 @@ define([
plugins.SummaryWidget = SummaryWidget; plugins.SummaryWidget = SummaryWidget;
plugins.TelemetryMean = TelemetryMean; plugins.TelemetryMean = TelemetryMean;
plugins.URLIndicator = URLIndicatorPlugin; plugins.URLIndicator = URLIndicatorPlugin;
plugins.Notebook = Notebook; plugins.Notebook = Notebook.default;
plugins.DisplayLayout = DisplayLayoutPlugin.default; plugins.DisplayLayout = DisplayLayoutPlugin.default;
plugins.FolderView = FolderView; plugins.FolderView = FolderView;
plugins.Tabs = Tabs; plugins.Tabs = Tabs;

View File

@ -19,7 +19,6 @@
* this source code distribution or the Licensing information page available * this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
export default class RemoveAction { export default class RemoveAction {
constructor(openmct) { constructor(openmct) {
this.name = 'Remove'; this.name = 'Remove';
@ -90,6 +89,13 @@ export default class RemoveAction {
if (this.inNavigationPath(child) && this.openmct.editor.isEditing()) { if (this.inNavigationPath(child) && this.openmct.editor.isEditing()) {
this.openmct.editor.save(); this.openmct.editor.save();
} }
const parentKeyString = this.openmct.objects.makeKeyString(parent.identifier);
const isAlias = parentKeyString !== child.location;
if (!isAlias) {
this.openmct.objects.mutate(child, 'location', null);
}
} }
appliesTo(objectPath) { appliesTo(objectPath) {

View File

@ -56,6 +56,10 @@ define(function () {
return formattedValue; return formattedValue;
} }
} }
getParsedValue(telemetryDatum) {
return this.formatter.parse(telemetryDatum);
}
} }
return TelemetryTableColumn; return TelemetryTableColumn;

View File

@ -42,6 +42,11 @@ define([], function () {
return column && column.getFormattedValue(this.datum[key]); return column && column.getFormattedValue(this.datum[key]);
} }
getParsedValue(key) {
let column = this.columns[key];
return column && column.getParsedValue(this.datum[key]);
}
getCellComponentName(key) { getCellComponentName(key) {
let column = this.columns[key]; let column = this.columns[key];
return column && return column &&

View File

@ -201,7 +201,7 @@ define(
sortBy(sortOptions) { sortBy(sortOptions) {
if (arguments.length > 0) { if (arguments.length > 0) {
this.sortOptions = sortOptions; this.sortOptions = sortOptions;
this.rows = _.sortByOrder(this.rows, 'datum.' + sortOptions.key, sortOptions.direction); this.rows = _.sortByOrder(this.rows, (row) => row.getParsedValue(sortOptions.key) , sortOptions.direction);
this.emit('sort'); this.emit('sort');
} }
// Return duplicate to avoid direct modification of underlying object // Return duplicate to avoid direct modification of underlying object
@ -222,7 +222,7 @@ define(
} }
getValueForSortColumn(row) { getValueForSortColumn(row) {
return row.datum[this.sortOptions.key]; return row.getParsedValue(this.sortOptions.key);
} }
remove(removedRows) { remove(removedRows) {

View File

@ -87,6 +87,11 @@ $colorSelectedFg: pullForward($colorBodyFg, 20%);
// Layout // Layout
$shellMainPad: 4px 0; $shellMainPad: 4px 0;
$shellPanePad: $interiorMargin, 7px; $shellPanePad: $interiorMargin, 7px;
$drawerBg: lighten($colorBodyBg, 5%);
$drawerFg: lighten($colorBodyFg, 5%);
$sideBarBg: $drawerBg;
$sideBarHeaderBg: rgba($colorBodyFg, 0.2);
$sideBarHeaderFg: rgba($colorBodyFg, 0.7);
// Status colors, mainly used for messaging and item ancillary symbols // Status colors, mainly used for messaging and item ancillary symbols
$colorStatusFg: #999; $colorStatusFg: #999;
@ -333,7 +338,7 @@ $colorSummaryFg: rgba($colorBodyFg, 0.7);
$colorSummaryFgEm: $colorBodyFg; $colorSummaryFgEm: $colorBodyFg;
// Plot // Plot
$colorPlotBg: rgba(black, 0.05); $colorPlotBg: rgba(black, 0.1);
$colorPlotFg: $colorBodyFg; $colorPlotFg: $colorBodyFg;
$colorPlotHash: black; $colorPlotHash: black;
$opacityPlotHash: 0.2; $opacityPlotHash: 0.2;

View File

@ -91,6 +91,11 @@ $colorSelectedFg: pullForward($colorBodyFg, 20%);
// Layout // Layout
$shellMainPad: 4px 0; $shellMainPad: 4px 0;
$shellPanePad: $interiorMargin, 7px; $shellPanePad: $interiorMargin, 7px;
$drawerBg: lighten($colorBodyBg, 5%);
$drawerFg: lighten($colorBodyFg, 5%);
$sideBarBg: $drawerBg;
$sideBarHeaderBg: rgba($colorBodyFg, 0.2);
$sideBarHeaderFg: rgba($colorBodyFg, 0.7);
// Status colors, mainly used for messaging and item ancillary symbols // Status colors, mainly used for messaging and item ancillary symbols
$colorStatusFg: #999; $colorStatusFg: #999;

View File

@ -26,7 +26,7 @@
$mobileListIconSize: 30px; $mobileListIconSize: 30px;
$mobileTitleDescH: 35px; $mobileTitleDescH: 35px;
$mobileOverlayMargin: 20px; $mobileOverlayMargin: 20px;
$mobileMenuIconD: 34px; $mobileMenuIconD: 25px;
$phoneItemH: floor($gridItemMobile / 4); $phoneItemH: floor($gridItemMobile / 4);
$tabletItemH: floor($gridItemMobile / 3); $tabletItemH: floor($gridItemMobile / 3);

View File

@ -87,6 +87,11 @@ $colorSelectedFg: pullForward($colorBodyFg, 10%);
// Layout // Layout
$shellMainPad: 4px 0; $shellMainPad: 4px 0;
$shellPanePad: $interiorMargin, 7px; $shellPanePad: $interiorMargin, 7px;
$drawerBg: darken($colorBodyBg, 5%);
$drawerFg: darken($colorBodyFg, 5%);
$sideBarBg: $drawerBg;
$sideBarHeaderBg: rgba(black, 0.25);
$sideBarHeaderFg: rgba($colorBodyFg, 0.7);
// Status colors, mainly used for messaging and item ancillary symbols // Status colors, mainly used for messaging and item ancillary symbols
$colorStatusFg: #999; $colorStatusFg: #999;

View File

@ -147,6 +147,7 @@ $glyph-icon-suitcase: '\e928';
$glyph-icon-cursor-lock: '\e929'; $glyph-icon-cursor-lock: '\e929';
$glyph-icon-flag: '\e92a'; $glyph-icon-flag: '\e92a';
$glyph-icon-eye-disabled: '\e92b'; $glyph-icon-eye-disabled: '\e92b';
$glyph-icon-notebook-page: '\e92c';
$glyph-icon-arrows-right-left: '\ea00'; $glyph-icon-arrows-right-left: '\ea00';
$glyph-icon-arrows-up-down: '\ea01'; $glyph-icon-arrows-up-down: '\ea01';
$glyph-icon-bullet: '\ea02'; $glyph-icon-bullet: '\ea02';

View File

@ -54,7 +54,6 @@ button {
background: $splitterBtnColorBg; background: $splitterBtnColorBg;
color: $splitterBtnColorFg; color: $splitterBtnColorFg;
border-radius: $smallCr; border-radius: $smallCr;
font-size: 6px;
line-height: 90%; line-height: 90%;
padding: 3px 10px; padding: 3px 10px;
@ -63,6 +62,10 @@ button {
color: $colorBtnFgHov; color: $colorBtnFgHov;
} }
@include desktop() {
font-size: 6px;
}
&:before { &:before {
content: $glyph-icon-arrow-down; content: $glyph-icon-arrow-down;
font-size: 1.1em; font-size: 1.1em;
@ -158,6 +161,26 @@ button {
} }
} }
.c-list-button {
@include cControl();
color: $colorBodyFg;
cursor: pointer;
justify-content: start;
padding: $interiorMargin;
> * + * {
margin-left: $interiorMargin;
}
@include hover() {
background: $colorItemTreeHoverBg;
}
.c-button {
flex: 0 0 auto;
}
}
/******************************************************** DISCLOSURE CONTROLS */ /******************************************************** DISCLOSURE CONTROLS */
/********* Disclosure Button */ /********* Disclosure Button */
// Provides a downward arrow icon that when clicked displays additional options and/or info. // Provides a downward arrow icon that when clicked displays additional options and/or info.
@ -232,13 +255,13 @@ section {
} }
&.is-expanded { &.is-expanded {
flex: 1 1 100%; flex: 0 1 auto;
max-height: max-content;
} }
.c-section__header { .c-section__header {
@include propertiesHeader(); @include propertiesHeader();
display: flex; display: flex;
flex: 0 0 auto;
align-items: center; align-items: center;
margin-bottom: $interiorMargin; margin-bottom: $interiorMargin;
@ -709,7 +732,7 @@ select {
$pTB: 2px; $pTB: 2px;
padding: $pTB $pLR; padding: $pTB $pLR;
&:hover { @include hover() {
background: $editUIBaseColorHov !important; background: $editUIBaseColorHov !important;
color: $editUIBaseColorFg !important; color: $editUIBaseColorFg !important;
} }
@ -724,7 +747,7 @@ select {
color: $colorBtnCautionBg; color: $colorBtnCautionBg;
} }
&:hover { @include hover() {
background: rgba($colorBtnCautionBgHov, 0.2); background: rgba($colorBtnCautionBgHov, 0.2);
:before { :before {
color: $colorBtnCautionBgHov; color: $colorBtnCautionBgHov;

View File

@ -83,6 +83,7 @@
.icon-cursor-lock { @include glyphBefore($glyph-icon-cursor-lock); } .icon-cursor-lock { @include glyphBefore($glyph-icon-cursor-lock); }
.icon-flag { @include glyphBefore($glyph-icon-flag); } .icon-flag { @include glyphBefore($glyph-icon-flag); }
.icon-eye-disabled { @include glyphBefore($glyph-icon-eye-disabled); } .icon-eye-disabled { @include glyphBefore($glyph-icon-eye-disabled); }
.icon-notebook-page { @include glyphBefore($glyph-icon-notebook-page); }
.icon-arrows-right-left { @include glyphBefore($glyph-icon-arrows-right-left); } .icon-arrows-right-left { @include glyphBefore($glyph-icon-arrows-right-left); }
.icon-arrows-up-down { @include glyphBefore($glyph-icon-arrows-up-down); } .icon-arrows-up-down { @include glyphBefore($glyph-icon-arrows-up-down); }
.icon-bullet { @include glyphBefore($glyph-icon-bullet); } .icon-bullet { @include glyphBefore($glyph-icon-bullet); }

View File

@ -80,4 +80,8 @@
&__object-name { &__object-name {
flex: 0 1 auto; flex: 0 1 auto;
} }
&__object-details {
opacity: 0.5;
}
} }

View File

@ -51,7 +51,7 @@
/************************** EFFECTS */ /************************** EFFECTS */
@mixin pulse($animName: pulse, $dur: 500ms, $iteration: infinite, $opacity0: 0.5, $opacity100: 1) { @mixin pulse($animName: pulse, $dur: 500ms, $iteration: infinite, $opacity0: 0.5, $opacity100: 1) {
@keyframes pulse { @keyframes #{$animName} {
0% { opacity: $opacity0; } 0% { opacity: $opacity0; }
100% { opacity: $opacity100; } 100% { opacity: $opacity100; }
} }
@ -62,6 +62,18 @@
animation-timing-function: ease-in-out; animation-timing-function: ease-in-out;
} }
@mixin pulseProp($animName: pulseProp, $dur: 500ms, $iter: 5, $prop: opacity, $valStart: 0, $valEnd: 1) {
@keyframes #{$animName} {
0% { #{$prop}: $valStart; }
100% { #{$prop}: $valEnd; }
}
animation-name: $animName;
animation-duration: $dur;
animation-direction: alternate;
animation-iteration-count: $iter;
animation-timing-function: ease-in-out;
}
/************************** VISUALS */ /************************** VISUALS */
@mixin ancillaryIcon($d, $c) { @mixin ancillaryIcon($d, $c) {
// Used for small icons used in combination with larger icons, // Used for small icons used in combination with larger icons,
@ -426,10 +438,15 @@
color: $colorBodyFg; color: $colorBodyFg;
cursor: pointer; cursor: pointer;
padding: 4px; // Bigger hit area padding: 4px; // Bigger hit area
opacity: 0.6; opacity: 0.7;
transition: $transOut; transition: $transOut;
transform-origin: center; transform-origin: center;
&[class*="--major"] {
color: $colorBtnMajorBg !important;
opacity: 0.8;
}
@include hover() { @include hover() {
transform: scale(1.1); transform: scale(1.1);
transition: $transIn; transition: $transIn;

View File

@ -22,34 +22,61 @@
/*********************************************** NOTEBOOK */ /*********************************************** NOTEBOOK */
.c-notebook { .c-notebook {
$headerFontSize: 1.3em;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
position: absolute; height: 100%;
top: 0px;
right: 0px;
bottom: 0px;
left: 0px;
&-snapshot { /****************************** CONTENT */
flex: 1 1 auto; &__body {
// Holds __nav and __page-view
display: flex; display: flex;
flex-direction: column; flex: 1 1 auto;
overflow: hidden;
}
> * + * { &__nav {
flex: 0 0 auto;
* {
overflow: hidden;
}
}
.c-sidebar {
background: $sideBarBg;
.c-sidebar__pane {
flex-basis: 50%;
}
}
body.mobile & {
.c-list-button,
&-snapshot-menubutton {
display: none;
}
}
/****************************** CONTENT */
&__contents {
width: 70%;
}
&__page-view {
// Holds __header, __drag-area and __entries
display: flex;
flex: 1 1 auto;
flex-direction: column;
width: 100%;
> * {
flex: 0 0 auto;
+ * {
margin-top: $interiorMargin; margin-top: $interiorMargin;
} }
&__header {
flex: 0 0 auto;
}
&__image {
flex: 1 1 auto;
} }
} }
> [class*="__"] + [class*="__"] { > * + * {
margin-top: $interiorMargin; margin-top: $interiorMargin;
} }
@ -111,17 +138,93 @@
} }
} }
/***** PAGE VIEW */
&__page-view {
&__header {
display: flex;
flex-wrap: wrap; // Allows wrapping in mobile portrait and narrow placements
line-height: 220%;
> * {
flex: 0 0 auto;
}
}
&__path {
flex: 1 1 auto;
margin: 0 $interiorMargin;
overflow: hidden;
white-space: nowrap;
font-size: $headerFontSize;
> * {
// Section
flex: 0 0 auto;
+ * {
// Page
display: inline;
flex: 1 1 auto;
@include ellipsize();
}
}
}
}
&__entries { &__entries {
flex-direction: column; flex-direction: column;
flex: 1 1 auto; flex: 1 1 auto;
padding-right: $interiorMarginSm;
overflow-x: hidden; overflow-x: hidden;
overflow-y: scroll; overflow-y: scroll;
@include desktop() {
padding-right: $interiorMarginSm; // Scrollbar kickoff
}
[class*="__entry"] + [class*="__entry"] { [class*="__entry"] + [class*="__entry"] {
margin-top: $interiorMarginSm; margin-top: $interiorMarginSm;
} }
}
/***** SEARCH RESULTS */
&__search-results {
display: flex;
flex: 1 1 auto;
flex-direction: column;
> * + * {
margin-top: 5px;
}
&__header {
font-size: $headerFontSize;
flex: 0 0 auto;
}
.c-notebook__entries {
flex: 1 1 auto;
}
.c-ne {
flex-direction: column;
> * + * {
margin-top: $interiorMargin;
}
}
}
}
.is-notebook-default {
&:after {
color: $colorFilter;
content: $glyph-icon-notebook-page;
display: block;
font-family: symbolsfont;
font-size: 0.9em;
margin-left: $interiorMargin;
}
&.c-list__item:after {
flex: 1 0 auto;
text-align: right;
} }
} }
@ -183,10 +286,29 @@
} }
&__embeds { &__embeds {
flex-wrap: wrap; //flex-wrap: wrap;
> [class*="__embed"] { > [class*="__embed"] {
margin: 0 $interiorMarginSm $interiorMarginSm 0; //margin: 0 $interiorMarginSm $interiorMarginSm 0;
}
}
&__section-and-page {
// Shown when c-ne within search results
background: rgba($colorBodyFg, 0.1); //$colorInteriorBorder;
border-radius: $controlCr;
display: inline-flex;
align-items: center;
align-self: flex-start;
padding: $interiorMargin;
> * + * {
margin-left: $interiorMargin;
}
[class*='icon'] {
font-size: 0.8em;
opacity: 0.7;
} }
} }
} }
@ -194,7 +316,7 @@
/****************************** EMBEDS */ /****************************** EMBEDS */
@mixin snapThumb() { @mixin snapThumb() {
// LEGACY: TODO: refactor when .snap-thumb in New Entry dialog is refactored // LEGACY: TODO: refactor when .snap-thumb in New Entry dialog is refactored
$d: 50px; $d: 30px;
border: 1px solid $colorInteriorBorder; border: 1px solid $colorInteriorBorder;
cursor: pointer; cursor: pointer;
width: $d; width: $d;
@ -269,6 +391,64 @@
.l-sticky-headers .l-tabular-body { overflow: auto; } .l-sticky-headers .l-tabular-body { overflow: auto; }
} }
.c-notebook-snapshot {
flex: 1 1 auto;
display: flex;
flex-direction: column;
> * + * {
margin-top: $interiorMargin;
}
&__header {
flex: 0 0 auto;
}
&__image {
background-size: contain;
background-repeat: no-repeat;
background-position: center center;
flex: 1 1 auto;
}
}
/****************************** SNAPSHOT CONTAINER */
.c-snapshots-h {
// Is hidden when the parent div l-shell__drawer is collapsed, so no worries about padding, etc.
display: flex;
flex-direction: column;
overflow: hidden;
padding: $interiorMarginLg;
> * {
flex: 1 1 auto;
&:first-child {
flex: 0 0 auto;
}
}
> * + * {
margin-top: $interiorMargin;
}
}
.c-snapshots {
flex-wrap: wrap;
overflow: auto;
.c-snapshot {
margin: 0 $interiorMarginSm $interiorMarginSm 0;
}
.hint {
font-size: 1.25em;
font-style: italic;
opacity: 0.7;
padding: $interiorMarginLg;
text-align: center;
}
}
/****************************** PAINTERRO OVERRIDES */ /****************************** PAINTERRO OVERRIDES */
.annotation-dialog .abs.editor { .annotation-dialog .abs.editor {
border-radius: 0; border-radius: 0;
@ -401,7 +581,8 @@ body.mobile {
&.phone.portrait { &.phone.portrait {
.c-notebook__head, .c-notebook__head,
.c-notebook__entry { .c-notebook__entry,
.c-ne__time-and-content {
flex-direction: column; flex-direction: column;
> [class*="__"] + [class*="__"] { > [class*="__"] + [class*="__"] {
@ -413,9 +594,14 @@ body.mobile {
.c-notebook__entry { .c-notebook__entry {
[class*="text"] { [class*="text"] {
min-height: 0; min-height: 0;
padding: 0;
pointer-events: none; pointer-events: none;
} }
} }
} }
} }
/****************************** INDICATOR */
.c-indicator.has-new-snapshot {
$c: $colorOk;
@include pulseProp($animName: flashSnapshot, $dur: 500ms, $iter: infinite, $prop: background, $valStart: rgba($c, 0.4), $valEnd: rgba($c, 0));
}

View File

@ -47,3 +47,4 @@
@import "../ui/preview/preview.scss"; @import "../ui/preview/preview.scss";
@import "../ui/toolbar/components/toolbar-checkbox.scss"; @import "../ui/toolbar/components/toolbar-checkbox.scss";
@import "./notebook.scss"; @import "./notebook.scss";
@import "../plugins/notebook/components/sidebar.scss";

View File

@ -29,7 +29,7 @@
> >
<div class="c-so-view__header"> <div class="c-so-view__header">
<div class="c-object-label" <div class="c-object-label"
:class="cssClass" :class="[cssClass, classList]"
> >
<div class="c-object-label__name"> <div class="c-object-label__name">
{{ domainObject && domainObject.name }} {{ domainObject && domainObject.name }}
@ -99,6 +99,16 @@ export default {
complexContent complexContent
} }
}, },
computed: {
classList() {
const classList = this.domainObject.classList;
if (!classList || !classList.length) {
return '';
}
return classList.join(' ');
}
},
methods: { methods: {
expand() { expand() {
let objectView = this.$refs.objectView, let objectView = this.$refs.objectView,

View File

@ -1,6 +1,7 @@
<template> <template>
<a <a
class="c-tree__item__label c-object-label" class="c-tree__item__label c-object-label"
:class="classList"
draggable="true" draggable="true"
:href="objectLink" :href="objectLink"
@dragstart="dragStart" @dragstart="dragStart"
@ -43,6 +44,14 @@ export default {
}; };
}, },
computed: { computed: {
classList() {
const classList = this.observedObject.classList;
if (!classList || !classList.length) {
return '';
}
return classList.join(' ');
},
typeClass() { typeClass() {
let type = this.openmct.types.get(this.observedObject.type); let type = this.openmct.types.get(this.observedObject.type);
if (!type) { if (!type) {

View File

@ -47,6 +47,13 @@ export default {
) )
} }
}, },
watch: {
value(inputValue) {
if (!inputValue.length) {
this.clearInput();
}
}
},
methods: { methods: {
clearInput() { clearInput() {
// Clear the user's input and set 'active' to false // Clear the user's input and set 'active' to false

View File

@ -1,3 +1,25 @@
/*****************************************************************************
* 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> <template>
<div class="u-contents"></div> <div class="u-contents"></div>
</template> </template>
@ -43,11 +65,13 @@ export default {
isChildItem = true; isChildItem = true;
//If there are more than 1 items in the selection[0] list, the first one could either be a sub domain object OR a layout drawing control. //If there are more than 1 items in the selection[0] list, the first one could either be a sub domain object OR a layout drawing control.
//The second item in the selection[0] list is the container object (usually a layout) //The second item in the selection[0] list is the container object (usually a layout)
if (!domainObject) {
styleProps = {}; styleProps = {};
layoutItem = selection[0][0].context.layoutItem; layoutItem = selection[0][0].context.layoutItem;
styleProps = this.getStyleProperties(layoutItem); styleProps = this.getStyleProperties(layoutItem);
domainObject = selection[0][1].context.item; domainObject = selection[0][1].context.item;
} }
}
if (this.component) { if (this.component) {
this.component.$destroy(); this.component.$destroy();

View File

@ -1,13 +1,13 @@
.c-path,
.c-location { .c-location {
// Path is two or more items, not clickable
// Location used in Inspector, is clickable
display: flex; display: flex;
flex-wrap: wrap;
&__item { &__item {
$m: $interiorMarginSm;
cursor: pointer;
display: flex; display: flex;
align-items: center; align-items: center;
margin: 0 $m $m 0; min-width: 0;
&:not(:last-child) { &:not(:last-child) {
&:after { &:after {
@ -15,10 +15,19 @@
content: $glyph-icon-arrow-right; content: $glyph-icon-arrow-right;
font-family: symbolsfont; font-family: symbolsfont;
font-size: 0.7em; font-size: 0.7em;
margin-left: $m; margin-left: $interiorMarginSm;
opacity: 0.8; opacity: 0.8;
} }
} }
}
}
.c-location {
flex-wrap: wrap;
&__item {
cursor: pointer;
margin: 0 $interiorMarginSm $interiorMarginSm 0;
.c-object-label { .c-object-label {
padding: 0; padding: 0;

View File

@ -8,7 +8,7 @@
></button> ></button>
<div <div
class="l-browse-bar__object-name--w c-object-label" class="l-browse-bar__object-name--w c-object-label"
:class="type.cssClass" :class="[ type.cssClass, classList ]"
> >
<span <span
class="l-browse-bar__object-name c-object-label__name c-input-inline" class="l-browse-bar__object-name c-object-label__name c-input-inline"
@ -33,13 +33,11 @@
@setView="setView" @setView="setView"
/> />
<!-- Action buttons --> <!-- Action buttons -->
<NotebookMenuSwitcher v-if="notebookEnabled"
:domain-object="domainObject"
class="c-notebook-snapshot-menubutton"
/>
<div class="l-browse-bar__actions"> <div class="l-browse-bar__actions">
<button
v-if="notebookEnabled"
class="l-browse-bar__actions__notebook-entry c-button icon-notebook"
title="New Notebook entry"
@click="snapshot()"
></button>
<button <button
v-if="isViewEditable & !isEditing" v-if="isViewEditable & !isEditing"
class="l-browse-bar__actions__edit c-button c-button--major icon-pencil" class="l-browse-bar__actions__edit c-button c-button--major icon-pencil"
@ -91,26 +89,37 @@
</template> </template>
<script> <script>
import NotebookSnapshot from '../utils/notebook-snapshot';
import ViewSwitcher from './ViewSwitcher.vue'; import ViewSwitcher from './ViewSwitcher.vue';
import NotebookMenuSwitcher from '@/plugins/notebook/components/notebook-menu-switcher.vue';
const PLACEHOLDER_OBJECT = {}; const PLACEHOLDER_OBJECT = {};
export default { export default {
inject: ['openmct'], inject: ['openmct'],
components: { components: {
NotebookMenuSwitcher,
ViewSwitcher ViewSwitcher
}, },
data: function () { data: function () {
return { return {
notebookTypes: [],
showViewMenu: false, showViewMenu: false,
showSaveMenu: false, showSaveMenu: false,
domainObject: PLACEHOLDER_OBJECT, domainObject: PLACEHOLDER_OBJECT,
viewKey: undefined, viewKey: undefined,
isEditing: this.openmct.editor.isEditing(), isEditing: this.openmct.editor.isEditing(),
notebookEnabled: false notebookEnabled: this.openmct.types.get('notebook')
} }
}, },
computed: { computed: {
classList() {
const classList = this.domainObject.classList;
if (!classList || !classList.length) {
return '';
}
return classList.join(' ');
},
currentView() { currentView() {
return this.views.filter(v => v.key === this.viewKey)[0] || {}; return this.views.filter(v => v.key === this.viewKey)[0] || {};
}, },
@ -163,12 +172,6 @@ export default {
} }
}, },
mounted: function () { mounted: function () {
if (this.openmct.types.get('notebook')) {
this.notebookSnapshot = new NotebookSnapshot(this.openmct);
this.notebookEnabled = true;
}
document.addEventListener('click', this.closeViewAndSaveMenu); document.addEventListener('click', this.closeViewAndSaveMenu);
window.addEventListener('beforeunload', this.promptUserbeforeNavigatingAway); window.addEventListener('beforeunload', this.promptUserbeforeNavigatingAway);
@ -266,10 +269,6 @@ export default {
showContextMenu(event) { showContextMenu(event) {
this.openmct.contextMenu._showContextMenuForObjectPath(this.openmct.router.path, event.clientX, event.clientY); this.openmct.contextMenu._showContextMenuForObjectPath(this.openmct.router.path, event.clientX, event.clientY);
}, },
snapshot() {
let element = document.getElementsByClassName("l-shell__main-container")[0];
this.notebookSnapshot.capture(this.domainObject, element);
},
goToParent() { goToParent() {
window.location.hash = this.parentUrl; window.location.hash = this.parentUrl;
} }

View File

@ -34,6 +34,9 @@
</div> </div>
<app-logo /> <app-logo />
</div> </div>
<div class="l-shell__drawer c-drawer c-drawer--push c-drawer--align-top"></div>
<multipane <multipane
class="l-shell__main" class="l-shell__main"
type="horizontal" type="horizontal"

View File

@ -6,6 +6,21 @@
flex-flow: column nowrap; flex-flow: column nowrap;
overflow: hidden; overflow: hidden;
&__drawer {
background: $drawerBg;
display: flex;
flex-direction: column;
height: 0;
min-height: 0;
max-height: 15%;
overflow: hidden;
transition: $transIn;
&.is-expanded {
height: max-content;
}
}
&__pane-tree { &__pane-tree {
width: 40%; width: 40%;
@ -15,7 +30,7 @@
@include cClickIconButton(); @include cClickIconButton();
color: $colorKey !important; color: $colorKey !important;
position: absolute; position: absolute;
right: -2 * nth($shellPanePad, 2); // Needs to be -1 * when pane is collapsed right: -18px;
top: 0; top: 0;
transform: translateX(100%); transform: translateX(100%);
width: $mobileMenuIconD; width: $mobileMenuIconD;
@ -51,13 +66,22 @@
} }
} }
&__pane-main {
.l-pane__header { display: none; }
}
body.mobile & { body.mobile & {
&__pane-main,
&__pane-tree {
padding: $interiorMarginLg;
}
&__pane-tree { &__pane-tree {
background: linear-gradient(90deg, transparent 70%, rgba(black, 0.2) 99%, rgba(black, 0.3)); background: linear-gradient(90deg, transparent 70%, rgba(black, 0.2) 99%, rgba(black, 0.3));
&[class*="--collapsed"] { &[class*="--collapsed"] {
[class*="collapse-button"] { [class*="collapse-button"] {
right: -1 * nth($shellPanePad, 2); right: -8px;
} }
} }
} }
@ -190,14 +214,14 @@
&__main { &__main {
> .l-pane { > .l-pane {
padding: nth($shellPanePad, 1) nth($shellPanePad, 2); padding: nth($shellPanePad, 1) 0;
} }
} }
body.desktop & { body.desktop & {
&__main { &__main {
// Top and bottom padding in container that holds tree, __pane-main and Inspector // Top and bottom padding in container that holds tree, __pane-main and Inspector
padding: $shellMainPad; padding: nth($shellPanePad, 1) 0;
min-height: 0; min-height: 0;
> .l-pane { > .l-pane {
@ -208,15 +232,17 @@
&__pane-tree, &__pane-tree,
&__pane-inspector { &__pane-inspector {
max-width: 30%; max-width: 70%;
} }
&__pane-tree { &__pane-tree {
width: 300px; width: 300px;
padding-left: nth($shellPanePad, 2);
} }
&__pane-inspector { &__pane-inspector {
width: 200px; width: 200px;
padding-right: nth($shellPanePad, 2);
} }
} }
@ -242,3 +268,68 @@
} }
} }
} }
.c-drawer {
/* New sliding overlay or push element to contain things
* Designed for mobile and compact desktop scenarios
* Variations:
* --overlays: position absolute, overlays neighboring elements
* --push: position relative, pushs/collapses neighboring elements
* --align-left, align-top: opens from left or top respectively
* &.is-expanded: applied when expanded.
*/
transition: $transOut;
min-height: 0;
min-width: 0;
overflow: hidden;
&:not(.is-expanded) {
// When collapsed, hide internal elements
> * {
display: none;
}
}
&.c-drawer--align-left {
height: 100%;
}
&.c-drawer--align-top {
// Need anything here?
}
&.c-drawer--overlays {
position: absolute;
z-index: 3;
&.is-expanded {
// Height and width must be set per usage
&.c-drawer--align-left {
box-shadow: rgba(black, 0.7) 3px 0 20px;
}
&.c-drawer--align-top {
box-shadow: rgba(black, 0.7) 0 3px 20px;
}
}
}
&.c-drawer--push {
position: relative;
&.is-expanded {
// Height and width must be set per usage
&.c-drawer--align-left {
box-shadow: rgba(black, 0.2) 3px 0 20px;
margin-right: $interiorMarginLg;
}
&.c-drawer--align-top {
box-shadow: rgba(black, 0.2) 0 3px 20px;
margin-bottom: $interiorMarginLg; // Not sure this is desired here
}
}
}
}

View File

@ -2,7 +2,8 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1 1 auto; flex: 1 1 auto;
padding-right: $interiorMarginSm; //TODO: Do we need this???
//padding-right: $interiorMarginSm;
overflow: auto; overflow: auto;
> * + * { margin-top: $interiorMargin; } > * + * { margin-top: $interiorMargin; }
@ -23,22 +24,22 @@
&__tree { &__tree {
flex: 1 1 auto; flex: 1 1 auto;
height: 0; // Chrome 73 overflow bug fix height: 0; // Chrome 73 overflow bug fix
padding-right: $interiorMarginSm;
} }
} }
.c-tree { .c-tree,
.c-list {
@include userSelectNone(); @include userSelectNone();
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
padding-right: $interiorMargin;
li { li {
position: relative; position: relative;
&.c-tree__item-h { display: block; } &[class*="__item-h"] { display: block; }
+ li {
margin-top: 1px;
} }
.c-tree {
margin-left: 15px;
} }
&__item { &__item {
@ -48,20 +49,13 @@
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
line-height: 110%; line-height: 110%;
padding: $interiorMargin - $aPad; padding: $interiorMarginSm $interiorMargin;
transition: background 150ms ease; transition: background 150ms ease;
> * + * {
margin-left: $interiorMarginSm;
}
&:hover { &:hover {
background: $colorItemTreeHoverBg; background: $colorItemTreeHoverBg;
.c-tree__item__type-icon:before {
color: $colorItemTreeIconHover;
}
.c-tree__item__name { [class*="__name"] {
color: $colorItemTreeHoverFg; color: $colorItemTreeHoverFg;
} }
} }
@ -69,12 +63,34 @@
&.is-navigated-object, &.is-navigated-object,
&.is-selected { &.is-selected {
background: $colorItemTreeSelectedBg; background: $colorItemTreeSelectedBg;
[class*="__name"] {
color: $colorItemTreeSelectedFg;
}
}
}
}
.c-tree {
.c-tree {
margin-left: 15px;
}
&__item {
> * + * {
margin-left: $interiorMarginSm;
}
&:hover {
.c-tree__item__type-icon:before { .c-tree__item__type-icon:before {
color: $colorItemTreeIconHover; color: $colorItemTreeIconHover;
} }
}
.c-tree__item__name { &.is-navigated-object,
color: $colorItemTreeSelectedFg; &.is-selected {
.c-tree__item__type-icon:before {
color: $colorItemTreeIconHover;
} }
} }
@ -121,6 +137,19 @@
} }
} }
.c-list {
padding-right: $interiorMargin;
&__item {
&__name {
$p: $interiorMarginSm;
@include ellipsize();
padding-bottom: $p;
padding-top: $p;
}
}
}
.c-selector { .c-selector {
.c-tree-and-search__tree.c-tree { .c-tree-and-search__tree.c-tree {
border: 1px solid $colorInteriorBorder; border: 1px solid $colorInteriorBorder;

View File

@ -2,6 +2,7 @@
.l-multipane { .l-multipane {
display: flex; display: flex;
flex: 1 1 auto; flex: 1 1 auto;
overflow: hidden;
&--horizontal, &--horizontal,
> .l-pane { > .l-pane {
@ -38,7 +39,7 @@
&__header { &__header {
display: flex; display: flex;
align-items: center; align-items: center;
margin-bottom: $interiorMargin; @include desktop() { margin-bottom: $interiorMargin; }
} }
&--reacts { &--reacts {
@ -232,8 +233,11 @@
} }
/************************** Horizontal Splitter Before */ /************************** Horizontal Splitter Before */
// Inspector pane // Example: Inspector pane
&[class*="-before"] { &[class*="-before"] {
margin-left: nth($shellPanePad, 2);
padding-left: nth($shellPanePad, 2);
> .l-pane__handle { > .l-pane__handle {
left: 0; left: 0;
transform: translateX(floor($splitterHandleD / -2)); // Center over the pane edge transform: translateX(floor($splitterHandleD / -2)); // Center over the pane edge
@ -247,8 +251,11 @@
} }
/************************** Horizontal Splitter After */ /************************** Horizontal Splitter After */
// Tree pane // Example: Tree pane
&[class*="-after"] { &[class*="-after"] {
margin-right: nth($shellPanePad, 2);
padding-right: nth($shellPanePad, 2);
> .l-pane__handle { > .l-pane__handle {
right: 0; right: 0;
transform: translateX(floor($splitterHandleD / 2)); transform: translateX(floor($splitterHandleD / 2));

View File

@ -16,11 +16,10 @@
class="l-pane__handle" class="l-pane__handle"
@mousedown="start" @mousedown="start"
></div> ></div>
<div <div class="l-pane__header">
v-if="label" <span v-if="label"
class="l-pane__header" class="l-pane__label"
> >{{ label }}</span>
<span class="l-pane__label">{{ label }}</span>
<button <button
v-if="collapsable" v-if="collapsable"
class="l-pane__collapse-button c-button" class="l-pane__collapse-button c-button"

View File

@ -1,14 +1,12 @@
.c-indicator { .c-indicator {
@include cControl(); @include cControl();
@include cClickIconButtonLayout(); @include cClickIconButtonLayout();
button { text-transform: uppercase; }
background: none !important;
border-radius: $controlCr; border-radius: $controlCr;
overflow: visible; overflow: visible;
position: relative; position: relative;
text-transform: uppercase; text-transform: uppercase;
button { text-transform: uppercase; }
&.no-minify { &.no-minify {
// For items that cannot be minified // For items that cannot be minified
@ -42,7 +40,7 @@
a, a,
button, button,
s-button, .s-button,
.c-button { .c-button {
// Make <a> in label look like buttons // Make <a> in label look like buttons
transition: $transIn; transition: $transIn;

View File

@ -40,12 +40,6 @@
:current-view="currentView" :current-view="currentView"
@setView="setView" @setView="setView"
/> />
<button
v-if="notebookEnabled"
class="l-browse-bar__actions__edit c-button icon-notebook"
title="New Notebook entry"
@click="snapshot"
></button>
</div> </div>
</div> </div>
</div> </div>
@ -58,7 +52,6 @@
<script> <script>
import ContextMenuDropDown from '../../ui/components/contextMenuDropDown.vue'; import ContextMenuDropDown from '../../ui/components/contextMenuDropDown.vue';
import ViewSwitcher from '../../ui/layout/ViewSwitcher.vue'; import ViewSwitcher from '../../ui/layout/ViewSwitcher.vue';
import NotebookSnapshot from '../utils/notebook-snapshot';
export default { export default {
components: { components: {
@ -94,20 +87,11 @@ export default {
mounted() { mounted() {
let view = this.openmct.objectViews.get(this.domainObject)[0]; let view = this.openmct.objectViews.get(this.domainObject)[0];
this.setView(view); this.setView(view);
if (this.openmct.types.get('notebook')) {
this.notebookSnapshot = new NotebookSnapshot(this.openmct);
this.notebookEnabled = true;
}
}, },
destroyed() { destroyed() {
this.view.destroy(); this.view.destroy();
}, },
methods: { methods: {
snapshot() {
let element = document.getElementsByClassName("l-preview-window__object-view")[0];
this.notebookSnapshot.capture(this.domainObject, element);
},
clear() { clear() {
if (this.view) { if (this.view) {
this.view.destroy(); this.view.destroy();

View File

@ -9,22 +9,31 @@ define([
let browseObject; let browseObject;
let unobserve = undefined; let unobserve = undefined;
let currentObjectPath; let currentObjectPath;
let isRoutingInProgress = false;
openmct.router.route(/^\/browse\/?$/, navigateToFirstChildOfRoot); openmct.router.route(/^\/browse\/?$/, navigateToFirstChildOfRoot);
openmct.router.route(/^\/browse\/(.*)$/, (path, results, params) => { openmct.router.route(/^\/browse\/(.*)$/, (path, results, params) => {
isRoutingInProgress = true;
let navigatePath = results[1]; let navigatePath = results[1];
navigateToPath(navigatePath, params.view); navigateToPath(navigatePath, params.view);
onParamsChanged(null, null, params);
}); });
openmct.router.on('change:params', function (newParams, oldParams, changed) { openmct.router.on('change:params', onParamsChanged);
function onParamsChanged(newParams, oldParams, changed) {
if (isRoutingInProgress) {
return;
}
if (changed.view && browseObject) { if (changed.view && browseObject) {
let provider = openmct let provider = openmct
.objectViews .objectViews
.getByProviderKey(changed.view); .getByProviderKey(changed.view);
viewObject(browseObject, provider); viewObject(browseObject, provider);
} }
}); }
function viewObject(object, viewProvider) { function viewObject(object, viewProvider) {
currentObjectPath = openmct.router.path; currentObjectPath = openmct.router.path;
@ -49,6 +58,8 @@ define([
} }
return pathToObjects(path).then((objects)=>{ return pathToObjects(path).then((objects)=>{
isRoutingInProgress = false;
if (currentNavigation !== navigateCall) { if (currentNavigation !== navigateCall) {
return; // Prevent race. return; // Prevent race.
} }
@ -63,6 +74,7 @@ define([
unobserve = this.openmct.objects.observe(openmct.router.path[0], '*', (newObject) => { unobserve = this.openmct.objects.observe(openmct.router.path[0], '*', (newObject) => {
openmct.router.path[0] = newObject; openmct.router.path[0] = newObject;
browseObject = newObject;
}); });
openmct.layout.$refs.browseBar.domainObject = navigatedObject; openmct.layout.$refs.browseBar.domainObject = navigatedObject;

View File

@ -1,135 +0,0 @@
/*****************************************************************************
* 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.
*****************************************************************************/
class NotebookSnapshot {
constructor(openmct) {
this.openmct = openmct;
this.exportImageService = openmct.$injector.get('exportImageService');
this.dialogService = openmct.$injector.get('dialogService');
this.capture = this.capture.bind(this);
this._saveSnapShot = this._saveSnapShot.bind(this);
}
capture(domainObject, domElement) {
let type = this.openmct.types.get(domainObject.type),
embedObject = {
id: domainObject.identifier.key,
cssClass: type.cssClass,
name: domainObject.name
};
this.exportImageService.exportPNGtoSRC(domElement, 's-status-taking-snapshot').then(function (blob) {
var reader = new window.FileReader();
reader.readAsDataURL(blob);
reader.onloadend = function () {
this._saveSnapShot(reader.result, embedObject);
}.bind(this);
}.bind(this));
}
/**
* @private
*/
_generateTaskForm(url) {
var taskForm = {
name: "Create a Notebook Entry",
hint: "Please select a Notebook",
sections: [{
rows: [
{
name: 'Entry',
key: 'entry',
control: 'textarea',
required: false,
cssClass: "l-textarea-sm"
},
{
name: 'Snap Preview',
key:"snapPreview",
control: "snap-view",
cssClass: "l-textarea-sm",
src: url
},
{
name: 'Save in Notebook',
key: 'notebook',
control: 'locator',
validate: validateLocation
}
]
}]
};
var overlayModel = {
title: taskForm.name,
message: 'Notebook Snapshot',
structure: taskForm,
value: {'entry': ""}
};
function validateLocation(newParentObj) {
return newParentObj.model.type === 'notebook';
}
return overlayModel;
}
/**
* @private
*/
_saveSnapShot(imageUrl, embedObject) {
let taskForm = this._generateTaskForm(imageUrl);
this.dialogService.getDialogResponse(
'overlay-dialog',
taskForm,
() => taskForm.value
).then(options => {
let snapshotObject = {
src: options.snapPreview || imageUrl
};
options.notebook.useCapability('mutation', function (model) {
var date = Date.now();
model.entries.push({
id: 'entry-' + date,
createdOn: date,
text: options.entry,
embeds: [{
name: embedObject.name,
cssClass: embedObject.cssClass,
type: embedObject.id,
id: 'embed-' + date,
createdOn: date,
snapshot: snapshotObject
}]
});
});
});
}
}
export default NotebookSnapshot;