Compare commits

...

12 Commits

Author SHA1 Message Date
d5028eed98 set this.left on resize (#3049)
code refactoring
2020-05-20 12:41:58 -07:00
480e327c63 [Time Conductor] add history and select range features to vista-r4.4.1 (#3015)
* linting

* linting

* more linting

* linting

* change realtime end bound to 30 secondes

* add max duration validation

* Tweaks to Time Conductor History menu

- Enhanced styles for `.c-menu`;
- Added hint messaging and separator;
- Reversed displayed history array so that latest entry is always first;

* refactor to use browser mouse events instead of d3brush

* move zoom/pan styling up to conductor

* WIP almost there

* fix zoom

* move altPressed up to parent

* handle no drag on pan

* rename inMode vars for clarity

* Styling for Time Conductor zoom and pan

- Minor fix to hover cursor for alt-pressed panning;

* add configurable bounds limit to time conductor

* add presets and records

* fixes for history

* remove lodash

* add default configurables for examples

* do not install local time system

* cleanup

* fix merge conflict

* fix merge conflict

* add styles from missing picks

* styling fixes

* local time system is utc based

* fix indentation

remove logging

* remove comments

* section-hint without section-separator styling

* provide reasonable defaults for conductor configuration

* specify input to check validation on

* improve validation

* first check both inputs for valid formats

* clear each valid input on new entry

* tear down listeners

* add user instructions

* allow preset bounds to be declared as callback function

Co-authored-by: charlesh88 <charlesh88@gmail.com>
2020-05-13 14:54:09 -07:00
e8b10f0193 Bug fixes for plots (#3019)
* prevent plots from breaking when more than one NaN is received. fix y axis label issue

* fix lint error

* dont emit a viewport change event when marquee doesnt happen
2020-05-13 14:27:50 -07:00
1d64dc4653 Regex search 4.4.1 (#3010)
* working prototype ready for styling

* Styling for table search regex button

- Hover display/hide and styling for regex button;

* Styling for table search regex button

- Allow user input to be retained when toggling regex on/off;

* change title and clear out input on toggle regex

* remove unused variable

* Styling for table search regex button

- Polish size of button;

* use  to trigger dom update when clearing input

* fixes regression when search string is modified

Co-authored-by: charlesh88 <charlesh88@gmail.com>
2020-05-08 11:09:10 -07:00
08c1053a66 Tabs 4.4.1 Lazy Load (#3009)
* remove listener on destroy

* lazy load tabs

* fix lint error

* Store current tab position on domainObject

* remove lodash dependency and use keystring
2020-05-08 10:37:42 -07:00
7508062aac improve plot gestures - and clean up (#3011) 2020-05-06 10:33:31 -07:00
2f58dfbfe7 lock moment version 2020-05-05 10:45:03 -07:00
166cb55945 [Conditionals] Handling for empty requests in vista-r4.4-rc6 (#2983)
* test empty request

* handle non-valid requests
2020-04-29 14:53:34 -07:00
83116257fc [Conditionals] evaluation fixes in vista-r4.4 (#2980)
* change metadata output to state and value

* do not send telemetryObjects to telemetry api request cal

* normalize data on requests
2020-04-29 10:05:19 -07:00
775f1048bc Show the Styles tab for non creatable layout objects including condition sets (#2976) 2020-04-28 13:10:07 -07:00
71a8b377bb [Conditionals] addendum to #2967 (#2972)
* use correct id for telemetry requests

* request and subscription data cache should be mutually exclusive

use latest timestamp for any/all requests

* do not add id prop

remove unnecessary if check
2020-04-28 13:01:30 -07:00
c43d3fcfc9 [Conditionals] fix #2961 in vista-r4.4 (#2967)
* use correct id for telemetry requests

* request and subscription data cache should be mutually exclusive

use latest timestamp for any/all requests
2020-04-24 12:55:33 -07:00
29 changed files with 939 additions and 202 deletions

View File

@ -34,8 +34,8 @@
<body>
</body>
<script>
const FIVE_MINUTES = 5 * 60 * 1000;
const THIRTY_MINUTES = 30 * 60 * 1000;
const THIRTY_SECONDS = 30 * 1000;
const THIRTY_MINUTES = THIRTY_SECONDS * 60;
[
'example/eventGenerator'
@ -63,7 +63,39 @@
bounds: {
start: Date.now() - THIRTY_MINUTES,
end: Date.now()
}
},
// commonly used bounds can be stored in history
// bounds (start and end) can accept either a milliseconds number
// or a callback function returning a milliseconds number
// a function is useful for invoking Date.now() at exact moment of preset selection
presets: [
{
label: 'Last Day',
bounds: {
start: () => Date.now() - 1000 * 60 * 60 * 24,
end: () => Date.now()
}
},
{
label: 'Last 2 hours',
bounds: {
start: () => Date.now() - 1000 * 60 * 60 * 2,
end: () => Date.now()
}
},
{
label: 'Last hour',
bounds: {
start: () => Date.now() - 1000 * 60 * 60,
end: () => Date.now()
}
}
],
// maximum recent bounds to retain in conductor history
records: 10,
// maximum duration between start and end bounds
// for utc-based time systems this is in milliseconds
limit: 1000 * 60 * 60 * 24
},
{
name: "Realtime",
@ -71,7 +103,7 @@
clock: 'local',
clockOffsets: {
start: - THIRTY_MINUTES,
end: FIVE_MINUTES
end: THIRTY_SECONDS
}
}
]

View File

@ -53,9 +53,9 @@
"marked": "^0.3.5",
"mini-css-extract-plugin": "^0.4.1",
"minimist": "^1.1.1",
"moment": "^2.11.1",
"moment": "2.25.3",
"moment-duration-format": "^2.2.2",
"moment-timezone": "^0.5.21",
"moment-timezone": "0.5.28",
"node-bourbon": "^4.2.3",
"node-sass": "^4.9.2",
"painterro": "^0.2.65",

View File

@ -204,7 +204,7 @@ export default class ConditionClass extends EventEmitter {
let latestTimestamp;
let criteriaResults = {};
const criteriaRequests = this.criteria
.map(criterion => criterion.requestLAD({telemetryObjects: this.conditionManager.telemetryObjects}));
.map(criterion => criterion.requestLAD(this.conditionManager.telemetryObjects));
return Promise.all(criteriaRequests)
.then(results => {

View File

@ -55,7 +55,7 @@ export default class ConditionManager extends EventEmitter {
this.telemetryObjects[id] = Object.assign({}, endpoint, {telemetryMetaData: this.openmct.telemetry.getMetadata(endpoint).valueMetadatas});
this.subscriptions[id] = this.openmct.telemetry.subscribe(
endpoint,
this.telemetryReceived.bind(this, id)
this.telemetryReceived.bind(this, endpoint)
);
this.updateConditionTelemetry();
}
@ -258,9 +258,13 @@ export default class ConditionManager extends EventEmitter {
this.openmct.time.timeSystem()
);
});
const currentCondition = this.getCurrentConditionLAD(conditionResults);
return Object.assign(
if (!Object.values(latestTimestamp).some(timeSystem => timeSystem)) {
return [];
}
const currentCondition = this.getCurrentConditionLAD(conditionResults);
const currentOutput = Object.assign(
{
output: currentCondition.configuration.output,
id: this.conditionSetDomainObject.identifier,
@ -268,11 +272,15 @@ export default class ConditionManager extends EventEmitter {
},
latestTimestamp
);
return [currentOutput];
});
});
}
isTelemetryUsed(id) {
isTelemetryUsed(endpoint) {
const id = this.openmct.objects.makeKeyString(endpoint.identifier);
for(const condition of this.conditionClassCollection) {
if (condition.isTelemetryUsed(id)) {
return true;
@ -282,12 +290,12 @@ export default class ConditionManager extends EventEmitter {
return false;
}
telemetryReceived(id, datum) {
if (!this.isTelemetryUsed(id)) {
telemetryReceived(endpoint, datum) {
if (!this.isTelemetryUsed(endpoint)) {
return;
}
const normalizedDatum = this.createNormalizedDatum(datum, id);
const normalizedDatum = this.createNormalizedDatum(datum, endpoint);
const timeSystemKey = this.openmct.time.timeSystem().key;
let timestamp = {};
timestamp[timeSystemKey] = normalizedDatum[timeSystemKey];
@ -321,8 +329,11 @@ export default class ConditionManager extends EventEmitter {
return data;
}
createNormalizedDatum(telemetryDatum, id) {
const normalizedDatum = Object.values(this.telemetryObjects[id].telemetryMetaData).reduce((datum, metadatum) => {
createNormalizedDatum(telemetryDatum, endpoint) {
const id = this.openmct.objects.makeKeyString(endpoint.identifier);
const metadata = this.openmct.telemetry.getMetadata(endpoint).valueMetadatas;
const normalizedDatum = Object.values(metadata).reduce((datum, metadatum) => {
const testValue = this.getTestData(metadatum);
const formatter = this.openmct.telemetry.getValueFormatter(metadatum);
datum[metadatum.key] = testValue !== undefined ? formatter.parse(testValue) : formatter.parse(telemetryDatum[metadatum.source]);

View File

@ -54,13 +54,22 @@ export default class ConditionSetMetadataProvider {
return {
values: this.getDomains().concat([
{
name: 'Output',
key: 'output',
format: 'enum',
key: "state",
source: "output",
name: "State",
format: "enum",
enumerations: enumerations,
hints: {
range: 1
}
},
{
key: "output",
name: "Value",
format: "string",
hints: {
range: 2
}
}
])
};

View File

@ -45,7 +45,7 @@ export default class ConditionSetTelemetryProvider {
return conditionManager.requestLADConditionSetOutput()
.then(latestOutput => {
return latestOutput ? [latestOutput] : [];
return latestOutput;
});
}

View File

@ -22,6 +22,7 @@
import TelemetryCriterion from './TelemetryCriterion';
import { evaluateResults } from "../utils/evaluator";
import { getLatestTimestamp } from '../utils/time';
export default class AllTelemetryCriterion extends TelemetryCriterion {
@ -107,40 +108,53 @@ export default class AllTelemetryCriterion extends TelemetryCriterion {
this.result = evaluateResults(Object.values(this.telemetryDataCache), this.telemetry);
}
requestLAD(options) {
options = Object.assign({},
options,
{
strategy: 'latest',
size: 1
}
);
requestLAD(telemetryObjects) {
const options = {
strategy: 'latest',
size: 1
};
if (!this.isValid()) {
return this.formatData({}, options.telemetryObjects);
return this.formatData({}, telemetryObjects);
}
let keys = Object.keys(Object.assign({}, options.telemetryObjects));
let keys = Object.keys(Object.assign({}, telemetryObjects));
const telemetryRequests = keys
.map(key => this.openmct.telemetry.request(
options.telemetryObjects[key],
telemetryObjects[key],
options
));
let telemetryDataCache = {};
return Promise.all(telemetryRequests)
.then(telemetryRequestsResults => {
let latestDatum;
let latestTimestamp;
const timeSystems = this.openmct.time.getAllTimeSystems();
const timeSystem = this.openmct.time.timeSystem();
telemetryRequestsResults.forEach((results, index) => {
latestDatum = results.length ? results[results.length - 1] : {};
if (index < telemetryRequestsResults.length-1) {
if (latestDatum) {
this.telemetryDataCache[latestDatum.id] = this.computeResult(latestDatum);
}
}
const latestDatum = results.length ? results[results.length - 1] : {};
const datumId = keys[index];
const normalizedDatum = this.createNormalizedDatum(latestDatum, telemetryObjects[datumId]);
telemetryDataCache[datumId] = this.computeResult(normalizedDatum);
latestTimestamp = getLatestTimestamp(
latestTimestamp,
normalizedDatum,
timeSystems,
timeSystem
);
});
const datum = {
result: evaluateResults(Object.values(telemetryDataCache), this.telemetry),
...latestTimestamp
};
return {
id: this.id,
data: this.formatData(latestDatum, options.telemetryObjects)
data: datum
};
});
}

View File

@ -61,6 +61,21 @@ export default class TelemetryCriterion extends EventEmitter {
this.telemetryObject = telemetryObjects[this.telemetryObjectIdAsString];
}
createNormalizedDatum(telemetryDatum, endpoint) {
const id = this.openmct.objects.makeKeyString(endpoint.identifier);
const metadata = this.openmct.telemetry.getMetadata(endpoint).valueMetadatas;
const normalizedDatum = Object.values(metadata).reduce((datum, metadatum) => {
const formatter = this.openmct.telemetry.getValueFormatter(metadatum);
datum[metadatum.key] = formatter.parse(telemetryDatum[metadatum.source]);
return datum;
}, {});
normalizedDatum.id = id;
return normalizedDatum;
}
formatData(data) {
const datum = {
result: this.computeResult(data)
@ -79,14 +94,11 @@ export default class TelemetryCriterion extends EventEmitter {
this.result = this.computeResult(validatedData);
}
requestLAD(options) {
options = Object.assign({},
options,
{
strategy: 'latest',
size: 1
}
);
requestLAD() {
const options = {
strategy: 'latest',
size: 1
};
if (!this.isValid()) {
return {
@ -100,9 +112,11 @@ export default class TelemetryCriterion extends EventEmitter {
options
).then(results => {
const latestDatum = results.length ? results[results.length - 1] : {};
const normalizedDatum = this.createNormalizedDatum(latestDatum, this.telemetryObject);
return {
id: this.id,
data: this.formatData(latestDatum)
data: this.formatData(normalizedDatum)
};
});
}

View File

@ -41,7 +41,7 @@ define([], function () {
this.timeFormat = 'local-format';
this.durationFormat = 'duration';
this.isUTCBased = false;
this.isUTCBased = true;
}
return LocalTimeSystem;

View File

@ -322,7 +322,15 @@ define([
* a point to the end without dupe checking.
*/
add: function (point, appendOnly) {
var insertIndex = this.data.length;
var insertIndex = this.data.length,
currentYVal = this.getYVal(point),
lastYVal = this.getYVal(this.data[insertIndex - 1]);
if (this.isValueInvalid(currentYVal) && this.isValueInvalid(lastYVal)) {
console.warn('[Plot] Invalid Y Values detected');
return;
}
if (!appendOnly) {
insertIndex = this.sortedIndex(point);
if (this.getXVal(this.data[insertIndex]) === this.getXVal(point)) {
@ -332,11 +340,21 @@ define([
return;
}
}
this.updateStats(point);
point.mctLimitState = this.evaluate(point);
this.data.splice(insertIndex, 0, point);
this.emit('add', point, insertIndex, this);
},
/**
*
* @private
*/
isValueInvalid: function (val) {
return Number.isNaN(val) || val === undefined;
},
/**
* Remove a point from the data array and notify listeners.
* @private

View File

@ -182,21 +182,6 @@ define([
this.set('format', yFormat.format.bind(yFormat));
this.set('values', yMetadata.values);
if (!label) {
var labelUnits = series.map(function (s) {
return s.metadata.value(s.get('yKey')).units;
}).reduce(function (a, b) {
if (a === undefined) {
return b;
}
if (a === b) {
return a;
}
return '';
}, undefined);
if (labelUnits) {
this.set('label', labelUnits);
return;
}
var labelName = series.map(function (s) {
return s.metadata.value(s.get('yKey')).name;
}).reduce(function (a, b) {
@ -208,7 +193,28 @@ define([
}
return '';
}, undefined);
this.set('label', labelName);
if (labelName) {
this.set('label', labelName);
return;
}
var labelUnits = series.map(function (s) {
return s.metadata.value(s.get('yKey')).units;
}).reduce(function (a, b) {
if (a === undefined) {
return b;
}
if (a === b) {
return a;
}
return '';
}, undefined);
if (labelUnits) {
this.set('label', labelUnits);
return;
}
}
},
defaults: function (options) {

View File

@ -65,10 +65,10 @@ define([
}
this.$canvas = this.$element.find('canvas');
this.listenTo(this.$canvas, 'click', this.onMouseClick, this);
this.listenTo(this.$canvas, 'mousemove', this.trackMousePosition, this);
this.listenTo(this.$canvas, 'mouseleave', this.untrackMousePosition, this);
this.listenTo(this.$canvas, 'mousedown', this.onMouseDown, this);
this.listenTo(this.$canvas, 'wheel', this.wheelZoom, this);
this.watchForMarquee();
};
@ -76,7 +76,6 @@ define([
MCTPlotController.prototype.initialize = function () {
this.$canvas = this.$element.find('canvas');
this.listenTo(this.$canvas, 'click', this.onMouseClick, this);
this.listenTo(this.$canvas, 'mousemove', this.trackMousePosition, this);
this.listenTo(this.$canvas, 'mouseleave', this.untrackMousePosition, this);
this.listenTo(this.$canvas, 'mousedown', this.onMouseDown, this);
@ -208,23 +207,6 @@ define([
this.highlightValues(point);
};
MCTPlotController.prototype.onMouseClick = function ($event) {
const isClick = this.isMouseClick();
if (this.pan) {
this.endPan($event);
}
if (this.marquee) {
this.endMarquee($event);
}
this.$scope.$apply();
if (!this.$scope.highlights.length || !isClick) {
return;
}
this.$scope.lockHighlightPoint = !this.$scope.lockHighlightPoint;
};
MCTPlotController.prototype.highlightValues = function (point) {
this.highlightPoint = point;
this.$scope.$emit('plot:highlight:update', point);
@ -272,11 +254,23 @@ define([
MCTPlotController.prototype.onMouseUp = function ($event) {
this.stopListening(this.$window, 'mouseup', this.onMouseUp, this);
this.stopListening(this.$window, 'mousemove', this.trackMousePosition, this);
if (this.isMouseClick()) {
this.$scope.lockHighlightPoint = !this.$scope.lockHighlightPoint;
}
if (this.allowPan) {
return this.endPan($event);
}
if (this.allowMarquee) {
return this.endMarquee($event);
}
};
MCTPlotController.prototype.isMouseClick = function () {
if (!this.marquee) {
return;
return false;
}
const { start, end } = this.marquee;
@ -329,7 +323,7 @@ define([
} else {
// A history entry is created by startMarquee, need to remove
// if marquee zoom doesn't occur.
this.back();
this.plotHistory.pop();
}
this.$scope.rectangles = [];
this.marquee = undefined;

View File

@ -27,7 +27,7 @@
{'is-current': isCurrent(tab)},
tab.type.definition.cssClass
]"
@click="showTab(tab)"
@click="showTab(tab, index)"
>
<span class="c-button__label">{{ tab.domainObject.name }}</span>
</button>
@ -48,6 +48,7 @@
</div>
</div>
<object-view
v-if="internalDomainObject.keep_alive ? currentTab : isCurrent(tab)"
class="c-tabs-view__object"
:object="tab.domainObject"
/>
@ -57,7 +58,6 @@
<script>
import ObjectView from '../../../ui/components/ObjectView.vue';
import _ from 'lodash';
var unknownObjectType = {
definition: {
@ -73,6 +73,7 @@ export default {
},
data: function () {
return {
internalDomainObject: this.domainObject,
currentTab: {},
tabsList: [],
setCurrentTab: true,
@ -85,9 +86,17 @@ export default {
this.composition.on('add', this.addItem);
this.composition.on('remove', this.removeItem);
this.composition.on('reorder', this.onReorder);
this.composition.load();
this.composition.load().then(() => {
let currentTabIndex = this.domainObject.currentTabIndex;
if (currentTabIndex !== undefined && this.tabsList.length > currentTabIndex) {
this.currentTab = this.tabsList[currentTabIndex];
}
});
}
this.unsubscribe = this.openmct.objects.observe(this.internalDomainObject, '*', this.updateInternalDomainObject);
document.addEventListener('dragstart', this.dragstart);
document.addEventListener('dragend', this.dragend);
},
@ -96,18 +105,25 @@ export default {
this.composition.off('remove', this.removeItem);
this.composition.off('reorder', this.onReorder);
this.unsubscribe();
document.removeEventListener('dragstart', this.dragstart);
document.removeEventListener('dragend', this.dragend);
},
methods:{
showTab(tab) {
showTab(tab, index) {
if (index !== undefined) {
this.storeCurrentTabIndex(index);
}
this.currentTab = tab;
},
addItem(domainObject) {
let type = this.openmct.types.get(domainObject.type) || unknownObjectType,
tabItem = {
domainObject,
type: type
type: type,
key: this.openmct.objects.makeKeyString(domainObject.identifier)
};
this.tabsList.push(tabItem);
@ -126,7 +142,7 @@ export default {
this.tabsList.splice(pos, 1);
if (this.isCurrent(tabToBeRemoved)) {
this.showTab(this.tabsList[this.tabsList.length - 1]);
this.showTab(this.tabsList[this.tabsList.length - 1], this.tabsList.length - 1);
}
},
onReorder(reorderPlan) {
@ -138,6 +154,7 @@ export default {
},
onDrop(e) {
this.setCurrentTab = true;
this.storeCurrentTabIndex(this.tabsList.length);
},
dragstart(e) {
if (e.dataTransfer.types.includes('openmct/domain-object-path')) {
@ -155,7 +172,13 @@ export default {
this.allowDrop = false;
},
isCurrent(tab) {
return _.isEqual(this.currentTab, tab)
return this.currentTab.key === tab.key;
},
updateInternalDomainObject(domainObject) {
this.internalDomainObject = domainObject;
},
storeCurrentTabIndex(index) {
this.openmct.objects.mutate(this.internalDomainObject, 'currentTabIndex', index);
}
}
}

View File

@ -36,7 +36,27 @@ define([
cssClass: 'icon-tabs-view',
initialize(domainObject) {
domainObject.composition = [];
}
domainObject.keep_alive = true;
},
form: [
{
"key": "keep_alive",
"name": "Keep Tabs Alive",
"control": "select",
"options": [
{
'name': 'True',
'value': true
},
{
'name': 'False',
'value': false
}
],
"required": true,
"cssClass": "l-input"
}
]
});
};
};

View File

@ -46,11 +46,23 @@ define(
filter = filter.trim().toLowerCase();
let rowsToFilter = this.getRowsToFilter(columnKey, filter);
if (filter.length === 0) {
delete this.columnFilters[columnKey];
} else {
this.columnFilters[columnKey] = filter;
}
this.rows = rowsToFilter.filter(this.matchesFilters, this);
this.emit('filter');
}
setColumnRegexFilter(columnKey, filter) {
filter = filter.trim();
let rowsToFilter = this.masterCollection.getRows();
this.columnFilters[columnKey] = new RegExp(filter);
this.rows = rowsToFilter.filter(this.matchesFilters, this);
this.emit('filter');
}
@ -70,6 +82,10 @@ define(
* @private
*/
isSubsetOfCurrentFilter(columnKey, filter) {
if (this.columnFilters[columnKey] instanceof RegExp) {
return false;
}
return this.columnFilters[columnKey] &&
filter.startsWith(this.columnFilters[columnKey]) &&
// startsWith check will otherwise fail when filter cleared
@ -96,7 +112,11 @@ define(
return false;
}
doesMatchFilters = formattedValue.toLowerCase().indexOf(this.columnFilters[key]) !== -1;
if (this.columnFilters[key] instanceof RegExp) {
doesMatchFilters = this.columnFilters[key].test(formattedValue);
} else {
doesMatchFilters = formattedValue.toLowerCase().indexOf(this.columnFilters[key]) !== -1;
}
});
return doesMatchFilters;
}

View File

@ -186,7 +186,17 @@
class="c-table__search"
@input="filterChanged(key)"
@clear="clearFilter(key)"
/>
>
<button
class="c-search__use-regex"
:class="{ 'is-active': enableRegexSearch[key] }"
title="Click to enable regex: enter a string with slashes, like this: /regex_exp/"
@click="toggleRegex(key)"
>
/R/
</button>
</search>
</table-column-header>
</tr>
</thead>
@ -336,7 +346,8 @@ export default {
markCounter: 0,
paused: false,
markedRows: [],
isShowingMarkedRowsOnly: false
isShowingMarkedRowsOnly: false,
enableRegexSearch: {}
}
},
computed: {
@ -543,7 +554,16 @@ export default {
this.headersHolderEl.scrollLeft = this.scrollable.scrollLeft;
},
filterChanged(columnKey) {
this.table.filteredRows.setColumnFilter(columnKey, this.filters[columnKey]);
if (this.enableRegexSearch[columnKey]) {
if (this.isCompleteRegex(this.filters[columnKey])) {
this.table.filteredRows.setColumnRegexFilter(columnKey, this.filters[columnKey].slice(1,-1));
} else {
return;
}
} else {
this.table.filteredRows.setColumnFilter(columnKey, this.filters[columnKey]);
}
this.setHeight();
},
clearFilter(columnKey) {
@ -869,6 +889,18 @@ export default {
this.isAutosizeEnabled = true;
this.$nextTick().then(this.calculateColumnWidths);
},
toggleRegex(key) {
this.$set(this.filters, key, '');
if (this.enableRegexSearch[key] === undefined) {
this.$set(this.enableRegexSearch, key, true)
} else {
this.$set(this.enableRegexSearch, key, !this.enableRegexSearch[key]);
}
},
isCompleteRegex(string) {
return (string.length > 2 && string[0] === '/' && string[string.length - 1] === '/')
}
}
}

View File

@ -22,7 +22,12 @@
<template>
<div
class="c-conductor"
:class="[isFixed ? 'is-fixed-mode' : 'is-realtime-mode']"
:class="[
{ 'is-zooming': isZooming },
{ 'is-panning': isPanning },
{ 'alt-pressed': altPressed },
isFixed ? 'is-fixed-mode' : 'is-realtime-mode'
]"
>
<form
ref="conductorForm"
@ -52,7 +57,7 @@
type="text"
autocorrect="off"
spellcheck="false"
@change="validateAllBounds(); submitForm()"
@change="validateAllBounds('startDate'); submitForm()"
>
<date-picker
v-if="isFixed && isUTCBased"
@ -92,7 +97,7 @@
autocorrect="off"
spellcheck="false"
:disabled="!isFixed"
@change="validateAllBounds(); submitForm()"
@change="validateAllBounds('endDate'); submitForm()"
>
<date-picker
v-if="isFixed && isUTCBased"
@ -122,14 +127,25 @@
<conductor-axis
class="c-conductor__ticks"
:bounds="rawBounds"
@panAxis="setViewFromBounds"
:view-bounds="viewBounds"
:is-fixed="isFixed"
:alt-pressed="altPressed"
@endPan="endPan"
@endZoom="endZoom"
@panAxis="pan"
@zoomAxis="zoom"
/>
</div>
<div class="c-conductor__controls">
<!-- Mode, time system menu buttons and duration slider -->
<ConductorMode class="c-conductor__mode-select" />
<ConductorTimeSystem class="c-conductor__time-system-select" />
<ConductorHistory
v-if="isFixed"
class="c-conductor__history-select"
:bounds="openmct.time.bounds()"
:time-system="timeSystem"
/>
</div>
<input
type="submit"
@ -145,6 +161,7 @@ import ConductorTimeSystem from './ConductorTimeSystem.vue';
import DatePicker from './DatePicker.vue';
import ConductorAxis from './ConductorAxis.vue';
import ConductorModeIcon from './ConductorModeIcon.vue';
import ConductorHistory from './ConductorHistory.vue'
const DEFAULT_DURATION_FORMATTER = 'duration';
@ -155,7 +172,8 @@ export default {
ConductorTimeSystem,
DatePicker,
ConductorAxis,
ConductorModeIcon
ConductorModeIcon,
ConductorHistory
},
data() {
let bounds = this.openmct.time.bounds();
@ -165,6 +183,7 @@ export default {
let durationFormatter = this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
return {
timeSystem: timeSystem,
timeFormatter: timeFormatter,
durationFormatter: durationFormatter,
offsets: {
@ -175,29 +194,68 @@ export default {
start: timeFormatter.format(bounds.start),
end: timeFormatter.format(bounds.end)
},
rawBounds: {
viewBounds: {
start: bounds.start,
end: bounds.end
},
isFixed: this.openmct.time.clock() === undefined,
isUTCBased: timeSystem.isUTCBased,
showDatePicker: false
showDatePicker: false,
altPressed: false,
isPanning: false,
isZooming: false
}
},
mounted() {
document.addEventListener('keydown', this.handleKeyDown);
document.addEventListener('keyup', this.handleKeyUp);
this.setTimeSystem(JSON.parse(JSON.stringify(this.openmct.time.timeSystem())));
this.openmct.time.on('bounds', this.setViewFromBounds);
this.openmct.time.on('timeSystem', this.setTimeSystem);
this.openmct.time.on('clock', this.setViewFromClock);
this.openmct.time.on('clockOffsets', this.setViewFromOffsets)
},
beforeDestroy() {
document.removeEventListener('keydown', this.handleKeyDown);
document.removeEventListener('keyup', this.handleKeyUp);
},
methods: {
handleKeyDown(event) {
if (event.key === 'Alt') {
this.altPressed = true;
}
},
handleKeyUp(event) {
if (event.key === 'Alt') {
this.altPressed = false;
}
},
pan(bounds) {
this.isPanning = true;
this.setViewFromBounds(bounds);
},
endPan(bounds) {
this.isPanning = false;
if (bounds) {
this.openmct.time.bounds(bounds);
}
},
zoom(bounds) {
this.isZooming = true;
this.formattedBounds.start = this.timeFormatter.format(bounds.start);
this.formattedBounds.end = this.timeFormatter.format(bounds.end);
},
endZoom(bounds) {
const _bounds = bounds ? bounds : this.openmct.time.bounds();
this.isZooming = false;
this.openmct.time.bounds(_bounds);
},
setTimeSystem(timeSystem) {
this.timeSystem = timeSystem
this.timeFormatter = this.getFormatter(timeSystem.timeFormat);
this.durationFormatter = this.getFormatter(
timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
this.isUTCBased = timeSystem.isUTCBased;
},
setOffsetsFromView($event) {
@ -237,8 +295,8 @@ export default {
setViewFromBounds(bounds) {
this.formattedBounds.start = this.timeFormatter.format(bounds.start);
this.formattedBounds.end = this.timeFormatter.format(bounds.end);
this.rawBounds.start = bounds.start;
this.rawBounds.end = bounds.end;
this.viewBounds.start = bounds.start;
this.viewBounds.end = bounds.end;
},
setViewFromOffsets(offsets) {
this.offsets.start = this.durationFormatter.format(Math.abs(offsets.start));
@ -251,6 +309,15 @@ export default {
this.setOffsetsFromView();
}
},
getBoundsLimit() {
const configuration = this.configuration.menuOptions
.filter(option => option.timeSystem === this.timeSystem.key)
.find(option => option.limit);
const limit = configuration ? configuration.limit : undefined;
return limit;
},
clearAllValidation() {
if (this.isFixed) {
[this.$refs.startDate, this.$refs.endDate].forEach(this.clearValidationForInput);
@ -262,36 +329,52 @@ export default {
input.setCustomValidity('');
input.title = '';
},
validateAllBounds() {
return [this.$refs.startDate, this.$refs.endDate].every((input) => {
let validationResult = true;
let formattedDate;
validateAllBounds(ref) {
if (!this.areBoundsFormatsValid()) {
return false;
}
if (input === this.$refs.startDate) {
formattedDate = this.formattedBounds.start;
let validationResult = true;
const currentInput = this.$refs[ref];
return [this.$refs.startDate, this.$refs.endDate].every((input) => {
let boundsValues = {
start: this.timeFormatter.parse(this.formattedBounds.start),
end: this.timeFormatter.parse(this.formattedBounds.end)
};
const limit = this.getBoundsLimit();
if (
this.timeSystem.isUTCBased
&& limit
&& boundsValues.end - boundsValues.start > limit
) {
if (input === currentInput) {
validationResult = "Start and end difference exceeds allowable limit";
}
} else {
formattedDate = this.formattedBounds.end;
if (input === currentInput) {
validationResult = this.openmct.time.validateBounds(boundsValues);
}
}
return this.handleValidationResults(input, validationResult);
});
},
areBoundsFormatsValid() {
let validationResult = true;
return [this.$refs.startDate, this.$refs.endDate].every((input) => {
const formattedDate = input === this.$refs.startDate
? this.formattedBounds.start
: this.formattedBounds.end
;
if (!this.timeFormatter.validate(formattedDate)) {
validationResult = 'Invalid date';
} else {
let boundsValues = {
start: this.timeFormatter.parse(this.formattedBounds.start),
end: this.timeFormatter.parse(this.formattedBounds.end)
};
validationResult = this.openmct.time.validateBounds(boundsValues);
}
if (validationResult !== true) {
input.setCustomValidity(validationResult);
input.title = validationResult;
return false;
} else {
input.setCustomValidity('');
input.title = '';
return true;
}
return this.handleValidationResults(input, validationResult);
});
},
validateAllOffsets(event) {
@ -315,17 +398,20 @@ export default {
validationResult = this.openmct.time.validateOffsets(offsetValues);
}
if (validationResult !== true) {
input.setCustomValidity(validationResult);
input.title = validationResult;
return false;
} else {
input.setCustomValidity('');
input.title = '';
return true;
}
return this.handleValidationResults(input, validationResult);
});
},
handleValidationResults(input, validationResult) {
if (validationResult !== true) {
input.setCustomValidity(validationResult);
input.title = validationResult;
return false;
} else {
input.setCustomValidity('');
input.title = '';
return true;
}
},
submitForm() {
// Allow Vue model to catch up to user input.
// Submitting form will cause validation messages to display (but only if triggered by button click)
@ -338,12 +424,12 @@ export default {
},
startDateSelected(date) {
this.formattedBounds.start = this.timeFormatter.format(date);
this.validateAllBounds();
this.validateAllBounds('startDate');
this.submitForm();
},
endDateSelected(date) {
this.formattedBounds.end = this.timeFormatter.format(date);
this.validateAllBounds();
this.validateAllBounds('endDate');
this.submitForm();
}
}

View File

@ -24,7 +24,12 @@
ref="axisHolder"
class="c-conductor-axis"
@mousedown="dragStart($event)"
></div>
>
<div
class="c-conductor-axis__zoom-indicator"
:style="zoomStyle"
></div>
</div>
</template>
<script>
@ -43,52 +48,81 @@ const PIXELS_PER_TICK_WIDE = 200;
export default {
inject: ['openmct'],
props: {
bounds: {
viewBounds: {
type: Object,
required: true
},
isFixed: {
type: Boolean,
required: true
},
altPressed: {
type: Boolean,
required: true
}
},
data() {
return {
inPanMode: false,
dragStartX: undefined,
dragX: undefined,
zoomStyle: {}
}
},
computed: {
inZoomMode() {
return !this.inPanMode;
}
},
watch: {
bounds: {
handler(bounds) {
viewBounds: {
handler() {
this.setScale();
},
deep: true
}
},
mounted() {
let axisHolder = this.$refs.axisHolder;
let height = axisHolder.offsetHeight;
let vis = d3Selection.select(axisHolder)
.append("svg:svg")
.attr("width", "100%")
.attr("height", height);
let vis = d3Selection.select(this.$refs.axisHolder).append("svg:svg");
this.width = this.$refs.axisHolder.clientWidth;
this.xAxis = d3Axis.axisTop();
this.dragging = false;
// draw x axis with labels. CSS is used to position them.
this.axisElement = vis.append("g");
this.axisElement = vis.append("g")
.attr("class", "axis");
this.setViewFromTimeSystem(this.openmct.time.timeSystem());
this.setAxisDimensions();
this.setScale();
//Respond to changes in conductor
this.openmct.time.on("timeSystem", this.setViewFromTimeSystem);
setInterval(this.resize, RESIZE_POLL_INTERVAL);
},
destroyed() {
},
methods: {
setAxisDimensions() {
const axisHolder = this.$refs.axisHolder;
const rect = axisHolder.getBoundingClientRect();
this.left = Math.round(rect.left);
this.width = axisHolder.clientWidth;
},
setScale() {
if (!this.width) {
return;
}
let timeSystem = this.openmct.time.timeSystem();
let bounds = this.bounds;
if (timeSystem.isUTCBased) {
this.xScale.domain([new Date(bounds.start), new Date(bounds.end)]);
this.xScale.domain(
[new Date(this.viewBounds.start), new Date(this.viewBounds.end)]
);
} else {
this.xScale.domain([bounds.start, bounds.end]);
this.xScale.domain(
[this.viewBounds.start, this.viewBounds.end]
);
}
this.xAxis.scale(this.xScale);
@ -102,7 +136,7 @@ export default {
this.xAxis.ticks(this.width / PIXELS_PER_TICK);
}
this.msPerPixel = (bounds.end - bounds.start) / this.width;
this.msPerPixel = (this.viewBounds.end - this.viewBounds.start) / this.width;
},
setViewFromTimeSystem(timeSystem) {
//The D3 scale used depends on the type of time system as d3
@ -120,9 +154,8 @@ export default {
},
getActiveFormatter() {
let timeSystem = this.openmct.time.timeSystem();
let isFixed = this.openmct.time.clock() === undefined;
if (isFixed) {
if (this.isFixed) {
return this.getFormatter(timeSystem.timeFormat);
} else {
return this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
@ -134,45 +167,131 @@ export default {
}).formatter;
},
dragStart($event) {
let isFixed = this.openmct.time.clock() === undefined;
if (isFixed) {
if (this.isFixed) {
this.dragStartX = $event.clientX;
if (this.altPressed) {
this.inPanMode = true;
}
document.addEventListener('mousemove', this.drag);
document.addEventListener('mouseup', this.dragEnd, {
once: true
});
if (this.inZoomMode) {
this.startZoom();
}
}
},
drag($event) {
if (!this.dragging) {
this.dragging = true;
requestAnimationFrame(()=>{
let deltaX = $event.clientX - this.dragStartX;
let percX = deltaX / this.width;
let bounds = this.openmct.time.bounds();
let deltaTime = bounds.end - bounds.start;
let newStart = bounds.start - percX * deltaTime;
this.$emit('panAxis',{
start: newStart,
end: newStart + deltaTime
});
requestAnimationFrame(() => {
this.dragX = $event.clientX;
this.inPanMode ? this.pan() : this.zoom();
this.dragging = false;
})
} else {
console.log('Rejected drag due to RAF cap');
});
}
},
dragEnd() {
this.inPanMode ? this.endPan() : this.endZoom();
document.removeEventListener('mousemove', this.drag);
this.openmct.time.bounds({
start: this.bounds.start,
end: this.bounds.end
this.dragStartX = undefined;
this.dragX = undefined;
},
pan() {
const panBounds = this.getPanBounds();
this.$emit('panAxis', panBounds);
},
endPan() {
const panBounds = this.dragStartX && this.dragX && this.dragStartX !== this.dragX
? this.getPanBounds()
: undefined;
this.$emit('endPan', panBounds);
this.inPanMode = false;
},
getPanBounds() {
const bounds = this.openmct.time.bounds();
const deltaTime = bounds.end - bounds.start;
const deltaX = this.dragX - this.dragStartX;
const percX = deltaX / this.width;
const panStart = bounds.start - percX * deltaTime;
return {
start: panStart,
end: panStart + deltaTime
};
},
startZoom() {
const x = this.scaleToBounds(this.dragStartX);
this.zoomStyle = {
left: `${this.dragStartX - this.left}px`
};
this.$emit('zoomAxis', {
start: x,
end: x
});
},
zoom() {
const zoomRange = this.getZoomRange();
this.zoomStyle = {
left: `${zoomRange.start - this.left}px`,
width: `${zoomRange.end - zoomRange.start}px`
};
this.$emit('zoomAxis', {
start: this.scaleToBounds(zoomRange.start),
end: this.scaleToBounds(zoomRange.end)
});
},
endZoom() {
const zoomRange = this.dragStartX && this.dragX && this.dragStartX !== this.dragX
? this.getZoomRange()
: undefined;
const zoomBounds = zoomRange
? {
start: this.scaleToBounds(zoomRange.start),
end: this.scaleToBounds(zoomRange.end)
}
: this.openmct.time.bounds();
this.zoomStyle = {};
this.$emit('endZoom', zoomBounds);
},
getZoomRange() {
const leftBound = this.left;
const rightBound = this.left + this.width;
const zoomStart = this.dragX < leftBound
? leftBound
: Math.min(this.dragX, this.dragStartX);
const zoomEnd = this.dragX > rightBound
? rightBound
: Math.max(this.dragX, this.dragStartX);
return {
start: zoomStart,
end: zoomEnd
};
},
scaleToBounds(value) {
const bounds = this.openmct.time.bounds();
const timeDelta = bounds.end - bounds.start;
const valueDelta = value - this.left;
const offset = valueDelta / this.width * timeDelta;
return bounds.start + offset;
},
resize() {
if (this.$refs.axisHolder.clientWidth !== this.width) {
this.width = this.$refs.axisHolder.clientWidth;
this.setAxisDimensions();
this.setScale();
}
}

View File

@ -0,0 +1,200 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web 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 Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
<template>
<div class="c-ctrl-wrapper c-ctrl-wrapper--menus-up">
<button class="c-button--menu c-history-button icon-history"
@click.prevent="toggle"
>
<span class="c-button__label">History</span>
</button>
<div v-if="open"
class="c-menu c-conductor__history-menu"
>
<ul v-if="hasHistoryPresets">
<li
v-for="preset in presets"
:key="preset.label"
class="icon-clock"
@click="selectPresetBounds(preset.bounds)"
>
{{ preset.label }}
</li>
</ul>
<div
v-if="hasHistoryPresets"
class="c-menu__section-separator"
></div>
<div class="c-menu__section-hint">
Past timeframes, ordered by latest first
</div>
<ul>
<li
v-for="(timespan, index) in historyForCurrentTimeSystem"
:key="index"
class="icon-history"
@click="selectTimespan(timespan)"
>
{{ formatTime(timespan.start) }} - {{ formatTime(timespan.end) }}
</li>
</ul>
</div>
</div>
</template>
<script>
import toggleMixin from '../../ui/mixins/toggle-mixin';
const LOCAL_STORAGE_HISTORY_KEY = 'tcHistory';
const DEFAULT_RECORDS = 10;
export default {
inject: ['openmct', 'configuration'],
mixins: [toggleMixin],
props: {
bounds: {
type: Object,
required: true
},
timeSystem: {
type: Object,
required: true
}
},
data() {
return {
history: {}, // contains arrays of timespans {start, end}, array key is time system key
presets: []
}
},
computed: {
hasHistoryPresets() {
return this.timeSystem.isUTCBased && this.presets.length;
},
historyForCurrentTimeSystem() {
const history = this.history[this.timeSystem.key];
return history;
}
},
watch: {
bounds: {
handler() {
this.addTimespan();
},
deep: true
},
timeSystem: {
handler() {
this.loadConfiguration();
this.addTimespan();
},
deep: true
},
history: {
handler() {
this.persistHistoryToLocalStorage();
},
deep: true
}
},
mounted() {
this.getHistoryFromLocalStorage();
},
methods: {
getHistoryFromLocalStorage() {
if (localStorage.getItem(LOCAL_STORAGE_HISTORY_KEY)) {
this.history = JSON.parse(localStorage.getItem(LOCAL_STORAGE_HISTORY_KEY))
} else {
this.history = {};
this.persistHistoryToLocalStorage();
}
},
persistHistoryToLocalStorage() {
localStorage.setItem(LOCAL_STORAGE_HISTORY_KEY, JSON.stringify(this.history));
},
addTimespan() {
const key = this.timeSystem.key;
let [...currentHistory] = this.history[key] || [];
const timespan = {
start: this.bounds.start,
end: this.bounds.end
};
const isNotEqual = function (entry) {
const start = entry.start !== this.start;
const end = entry.end !== this.end;
return start || end;
};
currentHistory = currentHistory.filter(isNotEqual, timespan);
while (currentHistory.length >= this.records) {
currentHistory.pop();
}
currentHistory.unshift(timespan);
this.history[key] = currentHistory;
},
selectTimespan(timespan) {
this.openmct.time.bounds(timespan);
},
selectPresetBounds(bounds) {
const start = typeof bounds.start === 'function' ? bounds.start() : bounds.start;
const end = typeof bounds.end === 'function' ? bounds.end() : bounds.end;
this.selectTimespan({
start: start,
end: end
});
},
loadConfiguration() {
const configurations = this.configuration.menuOptions
.filter(option => option.timeSystem === this.timeSystem.key);
this.presets = this.loadPresets(configurations);
this.records = this.loadRecords(configurations);
},
loadPresets(configurations) {
const configuration = configurations.find(option => option.presets);
const presets = configuration ? configuration.presets : [];
return presets;
},
loadRecords(configurations) {
const configuration = configurations.find(option => option.records);
const records = configuration ? configuration.records : DEFAULT_RECORDS;
return records;
},
formatTime(time) {
const formatter = this.openmct.telemetry.getValueFormatter({
format: this.timeSystem.timeFormat
}).formatter;
return formatter.format(time);
}
}
}
</script>

View File

@ -110,7 +110,7 @@ export default {
if (clock === undefined) {
return {
key: 'fixed',
name: 'Fixed Timespan Mode',
name: 'Fixed Timespan',
description: 'Query and explore data that falls between two fixed datetimes.',
cssClass: 'icon-tabular'
}

View File

@ -13,7 +13,7 @@
text-rendering: geometricPrecision;
width: 100%;
height: 100%;
> g {
> g.axis {
// Overall Tick holder
transform: translateY($tickYPos);
path {
@ -44,7 +44,6 @@
}
body.desktop .is-fixed-mode & {
@include cursorGrab();
background-size: 3px 30%;
background-color: $colorBodyBgSubtle;
box-shadow: inset rgba(black, 0.4) 0 1px 1px;
@ -55,17 +54,6 @@
stroke: $colorBodyBgSubtle;
transition: $transOut;
}
&:hover,
&:active {
$c: $colorKeySubtle;
background-color: $c;
transition: $transIn;
svg text {
stroke: $c;
transition: $transIn;
}
}
}
.is-realtime-mode & {

View File

@ -57,6 +57,65 @@
}
}
&.is-fixed-mode {
.c-conductor-axis {
&__zoom-indicator {
border: 1px solid transparent;
display: none; // Hidden by default
}
}
&:not(.is-panning),
&:not(.is-zooming) {
.c-conductor-axis {
&:hover,
&:active {
cursor: col-resize;
filter: $timeConductorAxisHoverFilter;
}
}
}
&.is-panning,
&.is-zooming {
.c-conductor-input input {
// Styles for inputs while zooming or panning
background: rgba($timeConductorActiveBg, 0.4);
}
}
&.alt-pressed {
.c-conductor-axis:hover {
// When alt is being pressed and user is hovering over the axis, set the cursor
@include cursorGrab();
}
}
&.is-panning {
.c-conductor-axis {
@include cursorGrab();
background-color: $timeConductorActivePanBg;
transition: $transIn;
svg text {
stroke: $timeConductorActivePanBg;
transition: $transIn;
}
}
}
&.is-zooming {
.c-conductor-axis__zoom-indicator {
display: block;
position: absolute;
background: rgba($timeConductorActiveBg, 0.4);
border-left-color: $timeConductorActiveBg;
border-right-color: $timeConductorActiveBg;
top: 0; bottom: 0;
}
}
}
&.is-realtime-mode {
.c-conductor__time-bounds {
grid-template-columns: 20px auto 1fr auto auto;

View File

@ -142,6 +142,9 @@ $colorTimeHov: pullForward($colorTime, 10%);
$colorTimeSubtle: pushBack($colorTime, 20%);
$colorTOI: $colorBodyFg; // was $timeControllerToiLineColor
$colorTOIHov: $colorTime; // was $timeControllerToiLineColorHov
$timeConductorAxisHoverFilter: brightness(1.2);
$timeConductorActiveBg: $colorKey;
$timeConductorActivePanBg: #226074;
/************************************************** BROWSING */
$browseFrameColor: pullForward($colorBodyBg, 10%);

View File

@ -146,6 +146,9 @@ $colorTimeHov: pullForward($colorTime, 10%);
$colorTimeSubtle: pushBack($colorTime, 20%);
$colorTOI: $colorBodyFg; // was $timeControllerToiLineColor
$colorTOIHov: $colorTime; // was $timeControllerToiLineColorHov
$timeConductorAxisHoverFilter: brightness(1.2);
$timeConductorActiveBg: $colorKey;
$timeConductorActivePanBg: #226074;
/************************************************** BROWSING */
$browseFrameColor: pullForward($colorBodyBg, 10%);

View File

@ -132,7 +132,7 @@ $colorPausedFg: #fff;
// Base variations
$colorBodyBgSubtle: pullForward($colorBodyBg, 5%);
$colorBodyBgSubtleHov: pushBack($colorKey, 50%);
$colorKeySubtle: pushBack($colorKey, 10%);
$colorKeySubtle: pushBack($colorKey, 20%);
// Time Colors
$colorTime: #618cff;
@ -142,6 +142,9 @@ $colorTimeHov: pushBack($colorTime, 5%);
$colorTimeSubtle: pushBack($colorTime, 20%);
$colorTOI: $colorBodyFg; // was $timeControllerToiLineColor
$colorTOIHov: $colorTime; // was $timeControllerToiLineColorHov
$timeConductorAxisHoverFilter: brightness(0.8);
$timeConductorActiveBg: $colorKey;
$timeConductorActivePanBg: #A0CDE1;
/************************************************** BROWSING */
$browseFrameColor: pullForward($colorBodyBg, 10%);

View File

@ -462,9 +462,17 @@ select {
text-shadow: $shdwMenuText;
padding: $interiorMarginSm;
box-shadow: $shdwMenu;
display: block;
display: flex;
flex-direction: column;
position: absolute;
z-index: 100;
> * {
flex: 0 0 auto;
//+ * {
// margin-top: $interiorMarginSm;
//}
}
}
@mixin menuInner() {
@ -502,6 +510,23 @@ select {
.c-menu {
@include menuOuter();
@include menuInner();
&__section-hint {
$m: $interiorMargin;
margin: $m 0;
padding: $m nth($menuItemPad, 2) 0 nth($menuItemPad, 2);
opacity: 0.6;
font-size: 0.9em;
font-style: italic;
}
&__section-separator {
$m: $interiorMargin;
border-top: 1px solid $colorInteriorBorder;
margin: $m 0;
padding: $m nth($menuItemPad, 2) 0 nth($menuItemPad, 2);
}
}
.c-super-menu {

View File

@ -1,6 +1,11 @@
@mixin visibleRegexButton {
opacity: 1;
padding: 1px 3px;
width: 24px;
}
.c-search {
@include wrappedInput();
padding-top: 2px;
padding-bottom: 2px;
@ -9,11 +14,46 @@
content: $glyph-icon-magnify;
}
&__use-regex {
// Button
$c: $colorBodyFg;
background: rgba($c, 0.2);
border: 1px solid rgba($c, 0.3);
color: $c;
border-radius: $controlCr;
font-weight: bold;
letter-spacing: 1px;
font-size: 0.8em;
margin-left: $interiorMarginSm;
min-width: 0;
opacity: 0;
order: 2;
overflow: hidden;
padding: 1px 0;
transform-origin: left;
transition: $transOut;
width: 0;
&.is-active {
$c: $colorBtnActiveBg;
@include visibleRegexButton();
background: rgba($c, 0.3);
border-color: $c;
color: $c;
}
}
&__clear-input {
display: none;
order: 99;
padding: 1px 0;
}
&.is-active {
.c-search__use-regex {
margin-left: 0;
}
.c-search__clear-input {
display: block;
}
@ -21,6 +61,15 @@
input[type='text'],
input[type='search'] {
margin-left: $interiorMargin;
order: 3;
text-align: left;
}
&:hover {
.c-search__use-regex {
@include visibleRegexButton();
transition: $transIn;
}
}
}

View File

@ -15,6 +15,7 @@
class="c-search__clear-input icon-x-in-circle"
@click="clearInput"
></a>
<slot></slot>
</div>
</template>

View File

@ -108,7 +108,7 @@ export default {
let object = selection[0][0].context.item;
if (object) {
let type = this.openmct.types.get(object.type);
this.showStyles = (this.excludeObjectTypes.indexOf(object.type) < 0) && type.definition.creatable;
this.showStyles = this.isLayoutObject(selection[0], object.type) || this.isCreatableObject(object, type);
}
if (!this.currentTabbedView.key || (!this.showStyles && this.currentTabbedView.key === this.tabbedViews[1].key))
{
@ -116,6 +116,14 @@ export default {
}
}
},
isLayoutObject(selection, objectType) {
//we allow conditionSets to be styled if they're part of a layout
return selection.length > 1 &&
((objectType === 'conditionSet') || (this.excludeObjectTypes.indexOf(objectType) < 0));
},
isCreatableObject(object, type) {
return (this.excludeObjectTypes.indexOf(object.type) < 0) && type.definition.creatable;
},
updateCurrentTab(view) {
this.currentTabbedView = view;
},