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.Notebook());
openmct.install(openmct.plugins.NotebookSVC());
openmct.install(openmct.plugins.Filters(['table', 'telemetry.plot.overlay']));
openmct.install(openmct.plugins.ObjectMigration());
openmct.install(openmct.plugins.ClearData(['table', 'telemetry.plot.overlay', 'telemetry.plot.stacked']));

View File

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

View File

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

View File

@ -49,6 +49,9 @@ class ContextMenuAPI {
* a single sentence or short paragraph) of this kind of view
* @property {string} cssClass the CSS class to apply to labels for this
* 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
@ -72,12 +75,21 @@ class ContextMenuAPI {
/**
* @private
*/
_showContextMenuForObjectPath(objectPath, x, y) {
_showContextMenuForObjectPath(objectPath, x, y, actionsToBeIncluded) {
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) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -23,6 +23,7 @@
export default class GoToOriginalAction {
constructor(openmct) {
this.name = 'Go To Original';
this.key = 'goToOriginal';
this.description = 'Go to the original unlinked instance of this object';
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;
openmct.legacyRegistry.register('notebook', {
openmct.legacyRegistry.register('notebook-old', {
name: 'Notebook Plugin',
extensions: {
types: [
@ -73,10 +73,10 @@ define([
}
});
openmct.legacyRegistry.enable('notebook');
openmct.legacyRegistry.enable('notebook-old');
openmct.objectViews.addProvider({
key: 'notebook-vue',
key: 'notebook-old',
name: 'Notebook View',
cssClass: 'icon-notebook',
canView: function (domainObject) {

View File

@ -115,10 +115,22 @@
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') }}
</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">
<div ng-repeat="tick in ticks track by tick.text"
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.legend = this.config.legend;
this.$scope.yAxisLabel = this.config.yAxis.get('label');
this.cursorGuideVertical = this.$element[0].querySelector('.js-cursor-guide--v');
this.cursorGuideHorizontal = this.$element[0].querySelector('.js-cursor-guide--h');
this.cursorGuide = false;
@ -103,9 +105,35 @@ define([
this.listenTo(this.$scope, 'plot:tickWidth', this.onTickWidthChange, this);
this.listenTo(this.$scope, 'plot:highlight:set', this.onPlotHighlightSet, this);
this.listenTo(this.$scope, 'plot:reinitializeCanvas', this.initCanvas, this);
this.listenTo(this.config.xAxis, 'change:displayRange', this.onXAxisChange, 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) {
@ -493,5 +521,13 @@ define([
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;
});

View File

@ -41,6 +41,7 @@ define([
'./flexibleLayout/plugin',
'./tabs/plugin',
'./LADTable/plugin',
'./notebook-svc/plugin',
'./filters/plugin',
'./objectMigration/plugin',
'./goToOriginalAction/plugin',
@ -66,6 +67,7 @@ define([
FlexibleLayout,
Tabs,
LADTable,
NotebookSVC,
Filters,
ObjectMigration,
GoToOriginalAction,
@ -159,12 +161,13 @@ define([
plugins.SummaryWidget = SummaryWidget;
plugins.TelemetryMean = TelemetryMean;
plugins.URLIndicator = URLIndicatorPlugin;
plugins.Notebook = Notebook;
// plugins.Notebook = Notebook;
plugins.DisplayLayout = DisplayLayoutPlugin.default;
plugins.FolderView = FolderView;
plugins.Tabs = Tabs;
plugins.FlexibleLayout = FlexibleLayout;
plugins.LADTable = LADTable;
plugins.NotebookSVC = NotebookSVC;
plugins.Filters = Filters;
plugins.ObjectMigration = ObjectMigration.default;
plugins.GoToOriginalAction = GoToOriginalAction.default;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
<template>
<td>{{formattedValue}}</td>
<td @click="selectCell($event.currentTarget, columnKey)" :title="formattedValue">{{formattedValue}}</td>
</template>
<script>
export default {
@ -33,11 +33,38 @@ export default {
columnKey: {
type: String,
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: {
formattedValue() {
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.
*****************************************************************************/
<template>
<tr :style="{ top: rowTop }" :class="rowClass">
<component
v-for="(title, key) in headers"
<tr :style="{ top: rowTop }"
class="noselect"
:class="[
rowClass,
{'is-selected': marked}
]"
v-on="listeners">
<component v-for="(title, key) in headers"
:key="key"
:is="componentList[key]"
:columnKey="key"
:style="columnWidths[key] === undefined ? {} : { width: columnWidths[key] + 'px', 'max-width': columnWidths[key] + 'px'}"
:title="formattedRow[key]"
:class="cellLimitClasses[key]"
class="is-selectable"
@click="selectCell($event.currentTarget, key)"
:row="row"></component>
:class="[cellLimitClasses[key], selectableColumns[key] ? 'is-selectable' : '']"
:objectPath="objectPath"
:row="row">
</component>
</tr>
</template>
<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>
<script>
import TableCell from './table-cell.vue';
export default {
inject: ['openmct', 'objectPath'],
data: function () {
return {
rowTop: (this.rowOffset + this.rowIndex) * this.rowHeight + 'px',
formattedRow: this.row.getFormattedDatum(this.headers),
rowClass: this.row.getRowClass(),
cellLimitClasses: this.row.getCellLimitClasses(),
componentList: Object.keys(this.headers).reduce((components, header) => {
components[header] = this.row.getCellComponentName(header) || 'table-cell';
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,
required: true
},
objectPath: {
type: Array,
required: false
},
rowIndex: {
type: Number,
required: false,
@ -81,6 +102,11 @@ export default {
type: Number,
required: false,
default: 0
},
marked: {
type: Boolean,
required: false,
default: false
}
},
methods: {
@ -88,26 +114,54 @@ export default {
this.rowTop = (rowOffset + this.rowIndex) * this.rowHeight + 'px';
},
formatRow: function (row) {
this.formattedRow = row.getFormattedDatum(this.headers);
this.rowClass = row.getRowClass();
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) {
//TODO: This is a hack. Cannot get parent this way.
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.openmct.router.path[0]
}
}], false);
event.stopPropagation();
if (this.selectableColumns[columnKey]) {
//TODO: This is a hack. Cannot get parent this way.
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.openmct.router.path[0]
}
}], 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
@ -120,6 +174,19 @@ export default {
},
components: {
TableCell
},
computed: {
listeners() {
let listenersObject = {
click: this.markRow
}
if (this.row.getContextMenuActions().length) {
listenersObject.contextmenu = this.showContextMenu;
}
return listenersObject;
}
}
}
</script>

View File

@ -20,95 +20,135 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
<template>
<div class="c-table c-telemetry-table c-table--filterable c-table--sortable has-control-bar"
:class="{'loading': loading}">
<div :style="{ 'max-width': widthWithScroll, 'min-width': '150px'}"><slot></slot></div>
<div v-if="allowExport" class="c-table__control-bar c-control-bar">
<div class="c-table-wrapper">
<div class="c-table-control-bar c-control-bar">
<button class="c-button icon-download labeled"
v-on:click="exportAsCSV()"
title="Export This View's Data">
<span class="c-button__label">Export As CSV</span>
v-if="allowExport"
v-on:click="exportAllDataAsCSV()"
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>
<slot name="buttons"></slot>
</div>
<div v-if="isDropTargetActive" class="c-telemetry-table__drop-target" :style="dropTargetStyle"></div>
<!-- Headers table -->
<div class="c-telemetry-table__headers-w js-table__headers-w" ref="headersTable" :style="{ 'max-width': widthWithScroll}">
<table class="c-table__headers c-telemetry-table__headers">
<thead>
<tr class="c-telemetry-table__headers__labels">
<table-column-header
v-for="(title, key, headerIndex) in headers"
:key="key"
:headerKey="key"
:headerIndex="headerIndex"
@sort="allowSorting && sortBy(key)"
@resizeColumn="resizeColumn"
@dropTargetOffsetChanged="setDropTargetOffset"
@dropTargetActive="dropTargetActive"
@reorderColumn="reorderColumn"
@resizeColumnEnd="updateConfiguredColumnWidths"
:columnWidth="columnWidths[key]"
:sortOptions="sortOptions"
:isEditing="isEditing"
><span class="c-telemetry-table__headers__label">{{title}}</span>
</table-column-header>
</tr>
<tr class="c-telemetry-table__headers__filter">
<table-column-header
v-for="(title, key, headerIndex) in headers"
:key="key"
:headerKey="key"
:headerIndex="headerIndex"
@resizeColumn="resizeColumn"
@dropTargetOffsetChanged="setDropTargetOffset"
@dropTargetActive="dropTargetActive"
@reorderColumn="reorderColumn"
@resizeColumnEnd="updateConfiguredColumnWidths"
: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>
<div class="c-table c-telemetry-table c-table--filterable c-table--sortable has-control-bar"
:class="{
'loading': loading,
'paused' : paused
}">
<div :style="{ 'max-width': widthWithScroll, 'min-width': '150px'}"><slot></slot></div>
<div v-if="isDropTargetActive" class="c-telemetry-table__drop-target" :style="dropTargetStyle"></div>
<!-- Headers table -->
<div class="c-telemetry-table__headers-w js-table__headers-w" ref="headersTable" :style="{ 'max-width': widthWithScroll}">
<table class="c-table__headers c-telemetry-table__headers">
<thead>
<tr class="c-telemetry-table__headers__labels">
<table-column-header
v-for="(title, key, headerIndex) in headers"
:key="key"
:headerKey="key"
:headerIndex="headerIndex"
@sort="allowSorting && sortBy(key)"
@resizeColumn="resizeColumn"
@dropTargetOffsetChanged="setDropTargetOffset"
@dropTargetActive="dropTargetActive"
@reorderColumn="reorderColumn"
@resizeColumnEnd="updateConfiguredColumnWidths"
:columnWidth="columnWidths[key]"
:sortOptions="sortOptions"
:isEditing="isEditing"
><span class="c-telemetry-table__headers__label">{{title}}</span>
</table-column-header>
</tr>
<tr class="c-telemetry-table__headers__filter">
<table-column-header
v-for="(title, key, headerIndex) in headers"
:key="key"
:headerKey="key"
:headerIndex="headerIndex"
@resizeColumn="resizeColumn"
@dropTargetOffsetChanged="setDropTargetOffset"
@dropTargetActive="dropTargetActive"
@reorderColumn="reorderColumn"
@resizeColumnEnd="updateConfiguredColumnWidths"
: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>
<telemetry-filter-indicator></telemetry-filter-indicator>
</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"
: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>
</div><!-- closes c-table-wrapper -->
</template>
<style lang="scss">
@ -134,7 +174,7 @@
display: block;
flex: 1 0 auto;
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 {
@ -219,6 +259,10 @@
align-items: stretch;
position: absolute;
height: 18px; // Needed when a row has empty values in its cells
&.is-selected {
background-color: $colorSelectedBg;
}
}
td {
@ -269,6 +313,10 @@
}
}
.paused {
border: 1px solid #ff9900;
}
/******************************* LEGACY */
.s-status-taking-snapshot,
.overlay.snapshot {
@ -301,7 +349,7 @@ export default {
search,
TelemetryFilterIndicator
},
inject: ['table', 'openmct'],
inject: ['table', 'openmct', 'objectPath'],
props: {
isEditing: {
type: Boolean,
@ -318,6 +366,10 @@ export default {
allowSorting: {
'type': Boolean,
'default': true
},
enableMarking: {
type: Boolean,
default: false
}
},
data() {
@ -346,7 +398,10 @@ export default {
dropOffsetLeft: undefined,
isDropTargetActive: false,
isAutosizeEnabled: configuration.autosize,
scrollW: 0
scrollW: 0,
markCounter: 0,
paused: false,
markedRows: []
}
},
computed: {
@ -532,15 +587,27 @@ export default {
// which causes subsequent scroll to use an out of date height.
this.contentTable.style.height = this.totalHeight + 'px';
},
exportAsCSV() {
exportAsCSV(data) {
const headerKeys = Object.keys(this.headers);
const justTheData = this.table.filteredRows.getRows()
.map(row => row.getFormattedDatum(this.headers));
this.csvExporter.export(justTheData, {
this.csvExporter.export(data, {
filename: this.table.domainObject.name + '.csv',
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) {
this.loading = loading;
},
@ -632,8 +699,105 @@ export default {
clearRowsAndRerender() {
this.visibleRows = [];
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() {
this.filterChanged = _.debounce(this.filterChanged, 500);

View File

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

View File

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

View File

@ -211,6 +211,10 @@ $btnStdH: 24px;
$colorCursorGuide: rgba(white, 0.6);
$shdwCursorGuide: rgba(black, 0.4) 0 0 2px;
$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
$colorMenuBg: pullForward($colorBodyBg, 15%);
@ -425,7 +429,3 @@ $createBtnTextTransform: uppercase;
background: linear-gradient(pullForward($c, 5%), $c);
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);
$shdwCursorGuide: rgba(black, 0.4) 0 0 2px;
$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
$colorMenuBg: pullForward($colorBodyBg, 15%);
@ -430,10 +434,6 @@ $createBtnTextTransform: uppercase;
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 */
.c-frame {
&:not(.no-frame) {

View File

@ -211,6 +211,10 @@ $btnStdH: 24px;
$colorCursorGuide: rgba(black, 0.6);
$shdwCursorGuide: rgba(white, 0.4) 0 0 2px;
$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
$colorMenuBg: pushBack($colorBodyBg, 10%);
@ -424,7 +428,3 @@ $createBtnTextTransform: uppercase;
@mixin themedButton($c: $colorBtnBg) {
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
select {
@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-position: right .4em top 80%, 0 0;
border: none;
@ -600,15 +603,15 @@ select {
margin-right: $m;
}
.c-separator {
@include cToolbarSeparator();
}
.c-toolbar {
> * + * {
margin-left: 2px;
}
&__button {
}
&__separator {
@include cToolbarSeparator();
}

View File

@ -161,8 +161,7 @@ mct-plot {
height: auto;
}
&.gl-plot-y-label,
&.l-plot-y-label {
&.gl-plot-y-label {
$x: -50%;
$r: -90deg;
transform-origin: 50% 0;
@ -172,6 +171,12 @@ mct-plot {
left: 0;
top: 50%;
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) {
background: rgba($c, $a) !important;
background-color: rgba($c, $a) !important;

View File

@ -76,23 +76,43 @@ div.c-table {
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 {
// Can be used by any type of table, scrolling, LAD, etc.
$min-w: 50px;
width: 100%;
&__control-bar,
&__headers-w {
flex: 0 0 auto;
}
/******************************* ELEMENTS */
&__control-bar {
margin-bottom: $interiorMarginSm;
}
thead tr,
&.c-table__headers {
background: $colorTabHeaderBg;

View File

@ -44,7 +44,8 @@
class="c-so-view__object-view"
ref="objectView"
:object="domainObject"
:show-edit-view="showEditView">
:show-edit-view="showEditView"
:object-path="objectPath">
</object-view>
</div>
</template>
@ -123,7 +124,12 @@
.c-click-icon,
.c-button {
// 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>

View File

@ -111,7 +111,7 @@ export default {
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
// (eg. notabook.)
// (eg. notebook.)
event.dataTransfer.setData("openmct/domain-object-path", serializedPath);
event.dataTransfer.setData(`openmct/domain-object/${keyString}`, this.domainObject);
}

View File

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

View File

@ -19,28 +19,11 @@
</div>
<div class="l-browse-bar__end">
<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>
<view-switcher
:currentView="currentView"
:views="views"
@setView="setView">
</view-switcher>
<!-- Action buttons -->
<div class="l-browse-bar__actions">
<button v-if="notebookEnabled"
@ -77,14 +60,15 @@
<script>
import NotebookSnapshot from '../utils/notebook-snapshot';
import ViewSwitcher from './ViewSwitcher.vue';
const PLACEHOLDER_OBJECT = {};
export default {
inject: ['openmct'],
components: {
ViewSwitcher
},
methods: {
toggleViewMenu() {
this.showViewMenu = !this.showViewMenu;
},
toggleSaveMenu() {
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 class="l-browse-bar__end">
<div class="l-browse-bar__actions">
<view-switcher
:views="views"
:currentView="currentView"
@setView="setView">
</view-switcher>
<button v-if="notebookEnabled"
class="l-browse-bar__actions__edit c-button icon-notebook"
title="New Notebook entry"
@ -80,20 +85,52 @@
<script>
import ContextMenuDropDown from '../../ui/components/contextMenuDropDown.vue';
import ViewSwitcher from '../../ui/layout/ViewSwitcher.vue';
import NotebookSnapshot from '../utils/notebook-snapshot';
export default {
components: {
ContextMenuDropDown
ContextMenuDropDown,
ViewSwitcher
},
inject: [
'openmct',
'objectPath'
],
computed: {
views() {
return this
.openmct
.objectViews
.get(this.domainObject);
},
currentView() {
return this.views.filter(v => v.key === this.viewKey)[0] || {};
}
},
methods: {
snapshot() {
let element = document.getElementsByClassName("l-preview-window__object-view")[0];
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() {
@ -103,13 +140,13 @@
return {
domainObject: domainObject,
type: type,
notebookEnabled: false
notebookEnabled: false,
viewKey: undefined
};
},
mounted() {
let viewProvider = this.openmct.objectViews.get(this.domainObject)[0];
this.view = viewProvider.view(this.domainObject);
this.view.show(this.$refs.objectView, false);
let view = this.openmct.objectViews.get(this.domainObject)[0];
this.setView(view);
if (this.openmct.types.get('notebook')) {
this.notebookSnapshot = new NotebookSnapshot(this.openmct);

View File

@ -28,6 +28,7 @@ export default class PreviewAction {
* Metadata
*/
this.name = 'Preview';
this.key = 'preview';
this.description = 'Preview in large dialog';
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.
*****************************************************************************/
import PreviewAction from './PreviewAction.js';
import ViewHistoricalDataAction from './ViewHistoricalDataAction';
export default function () {
return function (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.
*
* When called by Open MCT, this may include additional arguments
* which are on the path to the object to be viewed; for instance,
* when viewing "A Folder" within "My Items", this method will be
* invoked with "A Folder" (as a domain object) as the first argument,
* and "My Items" as the second argument.
* When called by Open MCT, the following arguments will be passed to it:
* @param {object} domainObject - the domainObject that the view is provided for
* @param {boolean} isEditing - A boolean value indicating wether openmct is in a global edit mode
* @param {array} objectPath - The current contextual object path of the view object
* eg current domainObject is located under MyItems which is under Root
*
* @method view
* @memberof module:openmct.ViewProvider#

View File

@ -8,6 +8,7 @@ define([
let navigateCall = 0;
let browseObject;
let unobserve = undefined;
let currentObjectPath;
openmct.router.route(/^\/browse\/?$/, navigateToFirstChildOfRoot);
@ -26,7 +27,9 @@ define([
});
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.viewKey = viewProvider.key;
}

View File

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