Compare commits

...

13 Commits

Author SHA1 Message Date
0a820aa869 merge latest tcr 2019-08-12 12:41:33 -07:00
7c92b6d206 Continued work on Notebook svc 2019-08-12 12:40:07 -07:00
aafe524454 bumped version number 2019-08-06 10:36:51 -07:00
e84ade1752 Removed unnecessary extra formatting (#2438) 2019-07-31 15:18:46 -07:00
3b094e43e3 Plot y axis label fix (#2437)
* set yAxis label if none is set yet

* remove empty listener
2019-07-31 10:24:27 -07:00
e6a7b4ed6c Move table cell selection to table cell component (#2436) 2019-07-31 10:08:27 -07:00
97230bb21f Context-Menu for Tables (#2424)
* add context menu capability to table rows, add view switcher to preview

* add an option to limit context menu actions to the ones requested, and modify preview action to also include a view historical data action

* extend preview action into view historical data action

* add context menu to LAD Table

* add keys to context menu actions, allow tables to conditionally attach context menu handler

* working switch y axis label

* New vertical select element for Y axis configuration in plots

- CSS for vertically rotated selects for Y axis label selection;
- New theme constants;
- Removed themedSelect theme mixins;
- New SASS svgColorFromHex function;

* use keys in lad table context menu options

* show historical view context menu on alpha-numerics

* make reviewer requested changes

* pass contextual object path from object view down etc

* made reviewer requested changes: removed options object, pass in object path instead

* remove redundant function from LADRow.vue
2019-07-26 16:09:59 -07:00
768d99d928 Select, Mark and export selected table rows (#2420)
* first pass

* add a unmark all rows button

* enable shift click to select multiple rows

* support row selection backwards

* Styling for marked table rows

- CSS class applied;
- Export button label modified;

* working pause

* working multi select
tables are paused when user selects a row

* Layout improvements for table and control bar elements

- Table markup re-org'd;
- New .c-separator css class;
- Renamed .c-table__control-bar to .c-table-control-bar;
- Added label to Pause button;
- TODO: refine styling for table within frame in Layouts;

* Refined styling for c-button in an object frame

- More compact, better alignment, font sizing and padding;

* change logic to marking/selecting

* use command key to mark multiple

* Fixed regression errors in markup

* add isSelectable functionality

* make reviewer requested changes

* remove key from v-for in table.vue
2019-07-25 13:47:40 -07:00
c760190a29 Time conductor Improvement 625 (#2432)
* when utc based time systems are switched, the bounds will stay the same

* inline check for utcBased time systems
2019-07-25 10:08:57 -07:00
395a1caeec improvement to new notebook entry 2019-04-02 10:55:53 -07:00
339b315642 working embeds 2019-04-01 14:52:00 -07:00
601d59d6bd merge with tcr 2019-04-01 13:34:52 -07:00
c6e2d5a363 working new entry and delete entry 2019-02-04 18:43:10 -08:00
53 changed files with 1196 additions and 236 deletions

View File

@ -78,7 +78,7 @@
] ]
})); }));
openmct.install(openmct.plugins.SummaryWidget()); openmct.install(openmct.plugins.SummaryWidget());
openmct.install(openmct.plugins.Notebook()); openmct.install(openmct.plugins.NotebookSVC());
openmct.install(openmct.plugins.Filters(['table', 'telemetry.plot.overlay'])); openmct.install(openmct.plugins.Filters(['table', 'telemetry.plot.overlay']));
openmct.install(openmct.plugins.ObjectMigration()); openmct.install(openmct.plugins.ObjectMigration());
openmct.install(openmct.plugins.ClearData(['table', 'telemetry.plot.overlay', 'telemetry.plot.stacked'])); openmct.install(openmct.plugins.ClearData(['table', 'telemetry.plot.overlay', 'telemetry.plot.stacked']));

View File

@ -1,6 +1,6 @@
{ {
"name": "openmct", "name": "openmct",
"version": "0.14.0-SNAPSHOT", "version": "1.0.0-beta",
"description": "The Open MCT core platform", "description": "The Open MCT core platform",
"dependencies": {}, "dependencies": {},
"devDependencies": { "devDependencies": {

View File

@ -26,6 +26,7 @@ const OUTSIDE_EDIT_PATH_BLACKLIST = ["copy", "follow", "properties", "move", "li
export default class LegacyContextMenuAction { export default class LegacyContextMenuAction {
constructor(openmct, LegacyAction) { constructor(openmct, LegacyAction) {
this.openmct = openmct; this.openmct = openmct;
this.key = LegacyAction.definition.key;
this.name = LegacyAction.definition.name; this.name = LegacyAction.definition.name;
this.description = LegacyAction.definition.description; this.description = LegacyAction.definition.description;
this.cssClass = LegacyAction.definition.cssClass; this.cssClass = LegacyAction.definition.cssClass;

View File

@ -49,6 +49,9 @@ class ContextMenuAPI {
* a single sentence or short paragraph) of this kind of view * a single sentence or short paragraph) of this kind of view
* @property {string} cssClass the CSS class to apply to labels for this * @property {string} cssClass the CSS class to apply to labels for this
* view (to add icons, for instance) * view (to add icons, for instance)
* @property {string} key unique key to identify the context menu action
* (used in custom context menu eg table rows, to identify which actions to include)
* @property {boolean} hideInDefaultMenu optional flag to hide action from showing in the default context menu (tree item)
*/ */
/** /**
* @method appliesTo * @method appliesTo
@ -72,12 +75,21 @@ class ContextMenuAPI {
/** /**
* @private * @private
*/ */
_showContextMenuForObjectPath(objectPath, x, y) { _showContextMenuForObjectPath(objectPath, x, y, actionsToBeIncluded) {
let applicableActions = this._allActions.filter((action) => { let applicableActions = this._allActions.filter((action) => {
if (action.appliesTo === undefined) {
return true; if (actionsToBeIncluded) {
if (action.appliesTo === undefined && actionsToBeIncluded.includes(action.key)) {
return true;
}
return action.appliesTo(objectPath, actionsToBeIncluded) && actionsToBeIncluded.includes(action.key);
} else {
if (action.appliesTo === undefined) {
return true;
}
return action.appliesTo(objectPath) && !action.hideInDefaultMenu;
} }
return action.appliesTo(objectPath);
}); });
if (this._activeContextMenu) { if (this._activeContextMenu) {

View File

@ -38,7 +38,7 @@ define([
canEdit: function (domainObject) { canEdit: function (domainObject) {
return domainObject.type === 'LadTableSet'; return domainObject.type === 'LadTableSet';
}, },
view: function (domainObject) { view: function (domainObject, isEditing, objectPath) {
let component; let component;
return { return {
@ -49,7 +49,8 @@ define([
}, },
provide: { provide: {
openmct, openmct,
domainObject domainObject,
objectPath
}, },
el: element, el: element,
template: '<lad-table-set></lad-table-set>' template: '<lad-table-set></lad-table-set>'

View File

@ -38,7 +38,7 @@ define([
canEdit: function (domainObject) { canEdit: function (domainObject) {
return domainObject.type === 'LadTable'; return domainObject.type === 'LadTable';
}, },
view: function (domainObject) { view: function (domainObject, isEditing, objectPath) {
let component; let component;
return { return {
@ -49,7 +49,8 @@ define([
}, },
provide: { provide: {
openmct, openmct,
domainObject domainObject,
objectPath
}, },
el: element, el: element,
template: '<lad-table-component></lad-table-component>' template: '<lad-table-component></lad-table-component>'

View File

@ -1,3 +1,4 @@
/***************************************************************************** /*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government * Open MCT, Copyright (c) 2014-2018, 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
@ -21,7 +22,7 @@
*****************************************************************************/ *****************************************************************************/
<template> <template>
<tr> <tr @contextmenu.prevent="showContextMenu">
<td>{{name}}</td> <td>{{name}}</td>
<td>{{timestamp}}</td> <td>{{timestamp}}</td>
<td :class="valueClass"> <td :class="valueClass">
@ -35,15 +36,25 @@
</style> </style>
<script> <script>
const CONTEXT_MENU_ACTIONS = [
'viewHistoricalData',
'remove'
];
export default { export default {
inject: ['openmct'], inject: ['openmct', 'objectPath'],
props: ['domainObject'], props: ['domainObject'],
data() { data() {
let currentObjectPath = this.objectPath.slice();
currentObjectPath.unshift(this.domainObject);
return { return {
name: this.domainObject.name, name: this.domainObject.name,
timestamp: '---', timestamp: '---',
value: '---', value: '---',
valueClass: '' valueClass: '',
currentObjectPath
} }
}, },
methods: { methods: {
@ -73,11 +84,15 @@ export default {
.request(this.domainObject, {strategy: 'latest'}) .request(this.domainObject, {strategy: 'latest'})
.then((array) => this.updateValues(array[array.length - 1])); .then((array) => this.updateValues(array[array.length - 1]));
},
showContextMenu(event) {
this.openmct.contextMenu._showContextMenuForObjectPath(this.currentObjectPath, event.x, event.y, CONTEXT_MENU_ACTIONS);
} }
}, },
mounted() { mounted() {
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject); this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
this.formats = this.openmct.telemetry.getFormatMap(this.metadata); this.formats = this.openmct.telemetry.getFormatMap(this.metadata);
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.limitEvaluator = openmct this.limitEvaluator = openmct
.telemetry .telemetry

View File

@ -44,7 +44,7 @@ import lodash from 'lodash';
import LadRow from './LADRow.vue'; import LadRow from './LADRow.vue';
export default { export default {
inject: ['openmct', 'domainObject'], inject: ['openmct', 'domainObject', 'objectPath'],
components: { components: {
LadRow LadRow
}, },

View File

@ -46,13 +46,14 @@ define([
return selection.every(isTelemetryObject); return selection.every(isTelemetryObject);
}, },
view: function (selection) { view: function (domainObject, isEditing, objectPath) {
let component; let component;
return { return {
show: function (element) { show: function (element) {
component = new Vue({ component = new Vue({
provide: { provide: {
openmct openmct,
objectPath
}, },
components: { components: {
AlphanumericFormatView: AlphanumericFormatView.default AlphanumericFormatView: AlphanumericFormatView.default

View File

@ -202,7 +202,7 @@
return selectionPath && selectionPath.length > 1 && !singleSelectedLine; return selectionPath && selectionPath.length > 1 && !singleSelectedLine;
} }
}, },
inject: ['openmct', 'options'], inject: ['openmct', 'options', 'objectPath'],
props: ['domainObject'], props: ['domainObject'],
components: components, components: components,
methods: { methods: {

View File

@ -27,7 +27,7 @@
@endMove="() => $emit('endMove')"> @endMove="() => $emit('endMove')">
<object-frame v-if="domainObject" <object-frame v-if="domainObject"
:domain-object="domainObject" :domain-object="domainObject"
:object-path="objectPath" :object-path="currentObjectPath"
:has-frame="item.hasFrame" :has-frame="item.hasFrame"
:show-edit-view="false" :show-edit-view="false"
ref="objectFrame"> ref="objectFrame">
@ -71,7 +71,7 @@
hasFrame: hasFrameByDefault(domainObject.type) hasFrame: hasFrameByDefault(domainObject.type)
}; };
}, },
inject: ['openmct'], inject: ['openmct', 'objectPath'],
props: { props: {
item: Object, item: Object,
gridSize: Array, gridSize: Array,
@ -81,7 +81,7 @@
data() { data() {
return { return {
domainObject: undefined, domainObject: undefined,
objectPath: [] currentObjectPath: []
} }
}, },
components: { components: {
@ -100,7 +100,7 @@
methods: { methods: {
setObject(domainObject) { setObject(domainObject) {
this.domainObject = domainObject; this.domainObject = domainObject;
this.objectPath = [this.domainObject].concat(this.openmct.router.path); this.currentObjectPath = [this.domainObject].concat(this.objectPath.slice());
this.$nextTick(function () { this.$nextTick(function () {
let childContext = this.$refs.objectFrame.getSelectionContext(); let childContext = this.$refs.objectFrame.getSelectionContext();
childContext.item = domainObject; childContext.item = domainObject;

View File

@ -27,7 +27,8 @@
@endMove="() => $emit('endMove')"> @endMove="() => $emit('endMove')">
<div class="c-telemetry-view" <div class="c-telemetry-view"
:style="styleObject" :style="styleObject"
v-if="domainObject"> v-if="domainObject"
@contextmenu.prevent="showContextMenu">
<div v-if="showLabel" <div v-if="showLabel"
class="c-telemetry-view__label"> class="c-telemetry-view__label">
<div class="c-telemetry-view__label-text">{{ domainObject.name }}</div> <div class="c-telemetry-view__label-text">{{ domainObject.name }}</div>
@ -82,7 +83,8 @@
import printj from 'printj' import printj from 'printj'
const DEFAULT_TELEMETRY_DIMENSIONS = [10, 5], const DEFAULT_TELEMETRY_DIMENSIONS = [10, 5],
DEFAULT_POSITION = [1, 1]; DEFAULT_POSITION = [1, 1],
CONTEXT_MENU_ACTIONS = ['viewHistoricalData'];
export default { export default {
makeDefinition(openmct, gridSize, domainObject, position) { makeDefinition(openmct, gridSize, domainObject, position) {
@ -103,7 +105,7 @@
size: "13px" size: "13px"
}; };
}, },
inject: ['openmct'], inject: ['openmct', 'objectPath'],
props: { props: {
item: Object, item: Object,
gridSize: Array, gridSize: Array,
@ -163,7 +165,8 @@
return { return {
datum: undefined, datum: undefined,
formats: undefined, formats: undefined,
domainObject: undefined domainObject: undefined,
currentObjectPath: undefined
} }
}, },
watch: { watch: {
@ -218,12 +221,16 @@
}, },
setObject(domainObject) { setObject(domainObject) {
this.domainObject = domainObject; this.domainObject = domainObject;
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);
this.formats = this.openmct.telemetry.getFormatMap(this.metadata); this.formats = this.openmct.telemetry.getFormatMap(this.metadata);
this.requestHistoricalData(); this.requestHistoricalData();
this.subscribeToObject(); this.subscribeToObject();
this.currentObjectPath = this.objectPath.slice();
this.currentObjectPath.unshift(this.domainObject);
this.context = { this.context = {
item: domainObject, item: domainObject,
layoutItem: this.item, layoutItem: this.item,
@ -235,6 +242,9 @@
}, },
updateTelemetryFormat(format) { updateTelemetryFormat(format) {
this.$emit('formatChanged', this.item, format); this.$emit('formatChanged', this.item, format);
},
showContextMenu(event) {
this.openmct.contextMenu._showContextMenuForObjectPath(this.currentObjectPath, event.x, event.y, CONTEXT_MENU_ACTIONS);
} }
}, },
mounted() { mounted() {

View File

@ -37,7 +37,7 @@ export default function DisplayLayoutPlugin(options) {
canEdit: function (domainObject) { canEdit: function (domainObject) {
return domainObject.type === 'layout'; return domainObject.type === 'layout';
}, },
view: function (domainObject) { view: function (domainObject, isEditing, objectPath) {
let component; let component;
return { return {
show(container) { show(container) {
@ -49,13 +49,14 @@ export default function DisplayLayoutPlugin(options) {
provide: { provide: {
openmct, openmct,
objectUtils, objectUtils,
options options,
objectPath
}, },
el: container, el: container,
data () { data () {
return { return {
domainObject: domainObject domainObject: domainObject
} };
} }
}); });
}, },

View File

@ -23,6 +23,7 @@
export default class GoToOriginalAction { export default class GoToOriginalAction {
constructor(openmct) { constructor(openmct) {
this.name = 'Go To Original'; this.name = 'Go To Original';
this.key = 'goToOriginal';
this.description = 'Go to the original unlinked instance of this object'; this.description = 'Go to the original unlinked instance of this object';
this._openmct = openmct; this._openmct = openmct;

View File

@ -0,0 +1,31 @@
<template>
<div class="c-ne__embed">
<div class="c-ne__embed__snap-thumb"
v-if="embed.snapshot">
<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"></a>
</div>
<div class="c-ne__embed__time" v-if="embed.snapshot">
{{formatTime(embed.createdOn, 'YYYY-MM-DD HH:mm:ss')}}
</div>
</div>
</div>
</template>
<script>
import objectLinkMixin from '../../../ui/mixins/object-link';
export default {
inject: ['openmct', 'formatTime'],
props: ['embed'],
mixins: [objectLinkMixin]
}
</script>

View File

@ -0,0 +1,63 @@
<template>
<li class="c-notebook__entry c-ne has-local-controls"
@dragover.prevent
@drop.prevent="onTreeItemDrop">
<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="updateEntry"
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">
</notebook-embed>
</div>
</div>
</div>
<div class="c-ne__local-controls--hidden">
<button class="c-click-icon c-click-icon--major icon-trash"
title="Delete this entry"
@click="deleteEntry">
</button>
</div>
</li>
</template>
<script>
import NotebookEmbed from './embed.vue';
export default {
inject: ['formatTime'],
props: ['entry'],
components: {
NotebookEmbed
},
methods: {
updateEntry(event) {
this.$emit('update-entry', this.entry.id, event.target.innerText);
},
deleteEntry() {
this.$emit('delete-entry', this.entry.id);
},
onTreeItemDrop(event) {
this.$emit('drop-embed', this.entry.id, event);
}
},
mounted() {
if (!this.entry.text && !this.entry.embeds.length) {
this.$refs.contenteditable.focus();
}
}
}
</script>

View File

@ -0,0 +1,198 @@
<template>
<div class="c-notebook"
@dragover.stop
@drop.stop>
<div class="c-notebook__head">
<search class="c-notebook__search"
:value="searchValue"
@input="searchEntries"
@clear="searchEntries">
</search>
<div class="c-notebook__controls ">
<select class="c-notebook__controls__time" v-model="timeFrame">
<option value="0" :selected="timeFrame==='0'">Show all</option>
<option value="1" :selected="timeFrame==='1'">Last hour</option>
<option value="8" :selected="timeFrame==='8'">Last 8 hours</option>
<option value="24" :selected="timeFrame==='24'">Last 24 hours</option>
</select>
<select class="c-notebook__controls__time" v-model="sortOrder">
<option value="newest" :selected="sortOrder === 'newest'">Newest first</option>
<option value="oldest" :selected="sortOrder === 'oldest'">Oldest first</option>
</select>
</div>
</div>
<div class="c-notebook__drag-area icon-plus"
@click="newEntry">
<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>
<entry
v-for="entry in entries"
:key="entry.id"
:entry="entry"
@update-entry="persistEntry"
@delete-entry="deleteEntry"
@drop-embed="addEmbed">
</entry>
</ul>
</div>
</div>
</template>
<style lang="scss">
</style>
<script>
import Search from '../../../ui/components/search.vue';
import Entry from './entry.vue';
import Moment from 'moment';
import searchVue from '../../../ui/components/search.vue';
function formatTime(unixTime, format) {
return Moment(unixTime).format(format)
}
export default {
inject: ['openmct', 'providedDomainObject'],
components: {
Search,
Entry
},
provide: {
formatTime
},
data() {
return {
domainObject: this.providedDomainObject,
timeFrame: '0',
searchValue: '',
sortOrder: this.providedDomainObject.configuration.sortOrder
}
},
methods: {
searchEntries(value) {
this.searchValue = value;
},
updateDomainObject(domainObject) {
this.domainObject = domainObject;
},
newEntry() {
this.searchValue = '';
let date = Date.now(),
entry = {'id': 'entry-' + date, 'createdOn': date, 'embeds':[]},
entries = this.domainObject.entries,
lastEntry = entries[entries.length - 1];
if (lastEntry && !lastEntry.text && !lastEntry.embeds.length) {
let lastEntryComponent = this.$children.find(entryComponent => {
if (entryComponent.entry) {
return entryComponent.entry.id === lastEntry.id;
}
});
lastEntryComponent.$refs.contenteditable.focus();
} else {
entries.push(entry);
this.openmct.objects.mutate(this.domainObject, 'entries', entries);
}
},
findEntry(entryId) {
return this.domainObject.entries.findIndex(entry => {
return entry.id === entryId;
})
},
persistEntry(entryId, text) {
let entryPos = this.findEntry(entryId);
this.openmct.objects.mutate(this.domainObject, `entries[${entryPos}].text`, text);
},
deleteEntry(entryId) {
let entryPos = this.findEntry(entryId),
entries = this.domainObject.entries;
if (entryPos !== -1) {
let 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: () => {
entries.splice(entryPos, 1);
this.openmct.objects.mutate(this.domainObject, 'entries', entries);
dialog.dismiss();
}
},
{
label: "Cancel",
callback: () => {
dialog.dismiss();
}
}
]
});
}
},
applySearch(entries) {
return entries.filter((entry) => {
if (entry.text.includes(this.searchValue)) {
return entry;
}
});
},
addEmbed(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.findEntry(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);
}
},
onDrop() {
console.log('droped');
}
},
computed: {
entries() {
let entries = [...this.domainObject.entries];
if (this.searchValue !== '') {
entries = this.applySearch(entries);
}
if (this.sortOrder === 'newest') {
return entries.reverse();
} else {
return entries;
}
}
},
mounted() {
this.openmct.objects.observe(this.domainObject, '*', this.updateDomainObject);
}
}
</script>

View File

@ -0,0 +1,67 @@
/*****************************************************************************
* 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',
'./components/notebook.vue'
], function (
Vue,
Notebook
) {
function NotebookViewProvider(openmct) {
return {
key: 'notebook',
name: 'Notebook',
cssClass: 'icon-notebook',
canView: function (domainObject) {
return domainObject.type === 'notebook';
},
view: function (domainObject) {
let component;
return {
show: function (element) {
component = new Vue({
components: {
Notebook: Notebook.default
},
provide: {
openmct,
providedDomainObject: domainObject
},
el: element,
template: '<notebook></notebook>'
});
},
destroy: function (element) {
component.$destroy();
component = undefined;
}
};
},
priority: function () {
return 1;
}
};
}
return NotebookViewProvider;
});

View File

@ -0,0 +1,47 @@
/*****************************************************************************
* 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([
'./notebook-view-provider'
], function (
NotebookViewProvider
) {
return function plugin() {
return function install(openmct) {
openmct.objectViews.addProvider(new NotebookViewProvider(openmct));
openmct.types.addType('notebook', {
name: "Notebook SVC",
creatable: true,
description: "Create and save timestamped notes with embedded object snapshots.",
cssClass: 'icon-notebook',
initialize: function (domainObject) {
domainObject.configuration = {
sortOrder: 'oldest'
};
domainObject.entries = [];
}
});
};
};
});

View File

@ -35,7 +35,7 @@ define([
installed = true; installed = true;
openmct.legacyRegistry.register('notebook', { openmct.legacyRegistry.register('notebook-old', {
name: 'Notebook Plugin', name: 'Notebook Plugin',
extensions: { extensions: {
types: [ types: [
@ -73,10 +73,10 @@ define([
} }
}); });
openmct.legacyRegistry.enable('notebook'); openmct.legacyRegistry.enable('notebook-old');
openmct.objectViews.addProvider({ openmct.objectViews.addProvider({
key: 'notebook-vue', key: 'notebook-old',
name: 'Notebook View', name: 'Notebook View',
cssClass: 'icon-notebook', cssClass: 'icon-notebook',
canView: function (domainObject) { canView: function (domainObject) {

View File

@ -115,10 +115,22 @@
width: (tickWidth + 30) + 'px' width: (tickWidth + 30) + 'px'
}"> }">
<div class="gl-plot-label gl-plot-y-label"> <div class="gl-plot-label gl-plot-y-label" ng-if="!yKeyOptions">
{{ yAxis.get('label') }} {{ yAxis.get('label') }}
</div> </div>
<div class="gl-plot-label gl-plot-y-label" ng-if="yKeyOptions.length > 1 && series.length === 1">
<select class="gl-plot-y-label__select"
ng-model="yAxisLabel" ng-change="plot.toggleYAxisLabel(yAxisLabel, yKeyOptions, series[0])">
<option ng-repeat="option in yKeyOptions"
value="{{option.name}}"
ng-selected="option.name === yAxisLabel">
{{option.name}}
</option>
</select>
</div>
<mct-ticks axis="yAxis"> <mct-ticks axis="yAxis">
<div ng-repeat="tick in ticks track by tick.text" <div ng-repeat="tick in ticks track by tick.text"
class="gl-plot-tick gl-plot-y-tick-label" class="gl-plot-tick gl-plot-y-tick-label"

View File

@ -93,6 +93,8 @@ define([
this.$scope.series = this.config.series.models; this.$scope.series = this.config.series.models;
this.$scope.legend = this.config.legend; this.$scope.legend = this.config.legend;
this.$scope.yAxisLabel = this.config.yAxis.get('label');
this.cursorGuideVertical = this.$element[0].querySelector('.js-cursor-guide--v'); this.cursorGuideVertical = this.$element[0].querySelector('.js-cursor-guide--v');
this.cursorGuideHorizontal = this.$element[0].querySelector('.js-cursor-guide--h'); this.cursorGuideHorizontal = this.$element[0].querySelector('.js-cursor-guide--h');
this.cursorGuide = false; this.cursorGuide = false;
@ -103,9 +105,35 @@ define([
this.listenTo(this.$scope, 'plot:tickWidth', this.onTickWidthChange, this); this.listenTo(this.$scope, 'plot:tickWidth', this.onTickWidthChange, this);
this.listenTo(this.$scope, 'plot:highlight:set', this.onPlotHighlightSet, this); this.listenTo(this.$scope, 'plot:highlight:set', this.onPlotHighlightSet, this);
this.listenTo(this.$scope, 'plot:reinitializeCanvas', this.initCanvas, this); this.listenTo(this.$scope, 'plot:reinitializeCanvas', this.initCanvas, this);
this.listenTo(this.config.xAxis, 'change:displayRange', this.onXAxisChange, this); this.listenTo(this.config.xAxis, 'change:displayRange', this.onXAxisChange, this);
this.listenTo(this.config.yAxis, 'change:displayRange', this.onYAxisChange, this); this.listenTo(this.config.yAxis, 'change:displayRange', this.onYAxisChange, this);
this.setUpYAxisOptions();
};
MCTPlotController.prototype.setUpYAxisOptions = function () {
if (this.$scope.series.length === 1) {
let metadata = this.$scope.series[0].metadata;
this.$scope.yKeyOptions = metadata
.valuesForHints(['range'])
.map(function (o) {
return {
name: o.name,
key: o.key
};
});
// set yAxisLabel if none is set yet
if (this.$scope.yAxisLabel === 'none') {
let yKey = this.$scope.series[0].model.yKey,
yKeyModel = this.$scope.yKeyOptions.filter(o => o.key === yKey)[0];
this.$scope.yAxisLabel = yKeyModel.name;
}
} else {
this.$scope.yKeyOptions = undefined;
}
}; };
MCTPlotController.prototype.onXAxisChange = function (displayBounds) { MCTPlotController.prototype.onXAxisChange = function (displayBounds) {
@ -493,5 +521,13 @@ define([
this.cursorGuide = !this.cursorGuide; this.cursorGuide = !this.cursorGuide;
}; };
MCTPlotController.prototype.toggleYAxisLabel = function (label, options, series) {
let yAxisObject = options.filter(o => o.name === label)[0];
if (yAxisObject) {
series.emit('change:yKey', yAxisObject.key);
}
};
return MCTPlotController; return MCTPlotController;
}); });

View File

@ -41,6 +41,7 @@ define([
'./flexibleLayout/plugin', './flexibleLayout/plugin',
'./tabs/plugin', './tabs/plugin',
'./LADTable/plugin', './LADTable/plugin',
'./notebook-svc/plugin',
'./filters/plugin', './filters/plugin',
'./objectMigration/plugin', './objectMigration/plugin',
'./goToOriginalAction/plugin', './goToOriginalAction/plugin',
@ -66,6 +67,7 @@ define([
FlexibleLayout, FlexibleLayout,
Tabs, Tabs,
LADTable, LADTable,
NotebookSVC,
Filters, Filters,
ObjectMigration, ObjectMigration,
GoToOriginalAction, GoToOriginalAction,
@ -159,12 +161,13 @@ 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;
plugins.DisplayLayout = DisplayLayoutPlugin.default; plugins.DisplayLayout = DisplayLayoutPlugin.default;
plugins.FolderView = FolderView; plugins.FolderView = FolderView;
plugins.Tabs = Tabs; plugins.Tabs = Tabs;
plugins.FlexibleLayout = FlexibleLayout; plugins.FlexibleLayout = FlexibleLayout;
plugins.LADTable = LADTable; plugins.LADTable = LADTable;
plugins.NotebookSVC = NotebookSVC;
plugins.Filters = Filters; plugins.Filters = Filters;
plugins.ObjectMigration = ObjectMigration.default; plugins.ObjectMigration = ObjectMigration.default;
plugins.GoToOriginalAction = GoToOriginalAction.default; plugins.GoToOriginalAction = GoToOriginalAction.default;

View File

@ -23,6 +23,7 @@
export default class RemoveAction { export default class RemoveAction {
constructor(openmct) { constructor(openmct) {
this.name = 'Remove'; this.name = 'Remove';
this.key = 'remove';
this.description = 'Remove this object from its containing object.'; this.description = 'Remove this object from its containing object.';
this.cssClass = "icon-trash"; this.cssClass = "icon-trash";

View File

@ -49,6 +49,7 @@ define([
this.telemetryObjects = []; this.telemetryObjects = [];
this.outstandingRequests = 0; this.outstandingRequests = 0;
this.configuration = new TelemetryTableConfiguration(domainObject, openmct); this.configuration = new TelemetryTableConfiguration(domainObject, openmct);
this.paused = false;
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier); this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.addTelemetryObject = this.addTelemetryObject.bind(this); this.addTelemetryObject = this.addTelemetryObject.bind(this);
@ -219,7 +220,10 @@ define([
if (!this.telemetryObjects.includes(telemetryObject)) { if (!this.telemetryObjects.includes(telemetryObject)) {
return; return;
} }
this.processRealtimeDatum(datum, columnMap, keyString, limitEvaluator);
if (!this.paused) {
this.processRealtimeDatum(datum, columnMap, keyString, limitEvaluator);
}
}, subscribeOptions); }, subscribeOptions);
} }
@ -255,6 +259,17 @@ define([
} }
} }
pause() {
this.paused = true;
this.boundedRows.unsubscribeFromBounds();
}
unpause() {
this.paused = false;
this.boundedRows.subscribeToBounds();
this.refreshData();
}
destroy() { destroy() {
this.boundedRows.destroy(); this.boundedRows.destroy();
this.filteredRows.destroy(); this.filteredRows.destroy();

View File

@ -21,10 +21,11 @@
*****************************************************************************/ *****************************************************************************/
define(function () { define(function () {
class TelemetryTableColumn { class TelemetryTableColumn {
constructor (openmct, metadatum) { constructor (openmct, metadatum, options = {selectable: false}) {
this.metadatum = metadatum; this.metadatum = metadatum;
this.formatter = openmct.telemetry.getValueFormatter(metadatum); this.formatter = openmct.telemetry.getValueFormatter(metadatum);
this.titleValue = this.metadatum.name; this.titleValue = this.metadatum.name;
this.selectable = options.selectable;
} }
getKey() { getKey() {
@ -55,8 +56,7 @@ define(function () {
return formattedValue; return formattedValue;
} }
} }
}
};
return TelemetryTableColumn; return TelemetryTableColumn;
}); });

View File

@ -29,7 +29,7 @@ define([], function () {
this.limitEvaluator = limitEvaluator; this.limitEvaluator = limitEvaluator;
this.objectKeyString = objectKeyString; this.objectKeyString = objectKeyString;
} }
getFormattedDatum(headers) { getFormattedDatum(headers) {
return Object.keys(headers).reduce((formattedDatum, columnKey) => { return Object.keys(headers).reduce((formattedDatum, columnKey) => {
formattedDatum[columnKey] = this.getFormattedValue(columnKey); formattedDatum[columnKey] = this.getFormattedValue(columnKey);
@ -62,12 +62,16 @@ define([], function () {
this.cellLimitClasses = Object.values(this.columns).reduce((alarmStateMap, column) => { this.cellLimitClasses = Object.values(this.columns).reduce((alarmStateMap, column) => {
let limitEvaluation = this.limitEvaluator.evaluate(this.datum, column.getMetadatum()); let limitEvaluation = this.limitEvaluator.evaluate(this.datum, column.getMetadatum());
alarmStateMap[column.getKey()] = limitEvaluation && limitEvaluation.cssClass; alarmStateMap[column.getKey()] = limitEvaluation && limitEvaluation.cssClass;
return alarmStateMap; return alarmStateMap;
}, {}); }, {});
} }
return this.cellLimitClasses; return this.cellLimitClasses;
} }
getContextMenuActions() {
return [];
}
} }
/** /**
@ -85,4 +89,4 @@ define([], function () {
} }
return TelemetryTableRow; return TelemetryTableRow;
}); });

View File

@ -48,7 +48,7 @@ define([
canEdit(domainObject) { canEdit(domainObject) {
return domainObject.type === 'table'; return domainObject.type === 'table';
}, },
view(domainObject) { view(domainObject, isEditing, objectPath) {
let table = new TelemetryTable(domainObject, openmct); let table = new TelemetryTable(domainObject, openmct);
let component; let component;
return { return {
@ -64,10 +64,11 @@ define([
}, },
provide: { provide: {
openmct, openmct,
table table,
objectPath
}, },
el: element, el: element,
template: '<table-component :isEditing="isEditing"></table-component>' template: '<table-component :isEditing="isEditing" :enableMarking="true"></table-component>'
}); });
}, },
onEditModeChange(isEditing) { onEditModeChange(isEditing) {

View File

@ -43,7 +43,8 @@ define(
this.sortByTimeSystem(openmct.time.timeSystem()); this.sortByTimeSystem(openmct.time.timeSystem());
this.lastBounds = openmct.time.bounds(); this.lastBounds = openmct.time.bounds();
openmct.time.on('bounds', this.bounds);
this.subscribeToBounds();
} }
addOne(item) { addOne(item) {
@ -140,9 +141,17 @@ define(
return this.parseTime(row.datum[this.sortOptions.key]); return this.parseTime(row.datum[this.sortOptions.key]);
} }
destroy() { unsubscribeFromBounds() {
this.openmct.time.off('bounds', this.bounds); this.openmct.time.off('bounds', this.bounds);
} }
subscribeToBounds() {
this.openmct.time.on('bounds', this.bounds);
}
destroy() {
this.unsubscribeFromBounds();
}
} }
return BoundedTableRowCollection; return BoundedTableRowCollection;
}); });

View File

@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
<template> <template>
<td>{{formattedValue}}</td> <td @click="selectCell($event.currentTarget, columnKey)" :title="formattedValue">{{formattedValue}}</td>
</template> </template>
<script> <script>
export default { export default {
@ -33,11 +33,38 @@ export default {
columnKey: { columnKey: {
type: String, type: String,
require: true require: true
},
objectPath: {
type: Array,
require: false
} }
}, },
methods: {
selectCell(element, columnKey) {
if (this.isSelectable) {
this.openmct.selection.select([{
element: element,
context: {
type: 'table-cell',
row: this.row.objectKeyString,
column: columnKey
}
},{
element: this.openmct.layout.$refs.browseObject.$el,
context: {
item: this.objectPath[0]
}
}], false);
event.stopPropagation();
}
},
},
computed: { computed: {
formattedValue() { formattedValue() {
return this.row.getFormattedValue(this.columnKey); return this.row.getFormattedValue(this.columnKey);
},
isSelectable() {
return this.row.columns[this.columnKey].selectable;
} }
} }
}; };

View File

@ -20,37 +20,54 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
<template> <template>
<tr :style="{ top: rowTop }" :class="rowClass"> <tr :style="{ top: rowTop }"
<component class="noselect"
v-for="(title, key) in headers" :class="[
rowClass,
{'is-selected': marked}
]"
v-on="listeners">
<component v-for="(title, key) in headers"
:key="key" :key="key"
:is="componentList[key]" :is="componentList[key]"
:columnKey="key" :columnKey="key"
:style="columnWidths[key] === undefined ? {} : { width: columnWidths[key] + 'px', 'max-width': columnWidths[key] + 'px'}" :style="columnWidths[key] === undefined ? {} : { width: columnWidths[key] + 'px', 'max-width': columnWidths[key] + 'px'}"
:title="formattedRow[key]" :class="[cellLimitClasses[key], selectableColumns[key] ? 'is-selectable' : '']"
:class="cellLimitClasses[key]" :objectPath="objectPath"
class="is-selectable" :row="row">
@click="selectCell($event.currentTarget, key)" </component>
:row="row"></component>
</tr> </tr>
</template> </template>
<style> <style>
.noselect {
-webkit-touch-callout: none; /* iOS Safari */
-webkit-user-select: none; /* Safari */
-khtml-user-select: none; /* Konqueror HTML */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* Internet Explorer/Edge */
user-select: none; /* Non-prefixed version, currently
supported by Chrome and Opera */
}
</style> </style>
<script> <script>
import TableCell from './table-cell.vue'; import TableCell from './table-cell.vue';
export default { export default {
inject: ['openmct', 'objectPath'],
data: function () { data: function () {
return { return {
rowTop: (this.rowOffset + this.rowIndex) * this.rowHeight + 'px', rowTop: (this.rowOffset + this.rowIndex) * this.rowHeight + 'px',
formattedRow: this.row.getFormattedDatum(this.headers),
rowClass: this.row.getRowClass(), rowClass: this.row.getRowClass(),
cellLimitClasses: this.row.getCellLimitClasses(), cellLimitClasses: this.row.getCellLimitClasses(),
componentList: Object.keys(this.headers).reduce((components, header) => { componentList: Object.keys(this.headers).reduce((components, header) => {
components[header] = this.row.getCellComponentName(header) || 'table-cell'; components[header] = this.row.getCellComponentName(header) || 'table-cell';
return components return components
}, {}),
selectableColumns : Object.keys(this.row.columns).reduce((selectable, columnKeys) => {
selectable[columnKeys] = this.row.columns[columnKeys].selectable;
return selectable;
}, {}) }, {})
} }
}, },
@ -67,6 +84,10 @@ export default {
type: Object, type: Object,
required: true required: true
}, },
objectPath: {
type: Array,
required: false
},
rowIndex: { rowIndex: {
type: Number, type: Number,
required: false, required: false,
@ -81,6 +102,11 @@ export default {
type: Number, type: Number,
required: false, required: false,
default: 0 default: 0
},
marked: {
type: Boolean,
required: false,
default: false
} }
}, },
methods: { methods: {
@ -88,26 +114,54 @@ export default {
this.rowTop = (rowOffset + this.rowIndex) * this.rowHeight + 'px'; this.rowTop = (rowOffset + this.rowIndex) * this.rowHeight + 'px';
}, },
formatRow: function (row) { formatRow: function (row) {
this.formattedRow = row.getFormattedDatum(this.headers);
this.rowClass = row.getRowClass(); this.rowClass = row.getRowClass();
this.cellLimitClasses = row.getCellLimitClasses(); this.cellLimitClasses = row.getCellLimitClasses();
}, },
markRow: function (event) {
let keyCtrlModifier = false;
if (event.ctrlKey || event.metaKey) {
keyCtrlModifier = true;
}
if (event.shiftKey) {
this.$emit('markMultipleConcurrent', this.rowIndex);
} else {
if (this.marked) {
this.$emit('unmark', this.rowIndex, keyCtrlModifier);
} else {
this.$emit('mark', this.rowIndex, keyCtrlModifier);
}
}
},
selectCell(element, columnKey) { selectCell(element, columnKey) {
//TODO: This is a hack. Cannot get parent this way. if (this.selectableColumns[columnKey]) {
this.openmct.selection.select([{ //TODO: This is a hack. Cannot get parent this way.
element: element, this.openmct.selection.select([{
context: { element: element,
type: 'table-cell', context: {
row: this.row.objectKeyString, type: 'table-cell',
column: columnKey row: this.row.objectKeyString,
} column: columnKey
},{ }
element: this.openmct.layout.$refs.browseObject.$el, },{
context: { element: this.openmct.layout.$refs.browseObject.$el,
item: this.openmct.router.path[0] context: {
} item: this.openmct.router.path[0]
}], false); }
event.stopPropagation(); }], false);
event.stopPropagation();
}
},
showContextMenu: function (event) {
event.preventDefault();
this.openmct.objects.get(this.row.objectKeyString).then((domainObject) => {
let contextualObjectPath = this.objectPath.slice();
contextualObjectPath.unshift(domainObject);
this.openmct.contextMenu._showContextMenuForObjectPath(contextualObjectPath, event.x, event.y, this.row.getContextMenuActions());
});
} }
}, },
// TODO: use computed properties // TODO: use computed properties
@ -120,6 +174,19 @@ export default {
}, },
components: { components: {
TableCell TableCell
},
computed: {
listeners() {
let listenersObject = {
click: this.markRow
}
if (this.row.getContextMenuActions().length) {
listenersObject.contextmenu = this.showContextMenu;
}
return listenersObject;
}
} }
} }
</script> </script>

View File

@ -20,95 +20,135 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
<template> <template>
<div class="c-table c-telemetry-table c-table--filterable c-table--sortable has-control-bar" <div class="c-table-wrapper">
:class="{'loading': loading}"> <div class="c-table-control-bar c-control-bar">
<div :style="{ 'max-width': widthWithScroll, 'min-width': '150px'}"><slot></slot></div>
<div v-if="allowExport" class="c-table__control-bar c-control-bar">
<button class="c-button icon-download labeled" <button class="c-button icon-download labeled"
v-on:click="exportAsCSV()" v-if="allowExport"
title="Export This View's Data"> v-on:click="exportAllDataAsCSV()"
<span class="c-button__label">Export As CSV</span> title="Export This View's Data">
<span class="c-button__label">Export Table Data</span>
</button>
<button class="c-button icon-download labeled"
v-if="allowExport"
v-show="markedRows.length"
v-on:click="exportMarkedDataAsCSV()"
title="Export Marked Rows As CSV">
<span class="c-button__label">Export Marked Rows</span>
</button>
<button class="c-button icon-x labeled"
v-show="markedRows.length"
v-on:click="unmarkAllRows()"
title="Unmark All Rows">
<span class="c-button__label">Unmark All Rows</span>
</button>
<div v-if="enableMarking"
class="c-separator">
</div>
<button v-if="enableMarking"
class="c-button icon-pause pause-play labeled"
:class=" paused ? 'icon-play is-paused' : 'icon-pause'"
v-on:click="togglePauseByButton()"
:title="paused ? 'Continue Data Flow' : 'Pause Data Flow'">
<span class="c-button__label">
{{paused ? 'Play' : 'Pause'}}
</span>
</button> </button>
<slot name="buttons"></slot> <slot name="buttons"></slot>
</div> </div>
<div v-if="isDropTargetActive" class="c-telemetry-table__drop-target" :style="dropTargetStyle"></div>
<!-- Headers table --> <div class="c-table c-telemetry-table c-table--filterable c-table--sortable has-control-bar"
<div class="c-telemetry-table__headers-w js-table__headers-w" ref="headersTable" :style="{ 'max-width': widthWithScroll}"> :class="{
<table class="c-table__headers c-telemetry-table__headers"> 'loading': loading,
<thead> 'paused' : paused
<tr class="c-telemetry-table__headers__labels"> }">
<table-column-header
v-for="(title, key, headerIndex) in headers" <div :style="{ 'max-width': widthWithScroll, 'min-width': '150px'}"><slot></slot></div>
:key="key"
:headerKey="key" <div v-if="isDropTargetActive" class="c-telemetry-table__drop-target" :style="dropTargetStyle"></div>
:headerIndex="headerIndex" <!-- Headers table -->
@sort="allowSorting && sortBy(key)" <div class="c-telemetry-table__headers-w js-table__headers-w" ref="headersTable" :style="{ 'max-width': widthWithScroll}">
@resizeColumn="resizeColumn" <table class="c-table__headers c-telemetry-table__headers">
@dropTargetOffsetChanged="setDropTargetOffset" <thead>
@dropTargetActive="dropTargetActive" <tr class="c-telemetry-table__headers__labels">
@reorderColumn="reorderColumn" <table-column-header
@resizeColumnEnd="updateConfiguredColumnWidths" v-for="(title, key, headerIndex) in headers"
:columnWidth="columnWidths[key]" :key="key"
:sortOptions="sortOptions" :headerKey="key"
:isEditing="isEditing" :headerIndex="headerIndex"
><span class="c-telemetry-table__headers__label">{{title}}</span> @sort="allowSorting && sortBy(key)"
</table-column-header> @resizeColumn="resizeColumn"
</tr> @dropTargetOffsetChanged="setDropTargetOffset"
<tr class="c-telemetry-table__headers__filter"> @dropTargetActive="dropTargetActive"
<table-column-header @reorderColumn="reorderColumn"
v-for="(title, key, headerIndex) in headers" @resizeColumnEnd="updateConfiguredColumnWidths"
:key="key" :columnWidth="columnWidths[key]"
:headerKey="key" :sortOptions="sortOptions"
:headerIndex="headerIndex" :isEditing="isEditing"
@resizeColumn="resizeColumn" ><span class="c-telemetry-table__headers__label">{{title}}</span>
@dropTargetOffsetChanged="setDropTargetOffset" </table-column-header>
@dropTargetActive="dropTargetActive" </tr>
@reorderColumn="reorderColumn" <tr class="c-telemetry-table__headers__filter">
@resizeColumnEnd="updateConfiguredColumnWidths" <table-column-header
:columnWidth="columnWidths[key]" v-for="(title, key, headerIndex) in headers"
:isEditing="isEditing" :key="key"
> :headerKey="key"
<search class="c-table__search" :headerIndex="headerIndex"
v-model="filters[key]" @resizeColumn="resizeColumn"
v-on:input="filterChanged(key)" @dropTargetOffsetChanged="setDropTargetOffset"
v-on:clear="clearFilter(key)" /> @dropTargetActive="dropTargetActive"
</table-column-header> @reorderColumn="reorderColumn"
</tr> @resizeColumnEnd="updateConfiguredColumnWidths"
</thead> :columnWidth="columnWidths[key]"
:isEditing="isEditing"
>
<search class="c-table__search"
v-model="filters[key]"
v-on:input="filterChanged(key)"
v-on:clear="clearFilter(key)" />
</table-column-header>
</tr>
</thead>
</table>
</div>
<!-- Content table -->
<div class="c-table__body-w c-telemetry-table__body-w js-telemetry-table__body-w" @scroll="scroll" :style="{ 'max-width': widthWithScroll}">
<div class="c-telemetry-table__scroll-forcer" :style="{ width: totalWidth + 'px' }"></div>
<table class="c-table__body c-telemetry-table__body js-telemetry-table__content"
:style="{ height: totalHeight + 'px'}">
<tbody>
<telemetry-table-row v-for="(row, rowIndex) in visibleRows"
:headers="headers"
:columnWidths="columnWidths"
:rowIndex="rowIndex"
:objectPath="objectPath"
:rowOffset="rowOffset"
:rowHeight="rowHeight"
:row="row"
:marked="row.marked"
@mark="markRow"
@unmark="unmarkRow"
@markMultipleConcurrent="markMultipleConcurrentRows">
</telemetry-table-row>
</tbody>
</table>
</div>
<!-- Sizing table -->
<table class="c-telemetry-table__sizing js-telemetry-table__sizing" :style="sizingTableWidth">
<tr>
<template v-for="(title, key) in headers">
<th :key="key" :style="{ width: configuredColumnWidths[key] + 'px', 'max-width': configuredColumnWidths[key] + 'px'}">{{title}}</th>
</template>
</tr>
<telemetry-table-row v-for="(sizingRowData, objectKeyString) in sizingRows"
:key="objectKeyString"
:headers="headers"
:columnWidths="configuredColumnWidths"
:row="sizingRowData">
</telemetry-table-row>
</table> </table>
<telemetry-filter-indicator></telemetry-filter-indicator>
</div> </div>
<!-- Content table --> </div><!-- closes c-table-wrapper -->
<div class="c-table__body-w c-telemetry-table__body-w js-telemetry-table__body-w" @scroll="scroll" :style="{ 'max-width': widthWithScroll}">
<div class="c-telemetry-table__scroll-forcer" :style="{ width: totalWidth + 'px' }"></div>
<table class="c-table__body c-telemetry-table__body js-telemetry-table__content"
:style="{ height: totalHeight + 'px'}">
<tbody>
<telemetry-table-row v-for="(row, rowIndex) in visibleRows"
:headers="headers"
:columnWidths="columnWidths"
:rowIndex="rowIndex"
:rowOffset="rowOffset"
:rowHeight="rowHeight"
:row="row">
</telemetry-table-row>
</tbody>
</table>
</div>
<!-- Sizing table -->
<table class="c-telemetry-table__sizing js-telemetry-table__sizing" :style="sizingTableWidth">
<tr>
<template v-for="(title, key) in headers">
<th :key="key" :style="{ width: configuredColumnWidths[key] + 'px', 'max-width': configuredColumnWidths[key] + 'px'}">{{title}}</th>
</template>
</tr>
<telemetry-table-row v-for="(sizingRowData, objectKeyString) in sizingRows"
:headers="headers"
:columnWidths="configuredColumnWidths"
:row="sizingRowData">
</telemetry-table-row>
</table>
<telemetry-filter-indicator></telemetry-filter-indicator>
</div>
</template> </template>
<style lang="scss"> <style lang="scss">
@ -134,7 +174,7 @@
display: block; display: block;
flex: 1 0 auto; flex: 1 0 auto;
width: 100px; width: 100px;
vertical-align: middle; // This is crucial to hiding f**king 4px height injected by browser by default vertical-align: middle; // This is crucial to hiding 4px height injected by browser by default
} }
td { td {
@ -219,6 +259,10 @@
align-items: stretch; align-items: stretch;
position: absolute; position: absolute;
height: 18px; // Needed when a row has empty values in its cells height: 18px; // Needed when a row has empty values in its cells
&.is-selected {
background-color: $colorSelectedBg;
}
} }
td { td {
@ -269,6 +313,10 @@
} }
} }
.paused {
border: 1px solid #ff9900;
}
/******************************* LEGACY */ /******************************* LEGACY */
.s-status-taking-snapshot, .s-status-taking-snapshot,
.overlay.snapshot { .overlay.snapshot {
@ -301,7 +349,7 @@ export default {
search, search,
TelemetryFilterIndicator TelemetryFilterIndicator
}, },
inject: ['table', 'openmct'], inject: ['table', 'openmct', 'objectPath'],
props: { props: {
isEditing: { isEditing: {
type: Boolean, type: Boolean,
@ -318,6 +366,10 @@ export default {
allowSorting: { allowSorting: {
'type': Boolean, 'type': Boolean,
'default': true 'default': true
},
enableMarking: {
type: Boolean,
default: false
} }
}, },
data() { data() {
@ -346,7 +398,10 @@ export default {
dropOffsetLeft: undefined, dropOffsetLeft: undefined,
isDropTargetActive: false, isDropTargetActive: false,
isAutosizeEnabled: configuration.autosize, isAutosizeEnabled: configuration.autosize,
scrollW: 0 scrollW: 0,
markCounter: 0,
paused: false,
markedRows: []
} }
}, },
computed: { computed: {
@ -532,15 +587,27 @@ export default {
// which causes subsequent scroll to use an out of date height. // which causes subsequent scroll to use an out of date height.
this.contentTable.style.height = this.totalHeight + 'px'; this.contentTable.style.height = this.totalHeight + 'px';
}, },
exportAsCSV() { exportAsCSV(data) {
const headerKeys = Object.keys(this.headers); const headerKeys = Object.keys(this.headers);
const justTheData = this.table.filteredRows.getRows()
.map(row => row.getFormattedDatum(this.headers)); this.csvExporter.export(data, {
this.csvExporter.export(justTheData, {
filename: this.table.domainObject.name + '.csv', filename: this.table.domainObject.name + '.csv',
headers: headerKeys headers: headerKeys
}); });
}, },
exportAllDataAsCSV() {
const justTheData = this.table.filteredRows.getRows()
.map(row => row.getFormattedDatum(this.headers));
this.exportAsCSV(justTheData);
},
exportMarkedDataAsCSV() {
const data = this.table.filteredRows.getRows()
.filter(row => row.marked === true)
.map(row => row.getFormattedDatum(this.headers));
this.exportAsCSV(data);
},
outstandingRequests(loading) { outstandingRequests(loading) {
this.loading = loading; this.loading = loading;
}, },
@ -632,8 +699,105 @@ export default {
clearRowsAndRerender() { clearRowsAndRerender() {
this.visibleRows = []; this.visibleRows = [];
this.$nextTick().then(this.updateVisibleRows); this.$nextTick().then(this.updateVisibleRows);
} },
pause(pausedByButton) {
if (pausedByButton) {
this.pausedByButton = true;
}
this.paused = true;
this.table.pause();
},
unpause(unpausedByButton) {
if (unpausedByButton) {
this.paused = false;
this.table.unpause();
this.markedRows = [];
this.pausedByButton = false;
} else {
if (!this.pausedByButton) {
this.paused = false;
this.table.unpause();
this.markedRows = [];
}
}
},
togglePauseByButton() {
if (this.paused) {
this.unpause(true);
} else {
this.pause(true);
}
},
undoMarkedRows(unpause) {
this.markedRows.forEach(r => r.marked = false);
this.markedRows = [];
},
unmarkRow(rowIndex) {
this.undoMarkedRows();
this.unpause();
},
markRow(rowIndex, keyModifier) {
if (!this.enableMarking) {
return;
}
let insertMethod = 'unshift';
if (this.markedRows.length && !keyModifier) {
this.undoMarkedRows();
insertMethod = 'push';
}
let markedRow = this.visibleRows[rowIndex];
this.$set(markedRow, 'marked', true);
this.pause();
this.markedRows[insertMethod](markedRow);
},
unmarkAllRows(skipUnpause) {
this.markedRows.forEach(row => row.marked = false);
this.markedRows = [];
this.unpause();
},
markMultipleConcurrentRows(rowIndex) {
if (!this.enableMarking) {
return;
}
if (!this.markedRows.length) {
this.markRow(rowIndex);
} else {
if (this.markedRows.length > 1) {
this.markedRows.forEach((r,i) => {
if (i !== 0) {
r.marked = false;
}
});
this.markedRows.splice(1);
}
let lastRowToBeMarked = this.visibleRows[rowIndex];
let allRows = this.table.filteredRows.getRows(),
firstRowIndex = allRows.indexOf(this.markedRows[0]),
lastRowIndex = allRows.indexOf(lastRowToBeMarked);
//supports backward selection
if (lastRowIndex < firstRowIndex) {
let temp = lastRowIndex;
lastRowIndex = firstRowIndex;
firstRowIndex = temp - 1;
}
for (var i = firstRowIndex + 1; i <= lastRowIndex; i++) {
let row = allRows[i];
row.marked = true;
this.markedRows.push(row);
}
}
}
}, },
created() { created() {
this.filterChanged = _.debounce(this.filterChanged, 500); this.filterChanged = _.debounce(this.filterChanged, 500);

View File

@ -167,6 +167,7 @@ export default {
this.xAxis.scale(this.xScale); this.xAxis.scale(this.xScale);
this.xAxis.tickFormat(utcMultiTimeFormat); this.xAxis.tickFormat(utcMultiTimeFormat);
this.axisElement.call(this.xAxis); this.axisElement.call(this.xAxis);
this.setScale();
}, },
getActiveFormatter() { getActiveFormatter() {
let timeSystem = this.openmct.time.timeSystem(); let timeSystem = this.openmct.time.timeSystem();

View File

@ -60,7 +60,6 @@ export default {
.filter(menuOption => menuOption.clock === (clock && clock.key)) .filter(menuOption => menuOption.clock === (clock && clock.key))
.map(menuOption => JSON.parse(JSON.stringify(this.openmct.time.timeSystems.get(menuOption.timeSystem)))); .map(menuOption => JSON.parse(JSON.stringify(this.openmct.time.timeSystems.get(menuOption.timeSystem))));
}, },
setTimeSystemFromView(timeSystem) { setTimeSystemFromView(timeSystem) {
if (timeSystem.key !== this.selectedTimeSystem.key) { if (timeSystem.key !== this.selectedTimeSystem.key) {
let activeClock = this.openmct.time.clock(); let activeClock = this.openmct.time.clock();
@ -69,7 +68,15 @@ export default {
timeSystem: timeSystem.key timeSystem: timeSystem.key
}); });
if (activeClock === undefined) { if (activeClock === undefined) {
this.openmct.time.timeSystem(timeSystem.key, configuration.bounds); let bounds;
if (this.selectedTimeSystem.isUTCBased && timeSystem.isUTCBased) {
bounds = this.openmct.time.bounds();
} else {
bounds = configuration.bounds;
}
this.openmct.time.timeSystem(timeSystem.key, bounds);
} else { } else {
this.openmct.time.timeSystem(timeSystem.key); this.openmct.time.timeSystem(timeSystem.key);
this.openmct.time.clockOffsets(configuration.clockOffsets); this.openmct.time.clockOffsets(configuration.clockOffsets);

View File

@ -211,6 +211,10 @@ $btnStdH: 24px;
$colorCursorGuide: rgba(white, 0.6); $colorCursorGuide: rgba(white, 0.6);
$shdwCursorGuide: rgba(black, 0.4) 0 0 2px; $shdwCursorGuide: rgba(black, 0.4) 0 0 2px;
$colorLocalControlOvrBg: rgba($colorBodyBg, 0.8); $colorLocalControlOvrBg: rgba($colorBodyBg, 0.8);
$colorSelectBg: $colorBtnBg; // This must be a solid color, not a gradient, due to usage of SVG bg in selects
$colorSelectFg: $colorBtnFg;
$colorSelectArw: lighten($colorBtnBg, 20%);
$shdwSelect: rgba(black, 0.5) 0 0.5px 3px;
// Menus // Menus
$colorMenuBg: pullForward($colorBodyBg, 15%); $colorMenuBg: pullForward($colorBodyBg, 15%);
@ -425,7 +429,3 @@ $createBtnTextTransform: uppercase;
background: linear-gradient(pullForward($c, 5%), $c); background: linear-gradient(pullForward($c, 5%), $c);
box-shadow: rgba(black, 0.5) 0 0.5px 2px; box-shadow: rgba(black, 0.5) 0 0.5px 2px;
} }
@mixin themedSelect($bg: $colorBtnBg, $fg: $colorBtnFg) {
@include cSelect(linear-gradient(lighten($bg, 5%), $bg), $fg, lighten($bg, 20%), rgba(black, 0.5) 0 0.5px 3px);
}

View File

@ -215,6 +215,10 @@ $btnStdH: 24px;
$colorCursorGuide: rgba(white, 0.6); $colorCursorGuide: rgba(white, 0.6);
$shdwCursorGuide: rgba(black, 0.4) 0 0 2px; $shdwCursorGuide: rgba(black, 0.4) 0 0 2px;
$colorLocalControlOvrBg: rgba($colorBodyBg, 0.8); $colorLocalControlOvrBg: rgba($colorBodyBg, 0.8);
$colorSelectBg: $colorBtnBg; // This must be a solid color, not a gradient, due to usage of SVG bg in selects
$colorSelectFg: $colorBtnFg;
$colorSelectArw: lighten($colorBtnBg, 20%);
$shdwSelect: rgba(black, 0.5) 0 0.5px 3px;
// Menus // Menus
$colorMenuBg: pullForward($colorBodyBg, 15%); $colorMenuBg: pullForward($colorBodyBg, 15%);
@ -430,10 +434,6 @@ $createBtnTextTransform: uppercase;
box-shadow: rgba(black, 0.5) 0 0.5px 2px; box-shadow: rgba(black, 0.5) 0 0.5px 2px;
} }
@mixin themedSelect($bg: $colorBtnBg, $fg: $colorBtnFg) {
@include cSelect(linear-gradient(lighten($bg, 5%), $bg), $fg, lighten($bg, 20%), rgba(black, 0.5) 0 0.5px 3px);
}
/**************************************************** OVERRIDES */ /**************************************************** OVERRIDES */
.c-frame { .c-frame {
&:not(.no-frame) { &:not(.no-frame) {

View File

@ -211,6 +211,10 @@ $btnStdH: 24px;
$colorCursorGuide: rgba(black, 0.6); $colorCursorGuide: rgba(black, 0.6);
$shdwCursorGuide: rgba(white, 0.4) 0 0 2px; $shdwCursorGuide: rgba(white, 0.4) 0 0 2px;
$colorLocalControlOvrBg: rgba($colorBodyFg, 0.8); $colorLocalControlOvrBg: rgba($colorBodyFg, 0.8);
$colorSelectBg: $colorBtnBg; // This must be a solid color, not a gradient, due to usage of SVG bg in selects
$colorSelectFg: $colorBtnFg;
$colorSelectArw: lighten($colorBtnBg, 20%);
$shdwSelect: none;
// Menus // Menus
$colorMenuBg: pushBack($colorBodyBg, 10%); $colorMenuBg: pushBack($colorBodyBg, 10%);
@ -424,7 +428,3 @@ $createBtnTextTransform: uppercase;
@mixin themedButton($c: $colorBtnBg) { @mixin themedButton($c: $colorBtnBg) {
background: $c; background: $c;
} }
@mixin themedSelect($bg: $colorBtnBg, $fg: $colorBtnFg) {
@include cSelect($bg, $fg, lighten($bg, 20%), none);
}

View File

@ -279,7 +279,10 @@ input[type=number]::-webkit-outer-spin-button {
// SELECTS // SELECTS
select { select {
@include appearanceNone(); @include appearanceNone();
@include themedSelect(); background-color: $colorSelectBg;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10'%3e%3cpath fill='%23#{svgColorFromHex($colorSelectArw)}' d='M5 5l5-5H0z'/%3e%3c/svg%3e");
color: $colorSelectFg;
box-shadow: $shdwSelect;
background-repeat: no-repeat, no-repeat; background-repeat: no-repeat, no-repeat;
background-position: right .4em top 80%, 0 0; background-position: right .4em top 80%, 0 0;
border: none; border: none;
@ -600,15 +603,15 @@ select {
margin-right: $m; margin-right: $m;
} }
.c-separator {
@include cToolbarSeparator();
}
.c-toolbar { .c-toolbar {
> * + * { > * + * {
margin-left: 2px; margin-left: 2px;
} }
&__button {
}
&__separator { &__separator {
@include cToolbarSeparator(); @include cToolbarSeparator();
} }

View File

@ -161,8 +161,7 @@ mct-plot {
height: auto; height: auto;
} }
&.gl-plot-y-label, &.gl-plot-y-label {
&.l-plot-y-label {
$x: -50%; $x: -50%;
$r: -90deg; $r: -90deg;
transform-origin: 50% 0; transform-origin: 50% 0;
@ -172,6 +171,12 @@ mct-plot {
left: 0; left: 0;
top: 50%; top: 50%;
white-space: nowrap; white-space: nowrap;
select {
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10'%3e%3cpath fill='%23#{svgColorFromHex($colorSelectArw)}' d='M0 5l5 5V0L0 5z'/%3e%3c/svg%3e");
background-position: left .4em top 50%, 0 0;
padding: 1px $interiorMargin 1px 20px;
}
} }
} }

View File

@ -585,6 +585,11 @@
} }
} }
@function svgColorFromHex($hexColor) {
// Remove initial # in color value
@return str-slice(inspect($hexColor), 2, str-length(inspect($hexColor)));
}
@mixin test($c: deeppink, $a: 0.3) { @mixin test($c: deeppink, $a: 0.3) {
background: rgba($c, $a) !important; background: rgba($c, $a) !important;
background-color: rgba($c, $a) !important; background-color: rgba($c, $a) !important;

View File

@ -76,23 +76,43 @@ div.c-table {
height: 100%; height: 100%;
} }
.c-table-wrapper {
// Wraps .c-control-bar and .c-table
@include abs();
overflow: hidden;
display: flex;
flex-direction: column;
> .c-table {
height: auto;
flex: 1 1 auto;
}
> * + * {
margin-top: $interiorMarginSm;
}
}
.c-table-control-bar {
display: flex;
flex: 0 0 auto;
> * + * {
margin-left: $interiorMarginSm;
}
}
.c-table { .c-table {
// Can be used by any type of table, scrolling, LAD, etc. // Can be used by any type of table, scrolling, LAD, etc.
$min-w: 50px; $min-w: 50px;
width: 100%; width: 100%;
&__control-bar,
&__headers-w { &__headers-w {
flex: 0 0 auto; flex: 0 0 auto;
} }
/******************************* ELEMENTS */ /******************************* ELEMENTS */
&__control-bar {
margin-bottom: $interiorMarginSm;
}
thead tr, thead tr,
&.c-table__headers { &.c-table__headers {
background: $colorTabHeaderBg; background: $colorTabHeaderBg;

View File

@ -44,7 +44,8 @@
class="c-so-view__object-view" class="c-so-view__object-view"
ref="objectView" ref="objectView"
:object="domainObject" :object="domainObject"
:show-edit-view="showEditView"> :show-edit-view="showEditView"
:object-path="objectPath">
</object-view> </object-view>
</div> </div>
</template> </template>
@ -123,7 +124,12 @@
.c-click-icon, .c-click-icon,
.c-button { .c-button {
// Shrink buttons a bit when they appear in a frame // Shrink buttons a bit when they appear in a frame
font-size: 0.8em; align-items: baseline;
font-size: 0.85em;
padding: 3px 5px;
&:before {
font-size: 0.8em;
}
} }
} }
</style> </style>

View File

@ -111,7 +111,7 @@ export default {
event.dataTransfer.setData("openmct/composable-domain-object", JSON.stringify(this.domainObject)); event.dataTransfer.setData("openmct/composable-domain-object", JSON.stringify(this.domainObject));
} }
// serialize domain object anyway, because some views can drag-and-drop objects without composition // serialize domain object anyway, because some views can drag-and-drop objects without composition
// (eg. notabook.) // (eg. notebook.)
event.dataTransfer.setData("openmct/domain-object-path", serializedPath); event.dataTransfer.setData("openmct/domain-object-path", serializedPath);
event.dataTransfer.setData(`openmct/domain-object/${keyString}`, this.domainObject); event.dataTransfer.setData(`openmct/domain-object/${keyString}`, this.domainObject);
} }

View File

@ -9,7 +9,8 @@ export default {
props: { props: {
view: String, view: String,
object: Object, object: Object,
showEditView: Boolean showEditView: Boolean,
objectPath: Array
}, },
destroyed() { destroyed() {
this.clear(); this.clear();
@ -91,17 +92,19 @@ export default {
return; return;
} }
let objectPath = this.currentObjectPath || this.objectPath;
if (provider.edit && this.showEditView) { if (provider.edit && this.showEditView) {
if (this.openmct.editor.isEditing()) { if (this.openmct.editor.isEditing()) {
this.currentView = provider.edit(this.currentObject); this.currentView = provider.edit(this.currentObject, true, objectPath);
} else { } else {
this.currentView = provider.view(this.currentObject, false); this.currentView = provider.view(this.currentObject, false, objectPath);
} }
this.openmct.editor.on('isEditing', this.toggleEditView); this.openmct.editor.on('isEditing', this.toggleEditView);
this.releaseEditModeHandler = () => this.openmct.editor.off('isEditing', this.toggleEditView); this.releaseEditModeHandler = () => this.openmct.editor.off('isEditing', this.toggleEditView);
} else { } else {
this.currentView = provider.view(this.currentObject, this.openmct.editor.isEditing()); this.currentView = provider.view(this.currentObject, this.openmct.editor.isEditing(), objectPath);
if (this.currentView.onEditModeChange) { if (this.currentView.onEditModeChange) {
this.openmct.editor.on('isEditing', this.invokeEditModeHandler); this.openmct.editor.on('isEditing', this.invokeEditModeHandler);
@ -117,7 +120,7 @@ export default {
this.openmct.objectViews.on('clearData', this.clearData); this.openmct.objectViews.on('clearData', this.clearData);
}, },
show(object, viewKey, immediatelySelect) { show(object, viewKey, immediatelySelect, currentObjectPath) {
if (this.unlisten) { if (this.unlisten) {
this.unlisten(); this.unlisten();
} }
@ -132,6 +135,11 @@ export default {
} }
this.currentObject = object; this.currentObject = object;
if (currentObjectPath) {
this.currentObjectPath = currentObjectPath;
}
this.unlisten = this.openmct.objects.observe(this.currentObject, '*', (mutatedObject) => { this.unlisten = this.openmct.objects.observe(this.currentObject, '*', (mutatedObject) => {
this.currentObject = mutatedObject; this.currentObject = mutatedObject;
}); });

View File

@ -19,28 +19,11 @@
</div> </div>
<div class="l-browse-bar__end"> <div class="l-browse-bar__end">
<div class="l-browse-bar__view-switcher c-ctrl-wrapper c-ctrl-wrapper--menus-left" <view-switcher
v-if="views.length > 1"> :currentView="currentView"
<button class="c-button--menu" :views="views"
:class="currentView.cssClass" @setView="setView">
title="Switch view type" </view-switcher>
@click.stop="toggleViewMenu">
<span class="c-button__label">
{{ currentView.name }}
</span>
</button>
<div class="c-menu" v-show="showViewMenu">
<ul>
<li v-for="(view, index) in views"
@click="setView(view)"
:key="index"
:class="view.cssClass"
:title="view.name">
{{ view.name }}
</li>
</ul>
</div>
</div>
<!-- Action buttons --> <!-- Action buttons -->
<div class="l-browse-bar__actions"> <div class="l-browse-bar__actions">
<button v-if="notebookEnabled" <button v-if="notebookEnabled"
@ -77,14 +60,15 @@
<script> <script>
import NotebookSnapshot from '../utils/notebook-snapshot'; import NotebookSnapshot from '../utils/notebook-snapshot';
import ViewSwitcher from './ViewSwitcher.vue';
const PLACEHOLDER_OBJECT = {}; const PLACEHOLDER_OBJECT = {};
export default { export default {
inject: ['openmct'], inject: ['openmct'],
components: {
ViewSwitcher
},
methods: { methods: {
toggleViewMenu() {
this.showViewMenu = !this.showViewMenu;
},
toggleSaveMenu() { toggleSaveMenu() {
this.showSaveMenu = !this.showSaveMenu; this.showSaveMenu = !this.showSaveMenu;
}, },

View File

@ -0,0 +1,55 @@
<template>
<div class="l-browse-bar__view-switcher c-ctrl-wrapper c-ctrl-wrapper--menus-left"
v-if="views.length > 1">
<button class="c-button--menu"
:class="currentView.cssClass"
title="Switch view type"
@click.stop="toggleViewMenu">
<span class="c-button__label">
{{ currentView.name }}
</span>
</button>
<div class="c-menu" v-show="showViewMenu">
<ul>
<li v-for="(view, index) in views"
@click="setView(view)"
:key="index"
:class="view.cssClass"
:title="view.name">
{{ view.name }}
</li>
</ul>
</div>
</div>
</template>
<script>
export default {
props: [
'currentView',
'views'
],
data() {
return {
showViewMenu: false
}
},
methods: {
setView(view) {
this.$emit('setView', view);
},
toggleViewMenu() {
this.showViewMenu = !this.showViewMenu;
},
hideViewMenu() {
this.showViewMenu = false;
}
},
mounted() {
document.addEventListener('click', this.hideViewMenu);
},
destroyed() {
document.removeEventListener('click', this.hideViewMenu);
}
}
</script>

View File

@ -33,6 +33,11 @@
</div> </div>
<div class="l-browse-bar__end"> <div class="l-browse-bar__end">
<div class="l-browse-bar__actions"> <div class="l-browse-bar__actions">
<view-switcher
:views="views"
:currentView="currentView"
@setView="setView">
</view-switcher>
<button v-if="notebookEnabled" <button v-if="notebookEnabled"
class="l-browse-bar__actions__edit c-button icon-notebook" class="l-browse-bar__actions__edit c-button icon-notebook"
title="New Notebook entry" title="New Notebook entry"
@ -80,20 +85,52 @@
<script> <script>
import ContextMenuDropDown from '../../ui/components/contextMenuDropDown.vue'; import ContextMenuDropDown from '../../ui/components/contextMenuDropDown.vue';
import ViewSwitcher from '../../ui/layout/ViewSwitcher.vue';
import NotebookSnapshot from '../utils/notebook-snapshot'; import NotebookSnapshot from '../utils/notebook-snapshot';
export default { export default {
components: { components: {
ContextMenuDropDown ContextMenuDropDown,
ViewSwitcher
}, },
inject: [ inject: [
'openmct', 'openmct',
'objectPath' 'objectPath'
], ],
computed: {
views() {
return this
.openmct
.objectViews
.get(this.domainObject);
},
currentView() {
return this.views.filter(v => v.key === this.viewKey)[0] || {};
}
},
methods: { methods: {
snapshot() { snapshot() {
let element = document.getElementsByClassName("l-preview-window__object-view")[0]; let element = document.getElementsByClassName("l-preview-window__object-view")[0];
this.notebookSnapshot.capture(this.domainObject, element); this.notebookSnapshot.capture(this.domainObject, element);
},
clear() {
if (this.view) {
this.view.destroy();
this.$refs.objectView.innerHTML = '';
}
delete this.view;
delete this.viewContainer;
},
setView(view) {
this.clear();
this.viewKey = view.key;
this.viewContainer = document.createElement('div');
this.viewContainer.classList.add('c-object-view','u-contents');
this.$refs.objectView.append(this.viewContainer);
this.view = this.currentView.view(this.domainObject, false, this.objectPath);
this.view.show(this.viewContainer, false);
} }
}, },
data() { data() {
@ -103,13 +140,13 @@
return { return {
domainObject: domainObject, domainObject: domainObject,
type: type, type: type,
notebookEnabled: false notebookEnabled: false,
viewKey: undefined
}; };
}, },
mounted() { mounted() {
let viewProvider = this.openmct.objectViews.get(this.domainObject)[0]; let view = this.openmct.objectViews.get(this.domainObject)[0];
this.view = viewProvider.view(this.domainObject); this.setView(view);
this.view.show(this.$refs.objectView, false);
if (this.openmct.types.get('notebook')) { if (this.openmct.types.get('notebook')) {
this.notebookSnapshot = new NotebookSnapshot(this.openmct); this.notebookSnapshot = new NotebookSnapshot(this.openmct);

View File

@ -28,6 +28,7 @@ export default class PreviewAction {
* Metadata * Metadata
*/ */
this.name = 'Preview'; this.name = 'Preview';
this.key = 'preview';
this.description = 'Preview in large dialog'; this.description = 'Preview in large dialog';
this.cssClass = 'icon-eye-open'; this.cssClass = 'icon-eye-open';

View File

@ -0,0 +1,35 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import PreviewAction from './PreviewAction';
export default class ViewHistoricalDataAction extends PreviewAction {
constructor(openmct) {
super(openmct);
this.name = 'View Historical Data';
this.key = 'viewHistoricalData';
this.description = 'View Historical Data in a Table or Plot';
this.cssClass = 'icon-eye-open';
this.hideInDefaultMenu = true;
}
}

View File

@ -20,9 +20,11 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
import PreviewAction from './PreviewAction.js'; import PreviewAction from './PreviewAction.js';
import ViewHistoricalDataAction from './ViewHistoricalDataAction';
export default function () { export default function () {
return function (openmct) { return function (openmct) {
openmct.contextMenu.registerAction(new PreviewAction(openmct)); openmct.contextMenu.registerAction(new PreviewAction(openmct));
} openmct.contextMenu.registerAction(new ViewHistoricalDataAction(openmct));
};
} }

View File

@ -223,11 +223,11 @@ define(['EventEmitter'], function (EventEmitter) {
/** /**
* Provide a view of this object. * Provide a view of this object.
* *
* When called by Open MCT, this may include additional arguments * When called by Open MCT, the following arguments will be passed to it:
* which are on the path to the object to be viewed; for instance, * @param {object} domainObject - the domainObject that the view is provided for
* when viewing "A Folder" within "My Items", this method will be * @param {boolean} isEditing - A boolean value indicating wether openmct is in a global edit mode
* invoked with "A Folder" (as a domain object) as the first argument, * @param {array} objectPath - The current contextual object path of the view object
* and "My Items" as the second argument. * eg current domainObject is located under MyItems which is under Root
* *
* @method view * @method view
* @memberof module:openmct.ViewProvider# * @memberof module:openmct.ViewProvider#

View File

@ -8,6 +8,7 @@ define([
let navigateCall = 0; let navigateCall = 0;
let browseObject; let browseObject;
let unobserve = undefined; let unobserve = undefined;
let currentObjectPath;
openmct.router.route(/^\/browse\/?$/, navigateToFirstChildOfRoot); openmct.router.route(/^\/browse\/?$/, navigateToFirstChildOfRoot);
@ -26,7 +27,9 @@ define([
}); });
function viewObject(object, viewProvider) { function viewObject(object, viewProvider) {
openmct.layout.$refs.browseObject.show(object, viewProvider.key, true); currentObjectPath = openmct.router.path;
openmct.layout.$refs.browseObject.show(object, viewProvider.key, true, currentObjectPath);
openmct.layout.$refs.browseBar.domainObject = object; openmct.layout.$refs.browseBar.domainObject = object;
openmct.layout.$refs.browseBar.viewKey = viewProvider.key; openmct.layout.$refs.browseBar.viewKey = viewProvider.key;
} }

View File

@ -90,7 +90,7 @@ class NotebookSnapshot {
}; };
function validateLocation(newParentObj) { function validateLocation(newParentObj) {
return newParentObj.model.type === 'notebook'; return newParentObj.model.type === 'notebook' || newParentObj.model.type === 'notebook-svc';
} }
return overlayModel; return overlayModel;