Compare commits

..

10 Commits

Author SHA1 Message Date
charlesh88
371d132bfc Missing/unknown item should appear visually distinct
- Remove unneeded in-progress CSS;
2019-07-01 14:37:17 -07:00
charlesh88
91904ec22d Missing/unknown item should appear visually distinct
- Add missing/unknown styling to browse-bar;
2019-07-01 13:29:02 -07:00
charlesh88
afaee9a483 Missing/unknown item should appear visually distinct
- Apply missing/unknown styling to tabs view;
- Augment `isUnknown` mixin to also target same element with class
icon*;
2019-07-01 13:21:59 -07:00
charlesh88
709e1eb10e Missing/unknown item should appear visually distinct
- 'no-frame' CSS class application only allowed if domainObject.type is
not unknown;
- Show telemetry-view label element if domainObject.type is unknown;
2019-07-01 12:42:43 -07:00
charlesh88
6740677652 Missing/unknown item should appear visually distinct
- Styling applied to telemetry views for type unknown;
2019-06-28 09:42:51 -07:00
charlesh88
ee7521915f Missing/unknown item should appear visually distinct
- New "isUnknown" sass mixin to consolidate styling;
- Refined styling for grid, list-item and object label views;
- Flex and Display Layout frame headers now style properly for
missing/unknown items;
- TODO: visual indication when header is hidden (alphanumerics, summary
widgets, frame hidden manually, etc.)
2019-06-26 16:41:55 -07:00
charlesh88
bc590081b3 Missing/unknown item should appear visually distinct
- Styling for grid and list-item views;
- Refactor of item hover approach to use filters rather than discrete
colors;
2019-06-26 09:52:13 -07:00
charlesh88
854ffb9153 Missing/unknown item should appear visually distinct
- Theme color added;
- Styling for tree items;
- Removed unused tree item hover styling;
2019-06-26 06:12:07 -07:00
Charles Hacskaylo
e0587bf0e7 Status styling (#2422)
- Primarily needed by VISTA Data Products table UI;
- Adds new styling for inline links with icons;
- Adds new status colors in theme files;
2019-06-25 17:02:23 -07:00
Andrew Henry
f1494fd285 Vista table sync (#2423)
* Working version of integrated tables

* Fixed bug with multi-composition in tables

* Changes to support tables from VISTA
2019-06-25 13:56:39 -07:00
31 changed files with 408 additions and 533 deletions

View File

@@ -22,7 +22,7 @@
<template>
<layout-frame :item="item"
:grid-size="gridSize"
:title="domainObject && domainObject.name"
:title="domainObject && domainObject.name + ': ' + domainObject.type"
@move="(gridDelta) => $emit('move', gridDelta)"
@endMove="() => $emit('endMove')">
<object-frame v-if="domainObject"

View File

@@ -23,12 +23,14 @@
<template>
<layout-frame :item="item"
:grid-size="gridSize"
:title="domainObject && domainObject.name + ': ' + domainObject.type"
:class="{'c-telemetry-view--unknown': domainObject.type.indexOf('unknown') !== -1}"
@move="(gridDelta) => $emit('move', gridDelta)"
@endMove="() => $emit('endMove')">
<div class="c-telemetry-view"
:style="styleObject"
v-if="domainObject">
<div v-if="showLabel"
<div v-if="showLabel || domainObject.type.indexOf('unknown') !== -1"
class="c-telemetry-view__label">
<div class="c-telemetry-view__label-text">{{ domainObject.name }}</div>
</div>
@@ -50,6 +52,21 @@
display: flex;
align-items: stretch;
&--unknown {
.c-telemetry-view__label-text {
@include isUnknown();
display: flex;
align-items: center;
&:before {
content: $glyph-icon-object-unknown;
font-family: symbolsfont;
font-style: normal;
display: inline-block;
margin-right: $interiorMarginSm;
}
}
}
> * {
// Label and value holders
flex: 1 1 auto;

View File

@@ -1,6 +1,6 @@
<template>
<a class="l-grid-view__item c-grid-item"
:class="{ 'is-alias': item.isAlias === true }"
:class="{ 'is-alias': item.isAlias === true, 'c-grid-item--unknown': item.type.cssClass === undefined || item.type.cssClass.indexOf('unknown') !== -1 }"
:href="objectLink">
<div class="c-grid-item__type-icon"
:class="(item.type.cssClass != undefined) ? 'bg-' + item.type.cssClass : 'bg-icon-object-unknown'">
@@ -34,10 +34,13 @@
padding: $interiorMarginLg;
&__type-icon {
filter: $colorKeyFilter;
flex: 0 0 $gridItemMobile;
font-size: floor($gridItemMobile / 2);
margin-right: $interiorMarginLg;
&:before {
filter: $colorKeyFilter;
height: 100%;
}
}
&.is-alias {
@@ -48,6 +51,22 @@
}
}
&--unknown {
@include isUnknown();
/*[class*='__'] {
opacity: 0.7;
}
[class*='__name'],
[class*='__metadata'] {
font-style: italic;
}*/
[class*='__type-icon__glyph'] {
filter: $filterItemUnknown;
}
}
&__details {
display: flex;
flex-flow: column nowrap;
@@ -93,11 +112,10 @@
transition: background $transOutMs ease-in-out;
&:hover {
background: $colorItemBgHov;
transition: $transIn;
filter: $filterItemHoverFg;
//transition: $transIn;
.c-grid-item__type-icon {
filter: $colorKeyFilterHov;
transform: scale(1);
transition: $transInBounce;
}

View File

@@ -1,5 +1,5 @@
<template>
<div class="l-grid-view">
<div class="l-grid-view foo">
<grid-item v-for="(item, index) in items"
:key="index"
:item="item"
@@ -31,129 +31,6 @@
}
}
}
/******************************* GRID ITEMS */
.c-grid-item {
// Mobile-first
@include button($bg: $colorItemBg, $fg: $colorItemFg);
cursor: pointer;
display: flex;
padding: $interiorMarginLg;
&__type-icon {
filter: $colorKeyFilter;
flex: 0 0 $gridItemMobile;
font-size: floor($gridItemMobile / 2);
margin-right: $interiorMarginLg;
}
&.is-alias {
// Object is an alias to an original.
[class*='__type-icon'] {
@include isAlias();
color: $colorIconAliasForKeyFilter;
}
}
&__details {
display: flex;
flex-flow: column nowrap;
flex: 1 1 auto;
}
&__name {
@include ellipsize();
color: $colorItemFg;
font-size: 1.2em;
font-weight: 400;
margin-bottom: $interiorMarginSm;
}
&__metadata {
color: $colorItemFgDetails;
font-size: 0.9em;
body.mobile & {
[class*='__item-count'] {
&:before {
content: ' - ';
}
}
}
}
&__controls {
color: $colorItemFgDetails;
flex: 0 0 64px;
font-size: 1.2em;
display: flex;
align-items: center;
justify-content: flex-end;
> * + * {
margin-left: $interiorMargin;
}
}
body.desktop & {
$transOutMs: 300ms;
flex-flow: column nowrap;
transition: background $transOutMs ease-in-out;
&:hover {
background: $colorItemBgHov;
transition: $transIn;
.c-grid-item__type-icon {
filter: $colorKeyFilterHov;
transform: scale(1);
transition: $transInBounce;
}
}
> * {
margin: 0; // Reset from mobile
}
&__controls {
align-items: start;
flex: 0 0 auto;
order: 1;
.c-info-button,
.c-pointer-icon { display: none; }
}
&__type-icon {
flex: 1 1 auto;
font-size: floor($gridItemDesk / 3);
margin: $interiorMargin 22.5% $interiorMargin * 3 22.5%;
order: 2;
transform: scale(0.9);
transform-origin: center;
transition: all $transOutMs ease-in-out;
}
&__details {
flex: 0 0 auto;
justify-content: flex-end;
order: 3;
}
&__metadata {
display: flex;
&__type {
flex: 1 1 auto;
@include ellipsize();
}
&__item-count {
opacity: 0.7;
flex: 0 0 auto;
}
}
}
}
</style>
<script>

View File

@@ -1,6 +1,6 @@
<template>
<tr class="c-list-item"
:class="{ 'is-alias': item.isAlias === true }"
:class="{ 'is-alias': item.isAlias === true, 'c-list-item--unknown': item.type.cssClass === undefined || item.type.cssClass.indexOf('unknown') !== -1 }"
@click="navigate">
<td class="c-list-item__name">
<a :href="objectLink" ref="objectLink">
@@ -20,6 +20,7 @@
/******************************* LIST ITEM */
.c-list-item {
&__name a {
color: $colorItemFg;
display: flex;
> * + * { margin-left: $interiorMarginSm; }
@@ -53,6 +54,11 @@
}
}
}
&--unknown {
@include isUnknown();
filter: $filterItemUnknown;
}
}
</style>

View File

@@ -62,15 +62,16 @@
tbody tr {
background: $colorListItemBg;
transition: $transOut;
}
body.desktop & {
tbody tr {
transition: $transOut;
cursor: pointer;
&:hover {
background: $colorListItemBgHov;
background: $colorItemTreeHoverBg;
filter: $filterItemHoverFg;
transition: $transIn;
}
}

View File

@@ -16,7 +16,10 @@
v-for="(tab,index) in tabsList"
:key="index"
:class="[
{'is-current': isCurrent(tab)},
{
'is-current': isCurrent(tab),
'c-tab--unknown': tab.type.definition.cssClass.indexOf('unknown') !== -1
},
tab.type.definition.cssClass
]"
@click="showTab(tab)">
@@ -29,7 +32,12 @@
:class="{'c-tabs-view__object-holder--hidden': !isCurrent(tab)}">
<div v-if="currentTab"
class="c-tabs-view__object-name l-browse-bar__object-name--w"
:class="currentTab.type.definition.cssClass">
:class="[
{
'c-tabs-view__object--unknown': tab.type.definition.cssClass.indexOf('unknown') !== -1
},
currentTab.type.definition.cssClass
]">
<div class="l-browse-bar__object-name">
{{currentTab.domainObject.name}}
</div>
@@ -102,6 +110,13 @@
width: 100%;
}
}
.c-tab,
.c-tabs-view__object {
&--unknown {
@include isUnknown();
}
}
</style>
<script>

View File

@@ -26,6 +26,7 @@ define([
'./collections/BoundedTableRowCollection',
'./collections/FilteredTableRowCollection',
'./TelemetryTableRow',
'./TelemetryTableColumn',
'./TelemetryTableConfiguration'
], function (
EventEmitter,
@@ -33,6 +34,7 @@ define([
BoundedTableRowCollection,
FilteredTableRowCollection,
TelemetryTableRow,
TelemetryTableColumn,
TelemetryTableConfiguration
) {
class TelemetryTable extends EventEmitter {
@@ -45,10 +47,8 @@ define([
this.subscriptions = {};
this.tableComposition = undefined;
this.telemetryObjects = [];
this.rowBuffer = [];
this.outstandingRequests = 0;
this.configuration = new TelemetryTableConfiguration(domainObject, openmct);
this.paused = false;
this.addTelemetryObject = this.addTelemetryObject.bind(this);
this.removeTelemetryObject = this.removeTelemetryObject.bind(this);
@@ -96,8 +96,6 @@ define([
this.tableComposition.load().then((composition) => {
composition = composition.filter(this.isTelemetryObject);
this.configuration.addColumnsForAllObjects(composition);
composition.forEach(this.addTelemetryObject);
this.tableComposition.on('add', this.addTelemetryObject);
@@ -107,7 +105,7 @@ define([
}
addTelemetryObject(telemetryObject) {
this.configuration.addColumnsForObject(telemetryObject, true);
this.addColumnsForObject(telemetryObject, true);
this.requestDataFor(telemetryObject);
this.subscribeTo(telemetryObject);
this.telemetryObjects.push(telemetryObject);
@@ -134,6 +132,13 @@ define([
this.emit('object-removed', objectIdentifier);
}
/**
* @private
*/
createRow(datum, columnMap, keyString, limitEvaluator) {
return new TelemetryTableRow(datum, columnMap, keyString, limitEvaluator);
}
requestDataFor(telemetryObject) {
this.incrementOutstandingRequests();
let requestOptions = this.buildOptionsFromConfiguration(telemetryObject);
@@ -147,7 +152,7 @@ define([
let columnMap = this.getColumnMapForObject(keyString);
let limitEvaluator = this.openmct.telemetry.limitEvaluator(telemetryObject);
let telemetryRows = telemetryData.map(datum => new TelemetryTableRow(datum, columnMap, keyString, limitEvaluator));
let telemetryRows = telemetryData.map(datum => this.createRow(datum, columnMap, keyString, limitEvaluator));
this.boundedRows.add(telemetryRows);
}).finally(() => {
this.decrementOutstandingRequests();
@@ -193,6 +198,19 @@ define([
}, {});
}
addColumnsForObject(telemetryObject) {
let metadataValues = this.openmct.telemetry.getMetadata(telemetryObject).values();
metadataValues.forEach(metadatum => {
let column = this.createColumn(metadatum);
this.configuration.addSingleColumnForObject(telemetryObject, column);
});
}
createColumn(metadatum) {
return new TelemetryTableColumn(this.openmct, metadatum);
}
subscribeTo(telemetryObject) {
let subscribeOptions = this.buildOptionsFromConfiguration(telemetryObject);
let keyString = this.openmct.objects.makeKeyString(telemetryObject.identifier);
@@ -204,11 +222,7 @@ define([
if (!this.telemetryObjects.includes(telemetryObject)) {
return;
}
if (!this.paused) {
let row = new TelemetryTableRow(datum, columnMap, keyString, limitEvaluator);
this.boundedRows.add(row);
}
this.boundedRows.add(this.createRow(datum, columnMap, keyString, limitEvaluator));
}, subscribeOptions);
}
@@ -240,17 +254,6 @@ 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

@@ -22,9 +22,8 @@
define([
'lodash',
'EventEmitter',
'./TelemetryTableColumn'
], function (_, EventEmitter, TelemetryTableColumn) {
'EventEmitter'
], function (_, EventEmitter) {
class TelemetryTableConfiguration extends EventEmitter {
constructor(domainObject, openmct) {
@@ -34,7 +33,6 @@ define([
this.openmct = openmct;
this.columns = {};
this.addColumnsForObject = this.addColumnsForObject.bind(this);
this.removeColumnsForObject = this.removeColumnsForObject.bind(this);
this.objectMutated = this.objectMutated.bind(this);
//Make copy of configuration, otherwise change detection is impossible if shared instance is being modified.
@@ -48,6 +46,7 @@ define([
configuration.hiddenColumns = configuration.hiddenColumns || {};
configuration.columnWidths = configuration.columnWidths || {};
configuration.columnOrder = configuration.columnOrder || [];
configuration.cellFormat = configuration.cellFormat || {};
configuration.autosize = configuration.autosize === undefined ? true : configuration.autosize;
return configuration;
@@ -65,26 +64,18 @@ define([
//Synchronize domain object reference. Duplicate object otherwise change detection becomes impossible.
this.domainObject = object;
//Was it the configuration that changed?
if (!_.eq(object.configuration, this.oldConfiguration)) {
if (object.configuration !== undefined && !_.eq(object.configuration, this.oldConfiguration)) {
//Make copy of configuration, otherwise change detection is impossible if shared instance is being modified.
this.oldConfiguration = JSON.parse(JSON.stringify(this.getConfiguration()));
this.emit('change', object.configuration);
}
}
addColumnsForAllObjects(objects) {
objects.forEach(object => this.addColumnsForObject(object, false));
}
addColumnsForObject(telemetryObject) {
let metadataValues = this.openmct.telemetry.getMetadata(telemetryObject).values();
addSingleColumnForObject(telemetryObject, column, position) {
let objectKeyString = this.openmct.objects.makeKeyString(telemetryObject.identifier);
this.columns[objectKeyString] = [];
metadataValues.forEach(metadatum => {
let column = new TelemetryTableColumn(this.openmct, metadatum);
this.columns[objectKeyString].push(column);
});
this.columns[objectKeyString] = this.columns[objectKeyString] || [];
position = position || this.columns[objectKeyString].length;
this.columns[objectKeyString].splice(position, 0, column);
}
removeColumnsForObject(objectIdentifier) {

View File

@@ -42,12 +42,19 @@ define([], function () {
return column && column.getFormattedValue(this.datum[key]);
}
getRowLimitClass() {
if (!this.rowLimitClass) {
getCellComponentName(key) {
let column = this.columns[key];
return column &&
column.getCellComponentName &&
column.getCellComponentName();
}
getRowClass() {
if (!this.rowClass) {
let limitEvaluation = this.limitEvaluator.evaluate(this.datum);
this.rowLimitClass = limitEvaluation && limitEvaluation.cssClass;
this.rowClass = limitEvaluation && limitEvaluation.cssClass;
}
return this.rowLimitClass;
return this.rowClass;
}
getCellLimitClasses() {

View File

@@ -22,12 +22,10 @@
define([
'./components/table.vue',
'../../exporters/CSVExporter',
'./TelemetryTable',
'vue'
], function (
TableComponent,
CSVExporter,
TelemetryTable,
Vue
) {
@@ -51,7 +49,6 @@ define([
return domainObject.type === 'table';
},
view(domainObject) {
let csvExporter = new CSVExporter.default();
let table = new TelemetryTable(domainObject, openmct);
let component;
return {
@@ -67,7 +64,6 @@ define([
},
provide: {
openmct,
csvExporter,
table
},
el: element,

View File

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

View File

@@ -0,0 +1,44 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
<template>
<td>{{formattedValue}}</td>
</template>
<script>
export default {
inject: ['openmct'],
props: {
row: {
type: Object,
required: true
},
columnKey: {
type: String,
require: true
}
},
computed: {
formattedValue() {
return this.row.getFormattedValue(this.columnKey);
}
}
};
</script>

View File

@@ -23,6 +23,8 @@
</style>
<script>
import TelemetryTableColumn from '../TelemetryTableColumn';
export default {
inject: ['tableConfiguration', 'openmct'],
data() {
@@ -43,7 +45,7 @@ export default {
this.tableConfiguration.updateConfiguration(this.configuration);
},
addObject(domainObject) {
this.tableConfiguration.addColumnsForObject(domainObject, true);
this.addColumnsForObject(domainObject, true);
this.updateHeaders(this.tableConfiguration.getAllHeaders());
},
removeObject(objectIdentifier) {
@@ -56,6 +58,17 @@ export default {
toggleAutosize() {
this.configuration.autosize = !this.configuration.autosize;
this.tableConfiguration.updateConfiguration(this.configuration);
},
addColumnsForAllObjects(objects) {
objects.forEach(object => this.addColumnsForObject(object, false));
},
addColumnsForObject(telemetryObject) {
let metadataValues = this.openmct.telemetry.getMetadata(telemetryObject).values();
metadataValues.forEach(metadatum => {
let column = new TelemetryTableColumn(this.openmct, metadatum);
this.tableConfiguration.addSingleColumnForObject(telemetryObject, column);
});
}
},
mounted() {
@@ -65,7 +78,7 @@ export default {
compositionCollection.load()
.then((composition) => {
this.tableConfiguration.addColumnsForAllObjects(composition);
this.addColumnsForAllObjects(composition);
this.updateHeaders(this.tableConfiguration.getAllHeaders());
compositionCollection.on('add', this.addObject);

View File

@@ -20,41 +20,38 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
<template>
<tr :style="{ top: rowTop }"
class="noselect"
:class="[
rowLimitClass,
{'is-selected': marked}
]"
@click="markRow">
<td v-for="(title, key) in headers"
<tr :style="{ top: rowTop }" :class="rowClass">
<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]">{{formattedRow[key]}}</td>
:class="cellLimitClasses[key]"
class="is-selectable"
@click="selectCell($event.currentTarget, key)"
: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 {
data: function () {
return {
rowTop: (this.rowOffset + this.rowIndex) * this.rowHeight + 'px',
formattedRow: this.row.getFormattedDatum(this.headers),
rowLimitClass: this.row.getRowLimitClass(),
cellLimitClasses: this.row.getCellLimitClasses()
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
}, {})
}
},
props: {
@@ -84,11 +81,6 @@ export default {
type: Number,
required: false,
default: 0
},
marked: {
type: Boolean,
required: false,
default: false
}
},
methods: {
@@ -97,19 +89,25 @@ export default {
},
formatRow: function (row) {
this.formattedRow = row.getFormattedDatum(this.headers);
this.rowLimitClass = row.getRowLimitClass();
this.rowClass = row.getRowClass();
this.cellLimitClasses = row.getCellLimitClasses();
},
markRow: function (event) {
if (event.shiftKey) {
this.$emit('markMultipleConcurrent', this.rowIndex);
} else {
if (this.marked) {
this.$emit('unmark', this.rowIndex);
} else {
this.$emit('mark', this.rowIndex);
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();
}
},
// TODO: use computed properties
@@ -119,6 +117,9 @@ export default {
handler: 'formatRow',
deep: false
}
},
components: {
TableCell
}
}
</script>

View File

@@ -20,111 +20,81 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
<template>
<div class="c-table-wrapper">
<div class="c-table-control-bar c-control-bar">
<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">
<button class="c-button icon-download labeled"
v-on:click="exportAllDataAsCSV()"
v-on:click="exportAsCSV()"
title="Export This View's Data">
<span class="c-button__label">Export Table Data</span>
</button>
<button class="c-button icon-download labeled"
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 class="c-separator"></div>
<button 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>
<span class="c-button__label">Export As CSV</span>
</button>
</div>
<div class="c-table c-telemetry-table c-table--filterable c-table--sortable has-control-bar"
:class="{
'loading': loading,
'paused' : paused
}">
<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="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"
:key="rowIndex"
:headers="headers"
:columnWidths="columnWidths"
:rowIndex="rowIndex"
: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">
<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"
: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>
@@ -136,8 +106,7 @@
:row="sizingRowData">
</telemetry-table-row>
</table>
</div>
</div><!-- closes c-table-wrapper -->
</div>
</template>
<style lang="scss">
@@ -248,10 +217,6 @@
align-items: stretch;
position: absolute;
height: 18px; // Needed when a row has empty values in its cells
&.is-selected {
background-color: $colorSelectedBg;
}
}
td {
@@ -302,10 +267,6 @@
}
}
.paused {
border: 1px solid #ff9900;
}
/******************************* LEGACY */
.s-status-taking-snapshot,
.overlay.snapshot {
@@ -319,6 +280,7 @@
import TelemetryTableRow from './table-row.vue';
import search from '../../../ui/components/search.vue';
import TableColumnHeader from './table-column-header.vue';
import CSVExporter from '../../../exporters/CSVExporter.js';
import _ from 'lodash';
const VISIBLE_ROW_COUNT = 100;
@@ -335,11 +297,23 @@ export default {
TableColumnHeader,
search
},
inject: ['table', 'openmct', 'csvExporter'],
inject: ['table', 'openmct'],
props: {
isEditing: {
type: Boolean,
default: false
},
allowExport: {
type: Boolean,
default: true
},
allowFiltering: {
'type': Boolean,
'default': true
},
allowSorting: {
'type': Boolean,
'default': true
}
},
data() {
@@ -368,10 +342,7 @@ export default {
dropOffsetLeft: undefined,
isDropTargetActive: false,
isAutosizeEnabled: configuration.autosize,
scrollW: 0,
markCounter: 0,
paused: false,
markedRows: []
scrollW: 0
}
},
computed: {
@@ -557,27 +528,15 @@ export default {
// which causes subsequent scroll to use an out of date height.
this.contentTable.style.height = this.totalHeight + 'px';
},
exportAsCSV(data) {
exportAsCSV() {
const headerKeys = Object.keys(this.headers);
this.csvExporter.export(data, {
const justTheData = this.table.filteredRows.getRows()
.map(row => row.getFormattedDatum(this.headers));
this.csvExporter.export(justTheData, {
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;
},
@@ -666,115 +625,17 @@ export default {
scrollTop = this.scrollable.scrollTop;
}, RESIZE_POLL_INTERVAL);
},
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) {
let insertMethod = 'unshift';
if (this.markedRows.length && !this.ctrlKeyPressed) {
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.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);
}
this.$forceUpdate();
}
},
keydown(event) {
if ((event.ctrlKey || event.metaKey)) {
this.ctrlKeyPressed = true;
}
},
keyup(event) {
if ((event.ctrlKey || event.key.toLowerCase() === 'meta')) {
console.log('up');
this.ctrlKeyPressed = false;
}
clearRowsAndRerender() {
this.visibleRows = [];
this.$nextTick().then(this.updateVisibleRows);
}
},
created() {
this.filterChanged = _.debounce(this.filterChanged, 500);
},
mounted() {
this.csvExporter = new CSVExporter();
this.rowsAdded = _.throttle(this.rowsAdded, 200);
this.rowsRemoved = _.throttle(this.rowsRemoved, 200);
this.scroll = _.throttle(this.scroll, 100);
@@ -782,6 +643,7 @@ export default {
this.table.on('object-added', this.addObject);
this.table.on('object-removed', this.removeObject);
this.table.on('outstanding-requests', this.outstandingRequests);
this.table.on('refresh', this.clearRowsAndRerender);
this.table.filteredRows.on('add', this.rowsAdded);
this.table.filteredRows.on('remove', this.rowsRemoved);
@@ -802,14 +664,12 @@ export default {
this.calculateScrollbarWidth();
this.table.initialize();
document.addEventListener('keydown', this.keydown);
document.addEventListener('keyup', this.keyup);
},
destroyed() {
this.table.off('object-added', this.addObject);
this.table.off('object-removed', this.removeObject);
this.table.off('outstanding-requests', this.outstandingRequests);
this.table.off('refresh', this.clearRowsAndRerender);
this.table.filteredRows.off('add', this.rowsAdded);
this.table.filteredRows.off('remove', this.rowsRemoved);
@@ -823,9 +683,6 @@ export default {
this.table.configuration.destroy();
this.table.destroy();
document.removeEventListener('keydown', this.keydown);
document.removeEventListener('keyup', this.keyup);
}
}
</script>

View File

@@ -1,6 +0,0 @@
<tr :style="{ top: rowTop }" :class="rowLimitClass">
<td v-for="(title, key, headerIndex) in headers"
:style="{ width: columnWidths[headerIndex], 'max-width': columnWidths[headerIndex]}"
:title="formattedRow[key]"
:class="cellLimitClasses[key]">{{formattedRow[key]}}</td>
</tr>

View File

@@ -108,6 +108,10 @@
a {
color: $colorAboutLink;
&:hover {
color: $colorAHov;
}
}
em {

View File

@@ -101,6 +101,8 @@ $colorStatusAlertFilter: invert(78%) sepia(26%) saturate(1160%) hue-rotate(324de
$colorStatusError: #da0004;
$colorStatusErrorFilter: invert(10%) sepia(96%) saturate(4360%) hue-rotate(351deg) brightness(111%) contrast(115%);
$colorStatusBtnBg: #666; // Where is this used?
$colorStatusPartialBg: #3f5e8b;
$colorStatusCompleteBg: #457638;
$colorAlert: #ff3c00;
$colorAlertFg: #fff;
$colorWarningHi: #990000;
@@ -140,6 +142,8 @@ $browseFrameColor: pullForward($colorBodyBg, 10%);
$browseFrameBorder: 1px solid $browseFrameColor; // Frames in Disp and Flex Layouts when frame is showing
$browseSelectableShdwHov: rgba($colorBodyFg, 0.3) 0 0 3px;
$browseSelectedBorder: 1px solid rgba($colorBodyFg, 0.4);
$filterItemHoverFg: brightness(1.2);
$filterItemUnknown: contrast(0);
/************************************************** EDITING */
$editUIColor: $uiColor; // Base color

View File

@@ -105,6 +105,8 @@ $colorStatusAlertFilter: invert(78%) sepia(26%) saturate(1160%) hue-rotate(324de
$colorStatusError: #da0004;
$colorStatusErrorFilter: invert(10%) sepia(96%) saturate(4360%) hue-rotate(351deg) brightness(111%) contrast(115%);
$colorStatusBtnBg: #666; // Where is this used?
$colorStatusPartialBg: #3f5e8b;
$colorStatusCompleteBg: #457638;
$colorAlert: #ff3c00;
$colorAlertFg: #fff;
$colorWarningHi: #990000;
@@ -144,6 +146,8 @@ $browseFrameColor: pullForward($colorBodyBg, 10%);
$browseFrameBorder: 1px solid $browseFrameColor; // Frames in Disp and Flex Layouts when frame is showing
$browseSelectableShdwHov: rgba($colorBodyFg, 0.3) 0 0 3px;
$browseSelectedBorder: 1px solid rgba($colorBodyFg, 0.4);
$filterItemHoverFg: brightness(1.2);
$filterItemUnknown: contrast(0);
/************************************************** EDITING */
$editUIColor: $uiColor; // Base color

View File

@@ -101,6 +101,8 @@ $colorStatusAlertFilter: invert(89%) sepia(26%) saturate(5035%) hue-rotate(316de
$colorStatusError: #da0004;
$colorStatusErrorFilter: invert(8%) sepia(96%) saturate(4511%) hue-rotate(352deg) brightness(136%) contrast(114%);
$colorStatusBtnBg: #666; // Where is this used?
$colorStatusPartialBg: #c9d6ff;
$colorStatusCompleteBg: #a4e4b4;
$colorAlert: #ff3c00;
$colorAlertFg: #fff;
$colorWarningHi: #990000;
@@ -140,6 +142,8 @@ $browseFrameColor: pullForward($colorBodyBg, 10%);
$browseFrameBorder: 1px solid $browseFrameColor; // Frames in Disp and Flex Layouts when frame is showing
$browseSelectableShdwHov: rgba($colorBodyFg, 0.3) 0 0 3px;
$browseSelectedBorder: 1px solid rgba($colorBodyFg, 0.4);
$filterItemHoverFg: brightness(0.9);
$filterItemUnknown: contrast(0);
/************************************************** EDITING */
$editUIColor: $uiColor; // Base color

View File

@@ -60,11 +60,17 @@ button {
}
}
/********* Icon Buttons */
/********* Icon Buttons and Links */
.c-click-icon {
@include cClickIcon();
}
.c-click-link {
// A clickable element, typically inline, with an icon and label
@include cControl();
cursor: pointer;
}
.c-icon-button,
.c-click-swatch {
@include cClickIconButton();
@@ -585,15 +591,15 @@ select {
margin-right: $m;
}
.c-separator {
@include cToolbarSeparator();
}
.c-toolbar {
> * + * {
margin-left: 2px;
}
&__button {
}
&__separator {
@include cToolbarSeparator();
}

View File

@@ -79,9 +79,6 @@ a {
color: $colorA;
cursor: pointer;
text-decoration: none;
&:hover {
color: $colorAHov;
}
}
body, html {

View File

@@ -36,10 +36,14 @@
}
@mixin glyphBg($glyphUrl) {
background-image: $glyphUrl;
background-position: center;
background-size: contain;
background-repeat: no-repeat;
&:before {
content: '';
display: block;
background-image: $glyphUrl;
background-position: center;
background-size: contain;
background-repeat: no-repeat;
}
}
[class*="icon-"] {
@@ -97,6 +101,24 @@
}
}
@mixin isUnknown() {
// Common styles to be applied to tree items, object labels, grid and list item views
font-style: italic;
opacity: 0.7;
&[class*='icon']:before,
&[class*='icon']:after,
[class*='icon']:before,
[class*='icon']:after {
font-style: normal; // Prevent symbolsfont element from being italicized;
}
[class*='icon']:before {
// Target :before to avoid affecting alias indicator
filter: $filterItemUnknown;
}
}
@mixin bgDiagonalStripes($c: yellow, $a: 0.1, $d: 40px) {
background-image: linear-gradient(-45deg,
rgba($c, $a) 25%, transparent 25%,

View File

@@ -159,3 +159,16 @@ tr {
@include indicatorStatusColors($colorStatusError);
}
}
.s-status {
&--partial {
// Partially completed things, such as a file downloading or process that's running
background-color: $colorStatusPartialBg;
}
&--complete {
// Completed things, such as a file downloaded or process that's finished
background-color: $colorStatusCompleteBg;
}
}

View File

@@ -76,43 +76,23 @@ 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

@@ -22,11 +22,13 @@
<template>
<div class="c-so-view has-local-controls"
:class="{
'c-so-view--no-frame': !hasFrame,
'c-so-view--no-frame': !hasFrame && cssClass.indexOf('unknown') === -1,
'has-complex-content': complexContent
}">
<div class="c-so-view__header">
<div class="c-so-view__header__icon" :class="cssClass"></div>
<div class="c-so-view__header"
:class="{'c-so-view__header--unknown': cssClass.indexOf('unknown') !== -1}">
<div class="c-so-view__header__icon"
:class="cssClass"></div>
<div class="c-so-view__header__name">
{{ domainObject && domainObject.name }}
</div>
@@ -74,6 +76,10 @@
@include ellipsize();
flex: 0 1 auto;
}
&--unknown {
@include isUnknown();
}
}
&:not(.c-so-view--no-frame) {
@@ -123,12 +129,7 @@
.c-click-icon,
.c-button {
// Shrink buttons a bit when they appear in a frame
align-items: baseline;
font-size: 0.85em;
padding: 3px 5px;
&:before {
font-size: 0.8em;
}
font-size: 0.8em;
}
}
</style>

View File

@@ -1,5 +1,6 @@
<template>
<a class="c-tree__item__label c-object-label"
:class="{'c-object-label--unknown': typeClass.indexOf('icon-object-unknown') !== -1}"
draggable="true"
@dragstart="dragStart"
@click="navigateOrPreview"
@@ -39,6 +40,10 @@
color: $colorItemTreeIcon;
width: $treeTypeIconW;
}
&--unknown {
@include isUnknown();
}
}
</style>

View File

@@ -5,7 +5,12 @@
class="l-browse-bar__nav-to-parent-button c-icon-button c-icon-button--major icon-pointer-left"
@click="goToParent"></button>
<div class="l-browse-bar__object-name--w"
:class="type.cssClass">
:class="[
{
'l-browse-bar--unknown': type.cssClass.indexOf('unknown') !== -1
},
type.cssClass
]">
<span
class="l-browse-bar__object-name c-input-inline"
@blur="updateName"
@@ -316,5 +321,9 @@ const PLACEHOLDER_OBJECT = {};
&__object-name {
flex: 0 1 auto;
}
&--unknown {
@include isUnknown();
}
}
</style>

View File

@@ -83,13 +83,7 @@
&:hover {
background: $colorItemTreeHoverBg;
.c-tree__item__type-icon:before {
color: $colorItemTreeIconHover;
}
.c-tree__item__name {
color: $colorItemTreeHoverFg;
}
> * { filter: $filterItemHoverFg; }
}
&.is-navigated-object,

View File

@@ -24,6 +24,7 @@ const webpackConfig = {
output: {
filename: '[name].js',
library: '[name]',
libraryTarget: 'umd',
path: path.resolve(__dirname, 'dist')
},
resolve: {