Compare commits

...

17 Commits

Author SHA1 Message Date
0d8a66c9e4 catching errors while retrieving git info 2022-06-16 13:19:14 -07:00
60d021ef82 Fix imagery filter slider drag in flexible layouts (#5326) (#5350) 2022-06-16 12:25:29 -07:00
59880955a2 Remove snapshot 2022-06-08 19:11:40 -07:00
b51ed7e844 Merge branch 'master' of https://github.com/nasa/openmct 2022-06-08 19:11:13 -07:00
0f0c6a7b17 2.0.4 merge into master (#5297)
* Release 2.0.3

* Fix tick values for plots ticks in log mode and null check (#5119)

* [2297] When there is no display range or range, skip setting the range value when auto scale is turned off.

* If the formatted value is a number and a float, set precision to 2 decimal points.

* Fix value assignment

* Use whole numbers in log mode

* Revert whole numbers fix - need floats for values between 0 and 1.

* Handle scrolling to focused image on resize/new data (#5121)

* Scroll to focused image when view resizes - this will force scrolling to focused image when going to/from view large mode

* Scroll to the right if there is no paused focused image

* [LAD Tables] Use Telemetry Collections (#5127)

* Use telemetry collections to handle bounds checks

* added telemetry collection to alphanumeric telemetry view (#5131)

* Added animation styling for POS and CAM; adjusted cutoff for isNewImage (#5116)

* Added animation styling for POS and CAM; adjusted cutoff for isNewImage

* Remove animation from POS and CAM

* Fix transactions overwriting latest objects with stale objects on save (#5132)

* use object (map) instead of set to track dirty objects
* fix tests due to internals change

Co-authored-by: Nikhil <nikhil.k.mandlik@nasa.gov>

* Gauge edit enabled 2.0.3 (#5133)

* Gauge plugin #4896, add edit mode

* Dynamic dial-type Gauge sizing by height and width (#5129)

* Improve sizing strategy for gauges.
* Do not install gauge by default for now

Co-authored-by: Nikhil <nikhil.k.mandlik@nasa.gov>
Co-authored-by: Jamie Vigliotta <jamie.j.vigliotta@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>

* [Telemetry Collections] Include data with start and end bounds (#5145)

* Reverts forced precision for log plots axis labels (#5147)

* Condition Widgets trigger hundreds of persistence calls (#5146)

Co-authored-by: unlikelyzero <jchill2@gmail.com>

* Update version for 2.0.4 (#5255)

* Eliminate NaN conditions and clear stale duration (#5248)

* Temp source map fix 2.0.4 (#5267)

* use dev mode for production

* mode -> production

* added extra devtool options

* wip

* Imagery Fixes for release/2.0.4 (#5282)

* Fallback for height

* Remove duplicated requestHistory call since setDataTimeContext already invokes it on mount

* Inverted datumIsNotValid and refactored requestHistory

* Remove old datumIsNotValid func

* Return false if datum is falsy

* Corrected brightness/contrast input

* Clone default values to avoid mutation

* Changed index of imageTelemetry to an item within bounds

* Implement clearData test for imagery differently

* x-out clearData tests

Co-authored-by: Joshi <simplyrender@gmail.com>

* Imagery test fixes (#5293)

* Fallback for height

* Remove duplicated requestHistory call since setDataTimeContext already invokes it on mount

* Inverted datumIsNotValid and refactored requestHistory

* Remove old datumIsNotValid func

* Return false if datum is falsy

* Corrected brightness/contrast input

* Clone default values to avoid mutation

* Changed index of imageTelemetry to an item within bounds

* Implement clearData test for imagery differently

* x-out clearData tests

* Set bounds on each test rather than the wrapper

Co-authored-by: Michael Rogers <contact@mhrogers.com>

* Imagery validation fix (#5295)

* Remove check for duplicate images
* Remove commented out code and add TODO

* lint fix

* Add missing tests

* Use the master version and ignore release/2.0.4 changes

Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
Co-authored-by: Michael Rogers <contact@mhrogers.com>
Co-authored-by: David Tsay <3614296+davetsay@users.noreply.github.com>
Co-authored-by: Nikhil <nikhil.k.mandlik@nasa.gov>
Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: unlikelyzero <jchill2@gmail.com>
2022-06-09 01:06:31 +00:00
370e6a0c37 fixing non functioning render test, boost cov also (#5311) 2022-06-08 17:39:43 -07:00
815506cf17 Demote notebook tests (#5313)
Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2022-06-09 00:17:41 +00:00
bdb1867c73 Selection of stacked plot items and customizing them (#5198)
* Adds stacked plot inspector view provider for non subObjects

* Initialize config for telemetry objects that cannot be persisted with the config in the stacked plot
Use events to save telemetry object config changes to the stacked plot
Remove changes that weren't relevant anymore

* Ensure the telemetry objects that cannot be persisted are initialized correctly

* Fixes for selection indication in Stacked Plots
- Better theme constant colors.
- Fixed broken selectors.
- Changes also improve selection editing UI for Display and Flex Layouts.

* Ensure unique colors for stacked plot if they are auto assigned

* Fix bug hiding legend when viewing plots nested within a stacked plot

* Move stacked plots tests to it's own pluginSpec to simplify tests

Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: Rukmini Bose <rukmini.bose15@gmail.com>
2022-06-08 22:17:40 +00:00
e288fdffea Fixes #3756 (#5192)
- Tweaks to image CSS to allow context click access.
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-06-08 21:47:51 +00:00
194060f30a [Flexible Layout] Unit test for rendering the view (#5308)
* flex layout render test to boost coverage
2022-06-08 13:58:49 -07:00
45bc317a59 [e2e] Add clarity to console.error failures (#5304)
- Create a separate assert for each message

- Format the `ConsoleMessage` to provide location, line, and col numbers
2022-06-08 13:05:08 -07:00
7bbaec4006 Merge branch 'master' of https://github.com/nasa/openmct 2022-06-07 14:02:58 -07:00
c0f24b3925 Merge branch 'master' of https://github.com/nasa/openmct 2022-05-31 11:06:55 -07:00
4e79725897 Merge branch 'master' of https://github.com/nasa/openmct 2022-05-24 15:05:16 -07:00
0674c9fc33 Merge branch 'master' of https://github.com/nasa/openmct 2022-05-20 09:25:39 -07:00
de1b877954 Merge branch 'master' of https://github.com/nasa/openmct 2022-05-09 14:00:43 -07:00
4db2f547d9 Bump d3-selection from 1.3.2 to 3.0.0
Bumps [d3-selection](https://github.com/d3/d3-selection) from 1.3.2 to 3.0.0.
- [Release notes](https://github.com/d3/d3-selection/releases)
- [Commits](https://github.com/d3/d3-selection/compare/v1.3.2...v3.0.0)

---
updated-dependencies:
- dependency-name: d3-selection
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-05-06 15:45:20 +00:00
37 changed files with 1456 additions and 530 deletions

View File

@ -4,15 +4,29 @@
const base = require('@playwright/test'); const base = require('@playwright/test');
const { expect } = require('@playwright/test'); const { expect } = require('@playwright/test');
/**
* Takes a `ConsoleMessage` and returns a formatted string
* @param {import('@playwright/test').ConsoleMessage} msg
* @returns {String} formatted string with message type, text, url, and line and column numbers
*/
function consoleMessageToString(msg) {
const { url, lineNumber, columnNumber } = msg.location();
return `[${msg.type()}] ${msg.text()}
at (${url} ${lineNumber}:${columnNumber})`;
}
exports.test = base.test.extend({ exports.test = base.test.extend({
page: async ({ baseURL, page }, use) => { page: async ({ baseURL, page }, use) => {
const messages = []; const messages = [];
page.on('console', msg => messages.push(`[${msg.type()}] ${msg.text()}`)); page.on('console', (msg) => messages.push(msg));
await use(page); await use(page);
await expect.soft(messages.toString()).not.toContain('[error]'); messages.forEach(
msg => expect.soft(msg.type(), `Console error detected: ${consoleMessageToString(msg)}`).not.toEqual('error')
);
}, },
browser: async ({ playwright, browser }, use, workerInfo) => { browser: async ({ playwright, browser }, use, workerInfo) => {
// Use browserless if configured // Use browserless if configured
if (workerInfo.project.name.match(/browserless/)) { if (workerInfo.project.name.match(/browserless/)) {
const vBrowser = await playwright.chromium.connectOverCDP({ const vBrowser = await playwright.chromium.connectOverCDP({
endpointURL: 'ws://localhost:3003' endpointURL: 'ws://localhost:3003'

View File

@ -19,4 +19,4 @@
] ]
} }
] ]
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "openmct", "name": "openmct",
"version": "2.0.5-SNAPSHOT", "version": "2.0.5",
"description": "The Open MCT core platform", "description": "The Open MCT core platform",
"devDependencies": { "devDependencies": {
"@babel/eslint-parser": "7.18.2", "@babel/eslint-parser": "7.18.2",
@ -89,7 +89,7 @@
"test": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run", "test": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run",
"test:firefox": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run --browsers=FirefoxHeadless", "test:firefox": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run --browsers=FirefoxHeadless",
"test:debug": "cross-env NODE_ENV=debug karma start --no-single-run", "test:debug": "cross-env NODE_ENV=debug karma start --no-single-run",
"test:e2e:ci": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome smoke branding default condition timeConductor clock exampleImagery notebook persistence performance", "test:e2e:ci": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome smoke branding default condition timeConductor clock exampleImagery persistence performance",
"test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome", "test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome",
"test:e2e:updatesnapshots": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome --grep @snapshot --update-snapshots", "test:e2e:updatesnapshots": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome --grep @snapshot --update-snapshots",
"test:e2e:visual": "percy exec --config ./e2e/.percy.yml -- npx playwright test --config=e2e/playwright-visual.config.js", "test:e2e:visual": "percy exec --config ./e2e/.percy.yml -- npx playwright test --config=e2e/playwright-visual.config.js",

View File

@ -28,7 +28,6 @@
&[s-selected] { &[s-selected] {
// All frames selected while editing // All frames selected while editing
border: $editFrameSelectedBorder;
box-shadow: $editFrameSelectedShdw; box-shadow: $editFrameSelectedShdw;
.c-frame__move-bar { .c-frame__move-bar {

View File

@ -41,7 +41,7 @@ describe('the plugin', function () {
element.appendChild(child); element.appendChild(child);
openmct.on('start', done); openmct.on('start', done);
openmct.startHeadless(); openmct.start(child);
}); });
afterEach(() => { afterEach(() => {

View File

@ -141,6 +141,10 @@
} }
} }
} }
[s-selected].c-fl-frame__drag-wrapper {
border: $editFrameSelectedBorder;
}
} }
/****** THEIR FRAMES */ /****** THEIR FRAMES */

View File

@ -22,6 +22,7 @@
import { createOpenMct, resetApplicationState } from 'utils/testing'; import { createOpenMct, resetApplicationState } from 'utils/testing';
import FlexibleLayout from './plugin'; import FlexibleLayout from './plugin';
import Vue from 'vue';
describe('the plugin', function () { describe('the plugin', function () {
let element; let element;
@ -61,7 +62,7 @@ describe('the plugin', function () {
element.appendChild(child); element.appendChild(child);
openmct.on('start', done); openmct.on('start', done);
openmct.startHeadless(); openmct.start(child);
}); });
afterEach(() => { afterEach(() => {
@ -83,6 +84,16 @@ describe('the plugin', function () {
it('provides a view', () => { it('provides a view', () => {
expect(flexibleLayoutViewProvider).toBeDefined(); expect(flexibleLayoutViewProvider).toBeDefined();
}); });
it('renders a view', async () => {
const flexibleView = flexibleLayoutViewProvider.view(testViewObject, []);
flexibleView.show(child, false);
await Vue.nextTick();
const flexTitle = child.querySelector('.l-browse-bar .c-object-label__name');
expect(flexTitle).not.toBeNull();
});
}); });
describe('the toolbar', () => { describe('the toolbar', () => {

View File

@ -14,6 +14,8 @@
type="range" type="range"
min="0" min="0"
max="500" max="500"
draggable="true"
@dragstart.stop.prevent
@change="notifyFiltersChanged" @change="notifyFiltersChanged"
@input="notifyFiltersChanged" @input="notifyFiltersChanged"
> >
@ -24,6 +26,8 @@
type="range" type="range"
min="0" min="0"
max="500" max="500"
draggable="true"
@dragstart.stop.prevent
@change="notifyFiltersChanged" @change="notifyFiltersChanged"
@input="notifyFiltersChanged" @input="notifyFiltersChanged"
> >

View File

@ -21,7 +21,11 @@
*****************************************************************************/ *****************************************************************************/
<template> <template>
<div class="h-local-controls h-local-controls--overlay-content h-local-controls--menus-aligned c-local-controls--show-on-hover"> <div
class="h-local-controls h-local-controls--overlay-content h-local-controls--menus-aligned c-local-controls--show-on-hover"
role="toolbar"
aria-label="Image controls"
>
<imagery-view-menu-switcher <imagery-view-menu-switcher
:icon-class="'icon-brightness'" :icon-class="'icon-brightness'"
:title="'Brightness and contrast'" :title="'Brightness and contrast'"
@ -173,7 +177,7 @@ export default {
this.$emit('filtersUpdated', this.filters); this.$emit('filtersUpdated', this.filters);
}, },
handleResetFilters() { handleResetFilters() {
this.filters = DEFAULT_FILTER_VALUES; this.filters = {...DEFAULT_FILTER_VALUES};
this.notifyFiltersChanged(); this.notifyFiltersChanged();
}, },
limitZoomRange(factor) { limitZoomRange(factor) {

View File

@ -403,6 +403,9 @@ export default {
formattedDuration() { formattedDuration() {
let result = 'N/A'; let result = 'N/A';
let negativeAge = -1; let negativeAge = -1;
if (!Number.isInteger(this.numericDuration)) {
return result;
}
if (this.numericDuration > TWENTYFOUR_HOURS) { if (this.numericDuration > TWENTYFOUR_HOURS) {
negativeAge *= (this.numericDuration / TWENTYFOUR_HOURS); negativeAge *= (this.numericDuration / TWENTYFOUR_HOURS);
@ -905,8 +908,10 @@ export default {
let currentTime = this.timeContext.clock() && this.timeContext.clock().currentValue(); let currentTime = this.timeContext.clock() && this.timeContext.clock().currentValue();
if (currentTime === undefined) { if (currentTime === undefined) {
this.numericDuration = currentTime; this.numericDuration = currentTime;
} else { } else if (Number.isInteger(this.parsedSelectedTime)) {
this.numericDuration = currentTime - this.parsedSelectedTime; this.numericDuration = currentTime - this.parsedSelectedTime;
} else {
this.numericDuration = undefined;
} }
}, },
resetAgeCSS() { resetAgeCSS() {

View File

@ -68,15 +68,24 @@
overflow: hidden; overflow: hidden;
} }
&__background-image { &__background-image {
// Actually does the image display
background-position: center; background-position: center;
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: contain; background-size: contain;
height: 100%; //fallback value
} }
&__image { &__image {
// Present to allow Save As... image
position: absolute;
height: 100%; height: 100%;
width: 100%; width: 100%;
visibility: hidden; opacity: 0;
display: contents; }
&__image-save-proxy {
height: 100%;
width: 100%;
z-index: 10;
} }
} }

View File

@ -70,22 +70,18 @@ export default {
this.timeContext.off('timeSystem', this.timeSystemChange); this.timeContext.off('timeSystem', this.timeSystemChange);
} }
}, },
datumIsNotValid(datum) { isDatumValid(datum) {
if (this.imageHistory.length === 0) { //TODO: Add a check to see if there are duplicate images (identical image timestamp and url subsequently)
if (!datum) {
return false; return false;
} }
const datumURL = this.formatImageUrl(datum);
const lastHistoryURL = this.formatImageUrl(this.imageHistory.slice(-1)[0]);
// datum is not valid if it matches the last datum in history,
// or it is before the last datum in the history
const datumTimeCheck = this.parseTime(datum); const datumTimeCheck = this.parseTime(datum);
const historyTimeCheck = this.parseTime(this.imageHistory.slice(-1)[0]); const bounds = this.timeContext.bounds();
const matchesLast = (datumTimeCheck === historyTimeCheck) && (datumURL === lastHistoryURL);
const isStale = datumTimeCheck < historyTimeCheck;
return matchesLast || isStale; const isOutOfBounds = datumTimeCheck < bounds.start || datumTimeCheck > bounds.end;
return !isOutOfBounds;
}, },
formatImageUrl(datum) { formatImageUrl(datum) {
if (!datum) { if (!datum) {
@ -132,25 +128,19 @@ export default {
return this.requestHistory(); return this.requestHistory();
}, },
async requestHistory() { async requestHistory() {
let bounds = this.timeContext.bounds();
this.requestCount++; this.requestCount++;
const requestId = this.requestCount; const requestId = this.requestCount;
this.imageHistory = []; const bounds = this.timeContext.bounds();
let data = await this.openmct.telemetry const data = await this.openmct.telemetry
.request(this.domainObject, bounds) || []; .request(this.domainObject, bounds) || [];
// wait until new request resolves to do comparison
if (this.requestCount === requestId) { if (this.requestCount !== requestId) {
let imagery = []; return this.imageHistory = [];
data.forEach((datum) => {
let image = this.normalizeDatum(datum);
if (image) {
imagery.push(image);
}
});
//this is to optimize anything that reacts to imageHistory length
this.imageHistory = imagery;
} }
const imagery = data.filter(this.isDatumValid).map(this.normalizeDatum);
this.imageHistory = imagery;
}, },
clearData(domainObjectToClear) { clearData(domainObjectToClear) {
// global clearData button is accepted therefore no truthy check on inputted param // global clearData button is accepted therefore no truthy check on inputted param
@ -180,27 +170,29 @@ export default {
.subscribe(this.domainObject, (datum) => { .subscribe(this.domainObject, (datum) => {
let parsedTimestamp = this.parseTime(datum); let parsedTimestamp = this.parseTime(datum);
let bounds = this.timeContext.bounds(); let bounds = this.timeContext.bounds();
if (!(parsedTimestamp >= bounds.start && parsedTimestamp <= bounds.end)) {
return;
}
if (parsedTimestamp >= bounds.start && parsedTimestamp <= bounds.end) { if (this.isDatumValid(datum)) {
let image = this.normalizeDatum(datum); this.imageHistory.push(this.normalizeDatum(datum));
if (image) {
this.imageHistory.push(image);
}
} }
}); });
}, },
normalizeDatum(datum) { normalizeDatum(datum) {
if (this.datumIsNotValid(datum)) {
return;
}
let image = { ...datum }; const formattedTime = this.formatTime(datum);
image.formattedTime = this.formatTime(datum); const url = this.formatImageUrl(datum);
image.url = this.formatImageUrl(datum); const time = this.parseTime(formattedTime);
image.time = this.parseTime(image.formattedTime); const imageDownloadName = this.getImageDownloadName(datum);
image.imageDownloadName = this.getImageDownloadName(datum);
return image; return {
...datum,
formattedTime,
url,
time,
imageDownloadName
};
}, },
getFormatter(key) { getFormatter(key) {
let metadataValue = this.metadata.value(key) || { format: key }; let metadataValue = this.metadata.value(key) || { format: key };

View File

@ -84,7 +84,6 @@ describe("The Imagery View Layouts", () => {
let telemetryPromise; let telemetryPromise;
let telemetryPromiseResolve; let telemetryPromiseResolve;
let cleanupFirst; let cleanupFirst;
let isClearDataTriggered;
let openmct; let openmct;
let parent; let parent;
@ -205,20 +204,12 @@ describe("The Imagery View Layouts", () => {
cleanupFirst = []; cleanupFirst = [];
openmct = createOpenMct(); openmct = createOpenMct();
openmct.time.timeSystem('utc', {
start: START - (5 * ONE_MINUTE),
end: START + (5 * ONE_MINUTE)
});
telemetryPromise = new Promise((resolve) => { telemetryPromise = new Promise((resolve) => {
telemetryPromiseResolve = resolve; telemetryPromiseResolve = resolve;
}); });
spyOn(openmct.telemetry, 'request').and.callFake(() => { spyOn(openmct.telemetry, 'request').and.callFake(() => {
if (isClearDataTriggered) {
return [];
}
telemetryPromiseResolve(imageTelemetry); telemetryPromiseResolve(imageTelemetry);
return telemetryPromise; return telemetryPromise;
@ -337,44 +328,93 @@ describe("The Imagery View Layouts", () => {
expect(imageryView).toBeDefined(); expect(imageryView).toBeDefined();
}); });
describe("imagery view", () => { describe("Clear data action for imagery", () => {
let applicableViews; let applicableViews;
let imageryViewProvider; let imageryViewProvider;
let imageryView; let imageryView;
let componentView;
let clearDataPlugin; let clearDataPlugin;
let clearDataAction; let clearDataAction;
beforeEach(() => { beforeEach(() => {
openmct.time.timeSystem('utc', {
start: START - (5 * ONE_MINUTE),
end: START + (5 * ONE_MINUTE)
});
applicableViews = openmct.objectViews.get(imageryObject, [imageryObject]); applicableViews = openmct.objectViews.get(imageryObject, [imageryObject]);
imageryViewProvider = applicableViews.find(viewProvider => viewProvider.key === imageryKey); imageryViewProvider = applicableViews.find(viewProvider => viewProvider.key === imageryKey);
imageryView = imageryViewProvider.view(imageryObject, [imageryObject]); imageryView = imageryViewProvider.view(imageryObject, [imageryObject]);
imageryView.show(child); imageryView.show(child);
componentView = imageryView._getInstance().$children[0];
clearDataPlugin = new ClearDataPlugin( clearDataPlugin = new ClearDataPlugin(
['example.imagery'], ['example.imagery'],
{indicator: true} {indicator: true}
); );
openmct.install(clearDataPlugin); openmct.install(clearDataPlugin);
clearDataAction = openmct.actions.getAction('clear-data-action'); clearDataAction = openmct.actions.getAction('clear-data-action');
return Vue.nextTick();
});
it('clear data action is installed', () => {
expect(clearDataAction).toBeDefined();
});
it('on clearData action should clear data for object is selected', (done) => {
// force show the thumbnails // force show the thumbnails
componentView.forceShowThumbnails = true;
Vue.nextTick(() => {
let clearDataResolve;
let telemetryRequestPromise = new Promise((resolve) => {
clearDataResolve = resolve;
});
expect(parent.querySelectorAll('.c-imagery__thumb').length).not.toBe(0);
openmct.objectViews.on('clearData', (_domainObject) => {
return Vue.nextTick(() => {
expect(parent.querySelectorAll('.c-imagery__thumb').length).toBe(0);
clearDataResolve();
});
});
clearDataAction.invoke(imageryObject);
telemetryRequestPromise.then(() => {
done();
});
});
});
});
describe("imagery view", () => {
let applicableViews;
let imageryViewProvider;
let imageryView;
beforeEach(() => {
openmct.time.timeSystem('utc', {
start: START - (5 * ONE_MINUTE),
end: START + (5 * ONE_MINUTE)
});
applicableViews = openmct.objectViews.get(imageryObject, [imageryObject]);
imageryViewProvider = applicableViews.find(viewProvider => viewProvider.key === imageryKey);
imageryView = imageryViewProvider.view(imageryObject, [imageryObject]);
imageryView.show(child);
imageryView._getInstance().$children[0].forceShowThumbnails = true; imageryView._getInstance().$children[0].forceShowThumbnails = true;
return Vue.nextTick(); return Vue.nextTick();
}); });
afterEach(() => {
isClearDataTriggered = false;
// openmct.time.stopClock();
// openmct.router.removeListener('change:hash', resolveFunction);
// imageryView.destroy();
});
it("on mount should show the the most recent image", (done) => { it("on mount should show the the most recent image", () => {
//Looks like we need Vue.nextTick here so that computed properties settle down //Looks like we need Vue.nextTick here so that computed properties settle down
Vue.nextTick(() => { return Vue.nextTick(() => {
const imageInfo = getImageInfo(parent); const imageInfo = getImageInfo(parent);
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 1].timeId)).not.toEqual(-1); expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 1].timeId)).not.toEqual(-1);
done();
}); });
}); });
@ -422,7 +462,7 @@ describe("The Imagery View Layouts", () => {
it("should show that an image is not new", (done) => { it("should show that an image is not new", (done) => {
Vue.nextTick(() => { Vue.nextTick(() => {
const target = imageTelemetry[2].url; const target = imageTelemetry[4].url;
parent.querySelectorAll(`img[src='${target}']`)[0].click(); parent.querySelectorAll(`img[src='${target}']`)[0].click();
Vue.nextTick(() => { Vue.nextTick(() => {
@ -544,25 +584,6 @@ describe("The Imagery View Layouts", () => {
expect(imageSizeAfter.width).toBeLessThan(imageSizeBefore.width); expect(imageSizeAfter.width).toBeLessThan(imageSizeBefore.width);
done(); done();
}); });
it('clear data action is installed', () => {
expect(clearDataAction).toBeDefined();
});
it('on clearData action should clear data for object is selected', async (done) => {
// force show the thumbnails
imageryView._getInstance().$children[0].forceShowThumbnails = true;
await Vue.nextTick();
expect(parent.querySelectorAll('.c-imagery__thumb').length).not.toBe(0);
openmct.objectViews.on('clearData', async (_domainObject) => {
await Vue.nextTick();
expect(parent.querySelectorAll('.c-imagery__thumb').length).toBe(0);
done();
});
// stubbed telemetry data will return empty array when true
isClearDataTriggered = true;
clearDataAction.invoke(imageryObject);
});
}); });
describe("imagery time strip view", () => { describe("imagery time strip view", () => {

View File

@ -26,6 +26,7 @@
:class="[plotLegendExpandedStateClass, plotLegendPositionClass]" :class="[plotLegendExpandedStateClass, plotLegendPositionClass]"
> >
<plot-legend <plot-legend
v-if="!isNestedWithinAStackedPlot"
:cursor-locked="!!lockHighlightPoint" :cursor-locked="!!lockHighlightPoint"
:series="seriesModels" :series="seriesModels"
:highlights="highlights" :highlights="highlights"
@ -246,6 +247,18 @@ export default {
default() { default() {
return 0; return 0;
} }
},
limitLineLabels: {
type: Object,
default() {
return {};
}
},
colorPalette: {
type: Object,
default() {
return undefined;
}
} }
}, },
data() { data() {
@ -266,7 +279,7 @@ export default {
isRealTime: this.openmct.time.clock() !== undefined, isRealTime: this.openmct.time.clock() !== undefined,
loaded: false, loaded: false,
isTimeOutOfSync: false, isTimeOutOfSync: false,
showLimitLineLabels: undefined, showLimitLineLabels: this.limitLineLabels,
isFrozenOnMouseDown: false, isFrozenOnMouseDown: false,
hasSameRangeValue: true, hasSameRangeValue: true,
cursorGuide: this.initCursorGuide, cursorGuide: this.initCursorGuide,
@ -274,13 +287,22 @@ export default {
}; };
}, },
computed: { computed: {
isNestedWithinAStackedPlot() {
const isNavigatedObject = this.openmct.router.isNavigatedObject([this.domainObject].concat(this.path));
return !isNavigatedObject && this.path.find((pathObject, pathObjIndex) => pathObject.type === 'telemetry.plot.stacked');
},
isFrozen() { isFrozen() {
return this.config.xAxis.get('frozen') === true && this.config.yAxis.get('frozen') === true; return this.config.xAxis.get('frozen') === true && this.config.yAxis.get('frozen') === true;
}, },
plotLegendPositionClass() { plotLegendPositionClass() {
return `plot-legend-${this.config.legend.get('position')}`; return !this.isNestedWithinAStackedPlot ? `plot-legend-${this.config.legend.get('position')}` : '';
}, },
plotLegendExpandedStateClass() { plotLegendExpandedStateClass() {
if (this.isNestedWithinAStackedPlot) {
return '';
}
if (this.config.legend.get('expanded')) { if (this.config.legend.get('expanded')) {
return 'plot-legend-expanded'; return 'plot-legend-expanded';
} else { } else {
@ -292,6 +314,12 @@ export default {
} }
}, },
watch: { watch: {
limitLineLabels: {
handler(limitLineLabels) {
this.legendHoverChanged(limitLineLabels);
},
deep: true
},
initGridLines(newGridLines) { initGridLines(newGridLines) {
this.gridLines = newGridLines; this.gridLines = newGridLines;
}, },
@ -310,6 +338,11 @@ export default {
this.config = this.getConfig(); this.config = this.getConfig();
this.legend = this.config.legend; this.legend = this.config.legend;
if (this.isNestedWithinAStackedPlot) {
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.$emit('configLoaded', configId);
}
this.listenTo(this.config.series, 'add', this.addSeries, this); this.listenTo(this.config.series, 'add', this.addSeries, this);
this.listenTo(this.config.series, 'remove', this.removeSeries, this); this.listenTo(this.config.series, 'remove', this.removeSeries, this);
@ -375,6 +408,7 @@ export default {
id: configId, id: configId,
domainObject: this.domainObject, domainObject: this.domainObject,
openmct: this.openmct, openmct: this.openmct,
palette: this.colorPalette,
callback: (data) => { callback: (data) => {
this.data = data; this.data = data;
} }
@ -758,6 +792,8 @@ export default {
}; };
}); });
} }
this.$emit('highlights', this.highlights);
}, },
untrackMousePosition() { untrackMousePosition() {
@ -792,6 +828,7 @@ export default {
if (this.isMouseClick()) { if (this.isMouseClick()) {
this.lockHighlightPoint = !this.lockHighlightPoint; this.lockHighlightPoint = !this.lockHighlightPoint;
this.$emit('lockHighlightPoint', this.lockHighlightPoint);
} }
if (this.pan) { if (this.pan) {

View File

@ -68,7 +68,8 @@ export default class PlotConfigurationModel extends Model {
this.series = new SeriesCollection({ this.series = new SeriesCollection({
models: options.model.series, models: options.model.series,
plot: this, plot: this,
openmct: options.openmct openmct: options.openmct,
palette: options.palette
}); });
if (this.get('domainObject').type === 'telemetry.plot.overlay') { if (this.get('domainObject').type === 'telemetry.plot.overlay') {

View File

@ -39,7 +39,7 @@ export default class SeriesCollection extends Collection {
this.modelClass = PlotSeries; this.modelClass = PlotSeries;
this.plot = options.plot; this.plot = options.plot;
this.openmct = options.openmct; this.openmct = options.openmct;
this.palette = new ColorPalette(); this.palette = options.palette || new ColorPalette();
this.listenTo(this, 'add', this.onSeriesAdd, this); this.listenTo(this, 'add', this.onSeriesAdd, this);
this.listenTo(this, 'remove', this.onSeriesRemove, this); this.listenTo(this, 'remove', this.onSeriesRemove, this);
this.listenTo(this.plot, 'change:domainObject', this.trackPersistedConfig, this); this.listenTo(this.plot, 'change:domainObject', this.trackPersistedConfig, this);

View File

@ -260,7 +260,7 @@ export default class YAxisModel extends Model {
const plotModel = this.plot.get('domainObject'); const plotModel = this.plot.get('domainObject');
const label = plotModel.configuration?.yAxis?.label; const label = plotModel.configuration?.yAxis?.label;
const sampleSeries = seriesCollection.first(); const sampleSeries = seriesCollection.first();
if (!sampleSeries) { if (!sampleSeries || !sampleSeries.metadata) {
if (!label) { if (!label) {
this.unset('label'); this.unset('label');
} }

View File

@ -24,7 +24,10 @@
v-if="loaded" v-if="loaded"
class="js-plot-options-browse" class="js-plot-options-browse"
> >
<ul class="c-tree"> <ul
v-if="!isStackedPlotObject"
class="c-tree"
>
<h2 title="Plot series display properties in this object">Plot Series</h2> <h2 title="Plot series display properties in this object">Plot Series</h2>
<plot-options-item <plot-options-item
v-for="series in plotSeries" v-for="series in plotSeries"
@ -36,7 +39,10 @@
v-if="plotSeries.length" v-if="plotSeries.length"
class="grid-properties" class="grid-properties"
> >
<ul class="l-inspector-part"> <ul
v-if="!isStackedPlotObject"
class="l-inspector-part js-yaxis-properties"
>
<h2 title="Y axis settings for this object">Y Axis</h2> <h2 title="Y axis settings for this object">Y Axis</h2>
<li class="grid-row"> <li class="grid-row">
<div <div
@ -84,7 +90,10 @@
<div class="grid-cell value">{{ rangeMax }}</div> <div class="grid-cell value">{{ rangeMax }}</div>
</li> </li>
</ul> </ul>
<ul class="l-inspector-part"> <ul
v-if="isStackedPlotObject || !isNestedWithinAStackedPlot"
class="l-inspector-part js-legend-properties"
>
<h2 title="Legend settings for this object">Legend</h2> <h2 title="Legend settings for this object">Legend</h2>
<li class="grid-row"> <li class="grid-row">
<div <div
@ -144,7 +153,7 @@ export default {
components: { components: {
PlotOptionsItem PlotOptionsItem
}, },
inject: ['openmct', 'domainObject'], inject: ['openmct', 'domainObject', 'path'],
data() { data() {
return { return {
config: {}, config: {},
@ -167,12 +176,21 @@ export default {
plotSeries: [] plotSeries: []
}; };
}, },
computed: {
isNestedWithinAStackedPlot() {
return this.path.find((pathObject, pathObjIndex) => pathObjIndex > 0 && pathObject.type === 'telemetry.plot.stacked');
},
isStackedPlotObject() {
return this.path.find((pathObject, pathObjIndex) => pathObjIndex === 0 && pathObject.type === 'telemetry.plot.stacked');
}
},
mounted() { mounted() {
eventHelpers.extend(this); eventHelpers.extend(this);
this.config = this.getConfig(); this.config = this.getConfig();
this.registerListeners(); this.registerListeners();
this.initConfiguration(); this.initConfiguration();
this.loaded = true; this.loaded = true;
}, },
beforeDestroy() { beforeDestroy() {
this.stopListening(); this.stopListening();

View File

@ -24,21 +24,31 @@
v-if="loaded" v-if="loaded"
class="js-plot-options-edit" class="js-plot-options-edit"
> >
<ul class="c-tree"> <ul
v-if="!isStackedPlotObject"
class="c-tree"
>
<h2 title="Display properties for this object">Plot Series</h2> <h2 title="Display properties for this object">Plot Series</h2>
<li <li
v-for="series in plotSeries" v-for="series in plotSeries"
:key="series.key" :key="series.key"
> >
<series-form :series="series" /> <series-form
:series="series"
@seriesUpdated="updateSeriesConfigForObject"
/>
</li> </li>
</ul> </ul>
<y-axis-form <y-axis-form
v-if="plotSeries.length" v-if="plotSeries.length && !isStackedPlotObject"
class="grid-properties" class="grid-properties"
:y-axis="config.yAxis" :y-axis="config.yAxis"
@seriesUpdated="updateSeriesConfigForObject"
/> />
<ul class="l-inspector-part"> <ul
v-if="isStackedPlotObject || !isStackedPlotNestedObject"
class="l-inspector-part"
>
<h2 title="Legend options">Legend</h2> <h2 title="Legend options">Legend</h2>
<legend-form <legend-form
v-if="plotSeries.length" v-if="plotSeries.length"
@ -61,7 +71,7 @@ export default {
SeriesForm, SeriesForm,
YAxisForm YAxisForm
}, },
inject: ['openmct', 'domainObject'], inject: ['openmct', 'domainObject', 'path'],
data() { data() {
return { return {
config: {}, config: {},
@ -69,6 +79,14 @@ export default {
loaded: false loaded: false
}; };
}, },
computed: {
isStackedPlotNestedObject() {
return this.path.find((pathObject, pathObjIndex) => pathObjIndex > 0 && pathObject.type === 'telemetry.plot.stacked');
},
isStackedPlotObject() {
return this.path.find((pathObject, pathObjIndex) => pathObjIndex === 0 && pathObject.type === 'telemetry.plot.stacked');
}
},
mounted() { mounted() {
eventHelpers.extend(this); eventHelpers.extend(this);
this.config = this.getConfig(); this.config = this.getConfig();
@ -98,6 +116,24 @@ export default {
resetAllSeries() { resetAllSeries() {
this.plotSeries = []; this.plotSeries = [];
this.config.series.forEach(this.addSeries, this); this.config.series.forEach(this.addSeries, this);
},
updateSeriesConfigForObject(config) {
const stackedPlotObject = this.path.find((pathObject) => pathObject.type === 'telemetry.plot.stacked');
let index = stackedPlotObject.configuration.series.findIndex((seriesConfig) => {
return this.openmct.objects.areIdsEqual(seriesConfig.identifier, config.identifier);
});
if (index < 0) {
index = stackedPlotObject.configuration.series.length;
}
const configPath = `configuration.series[${index}].${config.path}`;
this.openmct.objects.mutate(
stackedPlotObject,
configPath,
config.value
);
} }
} }
}; };

View File

@ -13,8 +13,10 @@ export default function PlotsInspectorViewProvider(openmct) {
let object = selection[0][0].context.item; let object = selection[0][0].context.item;
return object const isOverlayPlotObject = object && object.type === 'telemetry.plot.overlay';
&& object.type === 'telemetry.plot.overlay'; const isStackedPlotObject = object && object.type === 'telemetry.plot.stacked';
return isStackedPlotObject || isOverlayPlotObject;
}, },
view: function (selection) { view: function (selection) {
let component; let component;

View File

@ -0,0 +1,59 @@
import PlotOptions from "./PlotOptions.vue";
import Vue from 'vue';
export default function StackedPlotsInspectorViewProvider(openmct) {
return {
key: 'stacked-plots-inspector',
name: 'Stacked Plots Inspector View',
canView: function (selection) {
if (selection.length === 0 || selection[0].length === 0) {
return false;
}
const object = selection[0][0].context.item;
const parent = selection[0].length > 1 && selection[0][1].context.item;
const isOverlayPlotObject = object && object.type === 'telemetry.plot.overlay';
const isParentStackedPlotObject = parent && parent.type === 'telemetry.plot.stacked';
return !isOverlayPlotObject && isParentStackedPlotObject;
},
view: function (selection) {
let component;
let objectPath;
if (selection.length) {
objectPath = selection[0].map((selectionItem) => {
return selectionItem.context.item;
});
}
return {
show: function (element) {
component = new Vue({
el: element,
components: {
PlotOptions: PlotOptions
},
provide: {
openmct,
domainObject: selection[0][0].context.item,
path: objectPath
},
template: '<plot-options></plot-options>'
});
},
destroy: function () {
if (component) {
component.$destroy();
component = undefined;
}
}
};
},
priority: function () {
return 1;
}
};
}

View File

@ -298,28 +298,45 @@ export default {
this.series.set('color', color); this.series.set('color', color);
const getPath = this.dynamicPathForKey('color'); if (!this.domainObject.configuration || !this.domainObject.configuration.series) {
const seriesColorPath = getPath(this.domainObject, this.series); this.$emit('seriesUpdated', {
identifier: this.domainObject.identifier,
path: `series.color`,
value: color.asHexString()
});
} else {
const getPath = this.dynamicPathForKey('color');
const seriesColorPath = getPath(this.domainObject, this.series);
this.openmct.objects.mutate( this.openmct.objects.mutate(
this.domainObject, this.domainObject,
seriesColorPath, seriesColorPath,
color.asHexString() color.asHexString()
); );
}
if (otherSeriesWithColor) { if (otherSeriesWithColor) {
otherSeriesWithColor.set('color', oldColor); otherSeriesWithColor.set('color', oldColor);
const otherSeriesColorPath = getPath( if (!this.domainObject.configuration || !this.domainObject.configuration.series) {
this.domainObject, this.$emit('seriesUpdated', {
otherSeriesWithColor identifier: this.domainObject.identifier,
); path: `series.color`,
value: oldColor.asHexString()
});
} else {
const getPath = this.dynamicPathForKey('color');
const otherSeriesColorPath = getPath(
this.domainObject,
otherSeriesWithColor
);
this.openmct.objects.mutate( this.openmct.objects.mutate(
this.domainObject, this.domainObject,
otherSeriesColorPath, otherSeriesColorPath,
oldColor.asHexString() oldColor.asHexString()
); );
}
} }
}, },
toggleExpanded() { toggleExpanded() {
@ -343,11 +360,19 @@ export default {
if (!_.isEqual(coerce(newVal, formField.coerce), coerce(oldVal, formField.coerce))) { if (!_.isEqual(coerce(newVal, formField.coerce), coerce(oldVal, formField.coerce))) {
this.series.set(formKey, coerce(newVal, formField.coerce)); this.series.set(formKey, coerce(newVal, formField.coerce));
if (path) { if (path) {
this.openmct.objects.mutate( if (!this.domainObject.configuration || !this.domainObject.configuration.series) {
this.domainObject, this.$emit('seriesUpdated', {
path(this.domainObject, this.series), identifier: this.domainObject.identifier,
coerce(newVal, formField.coerce) path: `series.${formKey}`,
); value: coerce(newVal, formField.coerce)
});
} else {
this.openmct.objects.mutate(
this.domainObject,
path(this.domainObject, this.series),
coerce(newVal, formField.coerce)
);
}
} }
} }
}, },

View File

@ -230,11 +230,19 @@ export default {
// TODO: Why do we mutate yAxis twice, once directly, once via objects.mutate? Or are they different objects? // TODO: Why do we mutate yAxis twice, once directly, once via objects.mutate? Or are they different objects?
this.yAxis.set(formKey, newVal); this.yAxis.set(formKey, newVal);
if (path) { if (path) {
this.openmct.objects.mutate( if (!this.domainObject.configuration || !this.domainObject.configuration.series) {
this.domainObject, this.$emit('seriesUpdated', {
path(this.domainObject, this.yAxis), identifier: this.domainObject.identifier,
newVal path: `yAxis.${formKey}`,
); value: newVal
});
} else {
this.openmct.objects.mutate(
this.domainObject,
path(this.domainObject, this.yAxis),
newVal
);
}
} }
} }
} }

View File

@ -49,8 +49,8 @@
title="Cursor is point locked. Click anywhere in the plot to unlock." title="Cursor is point locked. Click anywhere in the plot to unlock."
></div> ></div>
<plot-legend-item-collapsed <plot-legend-item-collapsed
v-for="seriesObject in series" v-for="(seriesObject, seriesIndex) in series"
:key="seriesObject.keyString" :key="`seriesObject.keyString-${seriesIndex}`"
:highlights="highlights" :highlights="highlights"
:value-to-show-when-collapsed="legend.get('valueToShowWhenCollapsed')" :value-to-show-when-collapsed="legend.get('valueToShowWhenCollapsed')"
:series-object="seriesObject" :series-object="seriesObject"
@ -95,8 +95,8 @@
</thead> </thead>
<tbody> <tbody>
<plot-legend-item-expanded <plot-legend-item-expanded
v-for="seriesObject in series" v-for="(seriesObject, seriesIndex) in series"
:key="seriesObject.keyString" :key="`seriesObject.keyString-${seriesIndex}`"
:series-object="seriesObject" :series-object="seriesObject"
:highlights="highlights" :highlights="highlights"
:legend="legend" :legend="legend"

View File

@ -41,7 +41,7 @@
<span class="plot-series-name">{{ nameWithUnit }}</span> <span class="plot-series-name">{{ nameWithUnit }}</span>
</div> </div>
<div <div
v-show="!!highlights.length && (valueToShowWhenCollapsed !== 'none')" v-show="!!highlights.length && (valueToShowWhenCollapsed !== 'none' && valueToShowWhenCollapsed !== 'units')"
class="plot-series-value hover-value-enabled" class="plot-series-value hover-value-enabled"
:class="[{ 'cursor-hover': notNearest }, valueToDisplayWhenCollapsedClass, mctLimitStateClass]" :class="[{ 'cursor-hover': notNearest }, valueToDisplayWhenCollapsedClass, mctLimitStateClass]"
> >

View File

@ -26,6 +26,7 @@ import PlotsInspectorViewProvider from './inspector/PlotsInspectorViewProvider';
import OverlayPlotCompositionPolicy from './overlayPlot/OverlayPlotCompositionPolicy'; import OverlayPlotCompositionPolicy from './overlayPlot/OverlayPlotCompositionPolicy';
import StackedPlotCompositionPolicy from './stackedPlot/StackedPlotCompositionPolicy'; import StackedPlotCompositionPolicy from './stackedPlot/StackedPlotCompositionPolicy';
import PlotViewActions from "./actions/ViewActions"; import PlotViewActions from "./actions/ViewActions";
import StackedPlotsInspectorViewProvider from "./inspector/StackedPlotsInspectorViewProvider";
export default function () { export default function () {
return function install(openmct) { return function install(openmct) {
@ -39,9 +40,8 @@ export default function () {
initialize: function (domainObject) { initialize: function (domainObject) {
domainObject.composition = []; domainObject.composition = [];
domainObject.configuration = { domainObject.configuration = {
series: [], //series is an array of objects of type: {identifier, series: {color...}, yAxis:{}}
yAxis: {}, series: []
xAxis: {}
}; };
}, },
priority: 891 priority: 891
@ -55,7 +55,11 @@ export default function () {
creatable: true, creatable: true,
initialize: function (domainObject) { initialize: function (domainObject) {
domainObject.composition = []; domainObject.composition = [];
domainObject.configuration = {}; domainObject.configuration = {
series: [],
yAxis: {},
xAxis: {}
};
}, },
priority: 890 priority: 890
}); });
@ -65,6 +69,7 @@ export default function () {
openmct.objectViews.addProvider(new PlotViewProvider(openmct)); openmct.objectViews.addProvider(new PlotViewProvider(openmct));
openmct.inspectorViews.addProvider(new PlotsInspectorViewProvider(openmct)); openmct.inspectorViews.addProvider(new PlotsInspectorViewProvider(openmct));
openmct.inspectorViews.addProvider(new StackedPlotsInspectorViewProvider(openmct));
openmct.composition.addPolicy(new OverlayPlotCompositionPolicy(openmct).allow); openmct.composition.addPolicy(new OverlayPlotCompositionPolicy(openmct).allow);
openmct.composition.addPolicy(new StackedPlotCompositionPolicy(openmct).allow); openmct.composition.addPolicy(new StackedPlotCompositionPolicy(openmct).allow);

View File

@ -23,7 +23,6 @@
import {createMouseEvent, createOpenMct, resetApplicationState, spyOnBuiltins} from "utils/testing"; import {createMouseEvent, createOpenMct, resetApplicationState, spyOnBuiltins} from "utils/testing";
import PlotVuePlugin from "./plugin"; import PlotVuePlugin from "./plugin";
import Vue from "vue"; import Vue from "vue";
import StackedPlot from "./stackedPlot/StackedPlot.vue";
import configStore from "./configuration/ConfigStore"; import configStore from "./configuration/ConfigStore";
import EventEmitter from "EventEmitter"; import EventEmitter from "EventEmitter";
import PlotOptions from "./inspector/PlotOptions.vue"; import PlotOptions from "./inspector/PlotOptions.vue";
@ -348,14 +347,20 @@ describe("the plugin", function () {
} }
}; };
openmct.router.path = [testTelemetryObject];
applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath); applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath);
plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === "plot-single"); plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === "plot-single");
plotView = plotViewProvider.view(testTelemetryObject, [testTelemetryObject]); plotView = plotViewProvider.view(testTelemetryObject, []);
plotView.show(child, true); plotView.show(child, true);
return Vue.nextTick(); return Vue.nextTick();
}); });
afterEach(() => {
openmct.router.path = null;
});
it("Makes only one request for telemetry on load", () => { it("Makes only one request for telemetry on load", () => {
expect(openmct.telemetry.request).toHaveBeenCalledTimes(1); expect(openmct.telemetry.request).toHaveBeenCalledTimes(1);
}); });
@ -523,360 +528,6 @@ describe("the plugin", function () {
}); });
}); });
describe("The stacked plot view", () => {
let testTelemetryObject;
let testTelemetryObject2;
let config;
let stackedPlotObject;
let component;
let mockComposition;
let plotViewComponentObject;
beforeEach(() => {
stackedPlotObject = {
identifier: {
namespace: "",
key: "test-plot"
},
type: "telemetry.plot.stacked",
name: "Test Stacked Plot"
};
testTelemetryObject = {
identifier: {
namespace: "",
key: "test-object"
},
type: "test-object",
name: "Test Object",
telemetry: {
values: [{
key: "utc",
format: "utc",
name: "Time",
hints: {
domain: 1
}
}, {
key: "some-key",
name: "Some attribute",
hints: {
range: 1
}
}, {
key: "some-other-key",
name: "Another attribute",
hints: {
range: 2
}
}]
},
configuration: {
objectStyles: {
staticStyle: {
style: {
backgroundColor: 'rgb(0, 200, 0)',
color: '',
border: ''
}
},
conditionSetIdentifier: {
namespace: '',
key: 'testConditionSetId'
},
selectedConditionId: 'conditionId1',
defaultConditionId: 'conditionId1',
styles: [
{
conditionId: 'conditionId1',
style: {
backgroundColor: 'rgb(0, 155, 0)',
color: '',
output: '',
border: ''
}
}
]
}
}
};
testTelemetryObject2 = {
identifier: {
namespace: "",
key: "test-object2"
},
type: "test-object",
name: "Test Object2",
telemetry: {
values: [{
key: "utc",
format: "utc",
name: "Time",
hints: {
domain: 1
}
}, {
key: "some-key2",
name: "Some attribute2",
hints: {
range: 1
}
}, {
key: "some-other-key2",
name: "Another attribute2",
hints: {
range: 2
}
}]
}
};
mockComposition = new EventEmitter();
mockComposition.load = () => {
mockComposition.emit('add', testTelemetryObject);
return [testTelemetryObject];
};
spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
let viewContainer = document.createElement("div");
child.append(viewContainer);
component = new Vue({
el: viewContainer,
components: {
StackedPlot
},
provide: {
openmct: openmct,
domainObject: stackedPlotObject,
composition: openmct.composition.get(stackedPlotObject),
path: [stackedPlotObject]
},
template: "<stacked-plot></stacked-plot>"
});
return telemetryPromise
.then(Vue.nextTick())
.then(() => {
plotViewComponentObject = component.$root.$children[0];
const configId = openmct.objects.makeKeyString(testTelemetryObject.identifier);
config = configStore.get(configId);
});
});
it("Renders a collapsed legend for every telemetry", () => {
let legend = element.querySelectorAll(".plot-wrapper-collapsed-legend .plot-series-name");
expect(legend.length).toBe(1);
expect(legend[0].innerHTML).toEqual("Test Object");
});
it("Renders an expanded legend for every telemetry", () => {
let legendControl = element.querySelector(".c-plot-legend__view-control.gl-plot-legend__view-control.c-disclosure-triangle");
const clickEvent = createMouseEvent("click");
legendControl.dispatchEvent(clickEvent);
let legend = element.querySelectorAll(".plot-wrapper-expanded-legend .plot-legend-item td");
expect(legend.length).toBe(6);
});
it("Renders X-axis ticks for the telemetry object", (done) => {
let xAxisElement = element.querySelectorAll(".gl-plot-axis-area.gl-plot-x .gl-plot-tick-wrapper");
expect(xAxisElement.length).toBe(1);
config.xAxis.set('displayRange', {
min: 0,
max: 4
});
Vue.nextTick(() => {
let ticks = xAxisElement[0].querySelectorAll(".gl-plot-tick");
expect(ticks.length).toBe(9);
done();
});
});
it("Renders Y-axis ticks for the telemetry object", (done) => {
config.yAxis.set('displayRange', {
min: 10,
max: 20
});
Vue.nextTick(() => {
let yAxisElement = element.querySelectorAll(".gl-plot-axis-area.gl-plot-y .gl-plot-tick-wrapper");
expect(yAxisElement.length).toBe(1);
let ticks = yAxisElement[0].querySelectorAll(".gl-plot-tick");
expect(ticks.length).toBe(6);
done();
});
});
it("Renders Y-axis options for the telemetry object", () => {
let yAxisElement = element.querySelectorAll(".gl-plot-axis-area.gl-plot-y .gl-plot-y-label__select");
expect(yAxisElement.length).toBe(1);
let options = yAxisElement[0].querySelectorAll("option");
expect(options.length).toBe(2);
expect(options[0].value).toBe("Some attribute");
expect(options[1].value).toBe("Another attribute");
});
it("turns on cursor Guides all telemetry objects", (done) => {
expect(plotViewComponentObject.$children[0].cursorGuide).toBeFalse();
plotViewComponentObject.$children[0].cursorGuide = true;
Vue.nextTick(() => {
expect(plotViewComponentObject.$children[0].cursorGuide).toBeTrue();
done();
});
});
it("shows grid lines for all telemetry objects", () => {
expect(plotViewComponentObject.$children[0].gridLines).toBeTrue();
let gridLinesContainer = element.querySelectorAll(".gl-plot-display-area .js-ticks");
let visible = 0;
gridLinesContainer.forEach(el => {
if (el.style.display !== "none") {
visible++;
}
});
expect(visible).toBe(2);
});
it("hides grid lines for all telemetry objects", (done) => {
expect(plotViewComponentObject.$children[0].gridLines).toBeTrue();
plotViewComponentObject.$children[0].gridLines = false;
Vue.nextTick(() => {
expect(plotViewComponentObject.$children[0].gridLines).toBeFalse();
let gridLinesContainer = element.querySelectorAll(".gl-plot-display-area .js-ticks");
let visible = 0;
gridLinesContainer.forEach(el => {
if (el.style.display !== "none") {
visible++;
}
});
expect(visible).toBe(0);
done();
});
});
it('plots a new series when a new telemetry object is added', (done) => {
mockComposition.emit('add', testTelemetryObject2);
Vue.nextTick(() => {
let legend = element.querySelectorAll(".plot-wrapper-collapsed-legend .plot-series-name");
expect(legend.length).toBe(2);
expect(legend[1].innerHTML).toEqual("Test Object2");
done();
});
});
it('removes plots from series when a telemetry object is removed', (done) => {
mockComposition.emit('remove', testTelemetryObject.identifier);
Vue.nextTick(() => {
let legend = element.querySelectorAll(".plot-wrapper-collapsed-legend .plot-series-name");
expect(legend.length).toBe(0);
done();
});
});
it("Changes the label of the y axis when the option changes", (done) => {
let selectEl = element.querySelector('.gl-plot-y-label__select');
selectEl.value = 'Another attribute';
selectEl.dispatchEvent(new Event("change"));
Vue.nextTick(() => {
expect(config.yAxis.get('label')).toEqual('Another attribute');
done();
});
});
it("Renders a new series when added to one of the plots", (done) => {
mockComposition.emit('add', testTelemetryObject2);
Vue.nextTick(() => {
let legend = element.querySelectorAll(".plot-wrapper-collapsed-legend .plot-series-name");
expect(legend.length).toBe(2);
expect(legend[1].innerHTML).toEqual("Test Object2");
done();
});
});
it("Adds a new point to the plot", (done) => {
let originalLength = config.series.models[0].getSeriesData().length;
config.series.models[0].add({
utc: 2,
'some-key': 1,
'some-other-key': 2
});
Vue.nextTick(() => {
const seriesData = config.series.models[0].getSeriesData();
expect(seriesData.length).toEqual(originalLength + 1);
done();
});
});
it("updates the xscale", (done) => {
config.xAxis.set('displayRange', {
min: 0,
max: 10
});
Vue.nextTick(() => {
expect(plotViewComponentObject.$children[0].component.$children[0].xScale.domain()).toEqual({
min: 0,
max: 10
});
done();
});
});
it("updates the yscale", (done) => {
config.yAxis.set('displayRange', {
min: 10,
max: 20
});
Vue.nextTick(() => {
expect(plotViewComponentObject.$children[0].component.$children[0].yScale.domain()).toEqual({
min: 10,
max: 20
});
done();
});
});
it("shows styles for telemetry objects if available", (done) => {
Vue.nextTick(() => {
let conditionalStylesContainer = element.querySelectorAll(".c-plot--stacked-container .js-style-receiver");
let hasStyles = 0;
conditionalStylesContainer.forEach(el => {
if (el.style.backgroundColor !== '') {
hasStyles++;
}
});
expect(hasStyles).toBe(1);
done();
});
});
describe('limits', () => {
it('lines are not displayed by default', () => {
let limitEl = element.querySelectorAll(".js-limit-area hr");
expect(limitEl.length).toBe(0);
});
it('lines are displayed when configuration is set to true', (done) => {
config.series.models[0].set('limitLines', true);
Vue.nextTick(() => {
let limitEl = element.querySelectorAll(".js-limit-area .js-limit-line");
expect(limitEl.length).toBe(4);
done();
});
});
});
});
describe('the inspector view', () => { describe('the inspector view', () => {
let component; let component;
let viewComponentObject; let viewComponentObject;
@ -955,6 +606,7 @@ describe("the plugin", function () {
] ]
]; ];
openmct.router.path = [testTelemetryObject];
mockComposition = new EventEmitter(); mockComposition = new EventEmitter();
mockComposition.load = () => { mockComposition.load = () => {
mockComposition.emit('add', testTelemetryObject); mockComposition.emit('add', testTelemetryObject);
@ -993,6 +645,10 @@ describe("the plugin", function () {
}); });
}); });
afterEach(() => {
openmct.router.path = null;
});
describe('in view only mode', () => { describe('in view only mode', () => {
let browseOptionsEl; let browseOptionsEl;
let editOptionsEl; let editOptionsEl;
@ -1096,5 +752,24 @@ describe("the plugin", function () {
expect(colorSwatch).toBeDefined(); expect(colorSwatch).toBeDefined();
}); });
}); });
describe('limits', () => {
it('lines are not displayed by default', () => {
let limitEl = element.querySelectorAll(".js-limit-area .js-limit-line");
expect(limitEl.length).toBe(0);
});
xit('lines are displayed when configuration is set to true', (done) => {
config.series.models[0].set('limitLines', true);
Vue.nextTick(() => {
let limitEl = element.querySelectorAll(".js-limit-area .js-limit-line");
expect(limitEl.length).toBe(4);
done();
});
});
});
}); });
}); });

View File

@ -21,21 +21,37 @@
--> -->
<template> <template>
<div class="c-plot c-plot--stacked holder holder-plot has-control-bar"> <div
v-if="loaded"
class="c-plot c-plot--stacked holder holder-plot has-control-bar"
:class="[plotLegendExpandedStateClass, plotLegendPositionClass]"
>
<plot-legend
:cursor-locked="!!lockHighlightPoint"
:series="seriesModels"
:highlights="highlights"
:legend="legend"
@legendHoverChanged="legendHoverChanged"
/>
<div class="l-view-section"> <div class="l-view-section">
<stacked-plot-item <stacked-plot-item
v-for="object in compositionObjects" v-for="object in compositionObjects"
:key="object.id" :key="object.id"
class="c-plot--stacked-container" class="c-plot--stacked-container"
:object="object" :child-object="object"
:options="options" :options="options"
:grid-lines="gridLines" :grid-lines="gridLines"
:color-palette="colorPalette"
:cursor-guide="cursorGuide" :cursor-guide="cursorGuide"
:show-limit-line-labels="showLimitLineLabels"
:plot-tick-width="maxTickWidth" :plot-tick-width="maxTickWidth"
@plotTickWidth="onTickWidthChange" @plotTickWidth="onTickWidthChange"
@loadingUpdated="loadingUpdated" @loadingUpdated="loadingUpdated"
@cursorGuide="onCursorGuideChange" @cursorGuide="onCursorGuideChange"
@gridLines="onGridLinesChange" @gridLines="onGridLinesChange"
@lockHighlightPoint="lockHighlightPointUpdated"
@highlights="highlightsUpdated"
@configLoaded="registerSeriesListeners"
/> />
</div> </div>
</div> </div>
@ -43,12 +59,19 @@
<script> <script>
import PlotConfigurationModel from '../configuration/PlotConfigurationModel';
import configStore from '../configuration/ConfigStore';
import ColorPalette from "@/ui/color/ColorPalette";
import PlotLegend from "../legend/PlotLegend.vue";
import StackedPlotItem from './StackedPlotItem.vue'; import StackedPlotItem from './StackedPlotItem.vue';
import ImageExporter from '../../../exporters/ImageExporter'; import ImageExporter from '../../../exporters/ImageExporter';
import eventHelpers from "@/plugins/plot/lib/eventHelpers";
export default { export default {
components: { components: {
StackedPlotItem StackedPlotItem,
PlotLegend
}, },
inject: ['openmct', 'domainObject', 'composition', 'path'], inject: ['openmct', 'domainObject', 'composition', 'path'],
props: { props: {
@ -60,16 +83,35 @@ export default {
} }
}, },
data() { data() {
this.seriesConfig = {};
return { return {
hideExportButtons: false, hideExportButtons: false,
cursorGuide: false, cursorGuide: false,
gridLines: true, gridLines: true,
loading: false, loading: false,
compositionObjects: [], compositionObjects: [],
tickWidthMap: {} tickWidthMap: {},
legend: {},
loaded: false,
lockHighlightPoint: false,
highlights: [],
seriesModels: [],
showLimitLineLabels: undefined,
colorPalette: new ColorPalette()
}; };
}, },
computed: { computed: {
plotLegendPositionClass() {
return `plot-legend-${this.config.legend.get('position')}`;
},
plotLegendExpandedStateClass() {
if (this.config.legend.get('expanded')) {
return 'plot-legend-expanded';
} else {
return 'plot-legend-collapsed';
}
},
maxTickWidth() { maxTickWidth() {
return Math.max(...Object.values(this.tickWidthMap)); return Math.max(...Object.values(this.tickWidthMap));
} }
@ -78,6 +120,13 @@ export default {
this.destroy(); this.destroy();
}, },
mounted() { mounted() {
eventHelpers.extend(this);
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.config = this.getConfig(configId);
this.legend = this.config.legend;
this.loaded = true;
this.imageExporter = new ImageExporter(this.openmct); this.imageExporter = new ImageExporter(this.openmct);
this.composition.on('add', this.addChild); this.composition.on('add', this.addChild);
@ -86,10 +135,29 @@ export default {
this.composition.load(); this.composition.load();
}, },
methods: { methods: {
getConfig(configId) {
let config = configStore.get(configId);
if (!config) {
config = new PlotConfigurationModel({
id: configId,
domainObject: this.domainObject,
openmct: this.openmct,
callback: (data) => {
this.data = data;
}
});
configStore.add(configId, config);
}
return config;
},
loadingUpdated(loaded) { loadingUpdated(loaded) {
this.loading = loaded; this.loading = loaded;
}, },
destroy() { destroy() {
this.stopListening();
configStore.deleteStore(this.config.id);
this.composition.off('add', this.addChild); this.composition.off('add', this.addChild);
this.composition.off('remove', this.removeChild); this.composition.off('remove', this.removeChild);
this.composition.off('reorder', this.compositionReorder); this.composition.off('reorder', this.compositionReorder);
@ -99,6 +167,19 @@ export default {
const id = this.openmct.objects.makeKeyString(child.identifier); const id = this.openmct.objects.makeKeyString(child.identifier);
this.$set(this.tickWidthMap, id, 0); this.$set(this.tickWidthMap, id, 0);
const persistedConfig = this.domainObject.configuration.series && this.domainObject.configuration.series.find((seriesConfig) => {
return this.openmct.objects.areIdsEqual(seriesConfig.identifier, child.identifier);
});
if (persistedConfig === undefined) {
this.openmct.objects.mutate(
this.domainObject,
'configuration.series[' + this.compositionObjects.length + ']',
{
identifier: child.identifier
}
);
}
this.compositionObjects.push(child); this.compositionObjects.push(child);
}, },
@ -107,6 +188,13 @@ export default {
this.$delete(this.tickWidthMap, id); this.$delete(this.tickWidthMap, id);
const configIndex = this.domainObject.configuration.series.findIndex((seriesConfig) => {
return this.openmct.objects.areIdsEqual(seriesConfig.identifier, childIdentifier);
});
if (configIndex > -1) {
this.domainObject.configuration.series.splice(configIndex, 1);
}
const childObj = this.compositionObjects.filter((c) => { const childObj = this.compositionObjects.filter((c) => {
const identifier = this.openmct.objects.makeKeyString(c.identifier); const identifier = this.openmct.objects.makeKeyString(c.identifier);
@ -158,6 +246,34 @@ export default {
this.$set(this.tickWidthMap, plotId, width); this.$set(this.tickWidthMap, plotId, width);
}, },
legendHoverChanged(data) {
this.showLimitLineLabels = data;
},
lockHighlightPointUpdated(data) {
this.lockHighlightPoint = data;
},
highlightsUpdated(data) {
this.highlights = data;
},
registerSeriesListeners(configId) {
this.seriesConfig[configId] = this.getConfig(configId);
this.listenTo(this.seriesConfig[configId].series, 'add', this.addSeries, this);
this.listenTo(this.seriesConfig[configId].series, 'remove', this.removeSeries, this);
this.seriesConfig[configId].series.models.forEach(this.addSeries, this);
},
addSeries(series) {
const index = this.seriesModels.length;
this.$set(this.seriesModels, index, series);
},
removeSeries(plotSeries) {
const index = this.seriesModels.findIndex(seriesModel => this.openmct.objects.areIdsEqual(seriesModel.identifier, plotSeries.identifier));
if (index > -1) {
this.$delete(this.seriesModels, index);
}
this.stopListening(plotSeries);
},
onCursorGuideChange(cursorGuide) { onCursorGuideChange(cursorGuide) {
this.cursorGuide = cursorGuide === true; this.cursorGuide = cursorGuide === true;
}, },

View File

@ -27,12 +27,14 @@
import MctPlot from '../MctPlot.vue'; import MctPlot from '../MctPlot.vue';
import Vue from "vue"; import Vue from "vue";
import conditionalStylesMixin from "./mixins/objectStyles-mixin"; import conditionalStylesMixin from "./mixins/objectStyles-mixin";
import configStore from "@/plugins/plot/configuration/ConfigStore";
import PlotConfigurationModel from "@/plugins/plot/configuration/PlotConfigurationModel";
export default { export default {
mixins: [conditionalStylesMixin], mixins: [conditionalStylesMixin],
inject: ['openmct', 'domainObject', 'path'], inject: ['openmct', 'domainObject', 'path'],
props: { props: {
object: { childObject: {
type: Object, type: Object,
default() { default() {
return {}; return {};
@ -56,6 +58,18 @@ export default {
return true; return true;
} }
}, },
showLimitLineLabels: {
type: Object,
default() {
return {};
}
},
colorPalette: {
type: Object,
default() {
return undefined;
}
},
plotTickWidth: { plotTickWidth: {
type: Number, type: Number,
default() { default() {
@ -72,12 +86,22 @@ export default {
}, },
plotTickWidth(width) { plotTickWidth(width) {
this.updateComponentProp('plotTickWidth', width); this.updateComponentProp('plotTickWidth', width);
},
showLimitLineLabels: {
handler(data) {
this.updateComponentProp('limitLineLabels', data);
},
deep: true
} }
}, },
mounted() { mounted() {
this.updateView(); this.updateView();
}, },
beforeDestroy() { beforeDestroy() {
if (this.removeSelectable) {
this.removeSelectable();
}
if (this.component) { if (this.component) {
this.component.$destroy(); this.component.$destroy();
} }
@ -96,15 +120,19 @@ export default {
} }
const onTickWidthChange = this.onTickWidthChange; const onTickWidthChange = this.onTickWidthChange;
const onLockHighlightPointUpdated = this.onLockHighlightPointUpdated;
const onHighlightsUpdated = this.onHighlightsUpdated;
const onConfigLoaded = this.onConfigLoaded;
const onCursorGuideChange = this.onCursorGuideChange; const onCursorGuideChange = this.onCursorGuideChange;
const onGridLinesChange = this.onGridLinesChange; const onGridLinesChange = this.onGridLinesChange;
const loadingUpdated = this.loadingUpdated; const loadingUpdated = this.loadingUpdated;
const setStatus = this.setStatus; const setStatus = this.setStatus;
const openmct = this.openmct; const openmct = this.openmct;
const object = this.object;
const path = this.path; const path = this.path;
//If this object is not persistable, then package it with it's parent
const object = this.getPlotObject();
const getProps = this.getProps; const getProps = this.getProps;
let viewContainer = document.createElement('div'); let viewContainer = document.createElement('div');
this.$el.append(viewContainer); this.$el.append(viewContainer);
@ -123,14 +151,28 @@ export default {
return { return {
...getProps(), ...getProps(),
onTickWidthChange, onTickWidthChange,
onLockHighlightPointUpdated,
onHighlightsUpdated,
onConfigLoaded,
onCursorGuideChange, onCursorGuideChange,
onGridLinesChange, onGridLinesChange,
loadingUpdated, loadingUpdated,
setStatus setStatus
}; };
}, },
template: '<div ref="plotWrapper" class="l-view-section u-style-receiver js-style-receiver" :class="{\'s-status-timeconductor-unsynced\': status && status === \'timeconductor-unsynced\'}"><div v-show="!!loading" class="c-loading--overlay loading"></div><mct-plot :init-grid-lines="gridLines" :init-cursor-guide="cursorGuide" :plot-tick-width="plotTickWidth" :options="options" @plotTickWidth="onTickWidthChange" @cursorGuide="onCursorGuideChange" @gridLines="onGridLinesChange" @statusUpdated="setStatus" @loadingUpdated="loadingUpdated"/></div>' template: '<div ref="plotWrapper" class="l-view-section u-style-receiver js-style-receiver" :class="{\'s-status-timeconductor-unsynced\': status && status === \'timeconductor-unsynced\'}"><div v-show="!!loading" class="c-loading--overlay loading"></div><mct-plot :init-grid-lines="gridLines" :init-cursor-guide="cursorGuide" :plot-tick-width="plotTickWidth" :limit-line-labels="limitLineLabels" :color-palette="colorPalette" :options="options" @plotTickWidth="onTickWidthChange" @lockHighlightPoint="onLockHighlightPointUpdated" @highlights="onHighlightsUpdated" @configLoaded="onConfigLoaded" @cursorGuide="onCursorGuideChange" @gridLines="onGridLinesChange" @statusUpdated="setStatus" @loadingUpdated="loadingUpdated"/></div>'
}); });
this.setSelection();
},
onLockHighlightPointUpdated() {
this.$emit('lockHighlightPoint', ...arguments);
},
onHighlightsUpdated() {
this.$emit('highlights', ...arguments);
},
onConfigLoaded() {
this.$emit('configLoaded', ...arguments);
}, },
onTickWidthChange() { onTickWidthChange() {
this.$emit('plotTickWidth', ...arguments); this.$emit('plotTickWidth', ...arguments);
@ -145,19 +187,73 @@ export default {
this.status = status; this.status = status;
this.updateComponentProp('status', status); this.updateComponentProp('status', status);
}, },
setSelection() {
let childContext = {};
childContext.item = this.childObject;
this.context = childContext;
if (this.removeSelectable) {
this.removeSelectable();
}
this.removeSelectable = this.openmct.selection.selectable(
this.$el, this.context);
},
loadingUpdated(loaded) { loadingUpdated(loaded) {
this.loading = loaded; this.loading = loaded;
this.updateComponentProp('loading', loaded); this.updateComponentProp('loading', loaded);
}, },
getProps() { getProps() {
return { return {
limitLineLabels: this.showLimitLineLabels,
gridLines: this.gridLines, gridLines: this.gridLines,
cursorGuide: this.cursorGuide, cursorGuide: this.cursorGuide,
plotTickWidth: this.plotTickWidth, plotTickWidth: this.plotTickWidth,
loading: this.loading, loading: this.loading,
options: this.options, options: this.options,
status: this.status status: this.status,
colorPalette: this.colorPalette
}; };
},
getPlotObject() {
if (this.childObject.configuration && this.childObject.configuration.series) {
//If the object has a configuration, allow initialization of the config from it's persisted config
return this.childObject;
} else {
// If the object does not have configuration, initialize the series config with the persisted config from the stacked plot
const configId = this.openmct.objects.makeKeyString(this.childObject.identifier);
let config = configStore.get(configId);
if (!config) {
const persistedConfig = this.domainObject.configuration.series.find((seriesConfig) => {
return this.openmct.objects.areIdsEqual(seriesConfig.identifier, this.childObject.identifier);
});
if (persistedConfig) {
config = new PlotConfigurationModel({
id: configId,
domainObject: {
...this.childObject,
configuration: {
series: [
{
identifier: this.childObject.identifier,
...persistedConfig.series
}
],
yAxis: persistedConfig.yAxis
}
},
openmct: this.openmct,
palette: this.colorPalette,
callback: (data) => {
this.data = data;
}
});
configStore.add(configId, config);
}
}
return this.childObject;
}
} }
} }
}; };

View File

@ -31,7 +31,7 @@ export default {
}; };
}, },
mounted() { mounted() {
this.objectStyles = this.getObjectStyleForItem(this.object.configuration); this.objectStyles = this.getObjectStyleForItem(this.childObject.configuration);
this.initObjectStyles(); this.initObjectStyles();
}, },
beforeDestroy() { beforeDestroy() {
@ -62,18 +62,18 @@ export default {
this.stopListeningStyles(); this.stopListeningStyles();
} }
this.stopListeningStyles = this.openmct.objects.observe(this.object, 'configuration.objectStyles', (newObjectStyle) => { this.stopListeningStyles = this.openmct.objects.observe(this.childObject, 'configuration.objectStyles', (newObjectStyle) => {
//Updating styles in the inspector view will trigger this so that the changes are reflected immediately //Updating styles in the inspector view will trigger this so that the changes are reflected immediately
this.styleRuleManager.updateObjectStyleConfig(newObjectStyle); this.styleRuleManager.updateObjectStyleConfig(newObjectStyle);
}); });
if (this.object && this.object.configuration && this.object.configuration.fontStyle) { if (this.childObject && this.childObject.configuration && this.childObject.configuration.fontStyle) {
const { fontSize, font } = this.object.configuration.fontStyle; const { fontSize, font } = this.childObject.configuration.fontStyle;
this.setFontSize(fontSize); this.setFontSize(fontSize);
this.setFont(font); this.setFont(font);
} }
this.stopListeningFontStyles = this.openmct.objects.observe(this.object, 'configuration.fontStyle', (newFontStyle) => { this.stopListeningFontStyles = this.openmct.objects.observe(this.childObject, 'configuration.fontStyle', (newFontStyle) => {
this.setFontSize(newFontStyle.fontSize); this.setFontSize(newFontStyle.fontSize);
this.setFont(newFontStyle.font); this.setFont(newFontStyle.font);
}); });

View File

@ -0,0 +1,771 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, 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 {createMouseEvent, createOpenMct, resetApplicationState, spyOnBuiltins} from "utils/testing";
import PlotVuePlugin from "../plugin";
import Vue from "vue";
import StackedPlot from "./StackedPlot.vue";
import configStore from "../configuration/ConfigStore";
import EventEmitter from "EventEmitter";
import PlotConfigurationModel from "../configuration/PlotConfigurationModel";
import PlotOptions from "../inspector/PlotOptions.vue";
describe("the plugin", function () {
let element;
let child;
let openmct;
let telemetryPromise;
let telemetryPromiseResolve;
let mockObjectPath;
let stackedPlotObject = {
identifier: {
namespace: "",
key: "test-plot"
},
type: "telemetry.plot.stacked",
name: "Test Stacked Plot",
configuration: {
series: []
}
};
beforeEach((done) => {
mockObjectPath = [
{
name: 'mock folder',
type: 'fake-folder',
identifier: {
key: 'mock-folder',
namespace: ''
}
},
{
name: 'mock parent folder',
type: 'time-strip',
identifier: {
key: 'mock-parent-folder',
namespace: ''
}
}
];
const testTelemetry = [
{
'utc': 1,
'some-key': 'some-value 1',
'some-other-key': 'some-other-value 1'
},
{
'utc': 2,
'some-key': 'some-value 2',
'some-other-key': 'some-other-value 2'
},
{
'utc': 3,
'some-key': 'some-value 3',
'some-other-key': 'some-other-value 3'
}
];
const timeSystem = {
timeSystemKey: 'utc',
bounds: {
start: 0,
end: 4
}
};
openmct = createOpenMct(timeSystem);
telemetryPromise = new Promise((resolve) => {
telemetryPromiseResolve = resolve;
});
spyOn(openmct.telemetry, 'request').and.callFake(() => {
telemetryPromiseResolve(testTelemetry);
return telemetryPromise;
});
openmct.install(new PlotVuePlugin());
element = document.createElement("div");
element.style.width = "640px";
element.style.height = "480px";
child = document.createElement("div");
child.style.width = "640px";
child.style.height = "480px";
element.appendChild(child);
document.body.appendChild(element);
spyOn(window, 'ResizeObserver').and.returnValue({
observe() {},
unobserve() {},
disconnect() {}
});
openmct.types.addType("test-object", {
creatable: true
});
spyOnBuiltins(["requestAnimationFrame"]);
window.requestAnimationFrame.and.callFake((callBack) => {
callBack();
});
openmct.router.path = [stackedPlotObject];
openmct.on("start", done);
openmct.startHeadless();
});
afterEach((done) => {
openmct.time.timeSystem('utc', {
start: 0,
end: 1
});
configStore.deleteAll();
resetApplicationState(openmct).then(done).catch(done);
});
afterAll(() => {
openmct.router.path = null;
});
describe("the plot views", () => {
it("provides a stacked plot view for objects with telemetry", () => {
const testTelemetryObject = {
id: "test-object",
type: "telemetry.plot.stacked",
telemetry: {
values: [{
key: "some-key"
}]
}
};
const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath);
let plotView = applicableViews.find((viewProvider) => viewProvider.key === "plot-stacked");
expect(plotView).toBeDefined();
});
});
describe("The stacked plot view", () => {
let testTelemetryObject;
let testTelemetryObject2;
let config;
let component;
let mockComposition;
let plotViewComponentObject;
afterAll(() => {
openmct.router.path = null;
});
beforeEach(() => {
testTelemetryObject = {
identifier: {
namespace: "",
key: "test-object"
},
type: "test-object",
name: "Test Object",
telemetry: {
values: [{
key: "utc",
format: "utc",
name: "Time",
hints: {
domain: 1
}
}, {
key: "some-key",
name: "Some attribute",
hints: {
range: 1
}
}, {
key: "some-other-key",
name: "Another attribute",
hints: {
range: 2
}
}]
},
configuration: {
objectStyles: {
staticStyle: {
style: {
backgroundColor: 'rgb(0, 200, 0)',
color: '',
border: ''
}
},
conditionSetIdentifier: {
namespace: '',
key: 'testConditionSetId'
},
selectedConditionId: 'conditionId1',
defaultConditionId: 'conditionId1',
styles: [
{
conditionId: 'conditionId1',
style: {
backgroundColor: 'rgb(0, 155, 0)',
color: '',
output: '',
border: ''
}
}
]
}
}
};
testTelemetryObject2 = {
identifier: {
namespace: "",
key: "test-object2"
},
type: "test-object",
name: "Test Object2",
telemetry: {
values: [{
key: "utc",
format: "utc",
name: "Time",
hints: {
domain: 1
}
}, {
key: "some-key2",
name: "Some attribute2",
hints: {
range: 1
}
}, {
key: "some-other-key2",
name: "Another attribute2",
hints: {
range: 2
}
}]
}
};
mockComposition = new EventEmitter();
mockComposition.load = () => {
mockComposition.emit('add', testTelemetryObject);
return [testTelemetryObject];
};
spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
let viewContainer = document.createElement("div");
child.append(viewContainer);
component = new Vue({
el: viewContainer,
components: {
StackedPlot
},
provide: {
openmct: openmct,
domainObject: stackedPlotObject,
composition: openmct.composition.get(stackedPlotObject),
path: [stackedPlotObject]
},
template: "<stacked-plot></stacked-plot>"
});
return telemetryPromise
.then(Vue.nextTick())
.then(() => {
plotViewComponentObject = component.$root.$children[0];
const configId = openmct.objects.makeKeyString(testTelemetryObject.identifier);
config = configStore.get(configId);
});
});
it("Renders a collapsed legend for every telemetry", () => {
let legend = element.querySelectorAll(".plot-wrapper-collapsed-legend .plot-series-name");
expect(legend.length).toBe(1);
expect(legend[0].innerHTML).toEqual("Test Object");
});
it("Renders an expanded legend for every telemetry", () => {
let legendControl = element.querySelector(".c-plot-legend__view-control.gl-plot-legend__view-control.c-disclosure-triangle");
const clickEvent = createMouseEvent("click");
legendControl.dispatchEvent(clickEvent);
let legend = element.querySelectorAll(".plot-wrapper-expanded-legend .plot-legend-item td");
expect(legend.length).toBe(6);
});
it("Renders X-axis ticks for the telemetry object", (done) => {
let xAxisElement = element.querySelectorAll(".gl-plot-axis-area.gl-plot-x .gl-plot-tick-wrapper");
expect(xAxisElement.length).toBe(1);
config.xAxis.set('displayRange', {
min: 0,
max: 4
});
Vue.nextTick(() => {
let ticks = xAxisElement[0].querySelectorAll(".gl-plot-tick");
expect(ticks.length).toBe(9);
done();
});
});
it("Renders Y-axis ticks for the telemetry object", (done) => {
config.yAxis.set('displayRange', {
min: 10,
max: 20
});
Vue.nextTick(() => {
let yAxisElement = element.querySelectorAll(".gl-plot-axis-area.gl-plot-y .gl-plot-tick-wrapper");
expect(yAxisElement.length).toBe(1);
let ticks = yAxisElement[0].querySelectorAll(".gl-plot-tick");
expect(ticks.length).toBe(6);
done();
});
});
it("Renders Y-axis options for the telemetry object", () => {
let yAxisElement = element.querySelectorAll(".gl-plot-axis-area.gl-plot-y .gl-plot-y-label__select");
expect(yAxisElement.length).toBe(1);
let options = yAxisElement[0].querySelectorAll("option");
expect(options.length).toBe(2);
expect(options[0].value).toBe("Some attribute");
expect(options[1].value).toBe("Another attribute");
});
it("turns on cursor Guides all telemetry objects", (done) => {
expect(plotViewComponentObject.cursorGuide).toBeFalse();
plotViewComponentObject.cursorGuide = true;
Vue.nextTick(() => {
let childCursorGuides = element.querySelectorAll(".c-cursor-guide--v");
expect(childCursorGuides.length).toBe(1);
done();
});
});
it("shows grid lines for all telemetry objects", () => {
expect(plotViewComponentObject.gridLines).toBeTrue();
let gridLinesContainer = element.querySelectorAll(".gl-plot-display-area .js-ticks");
let visible = 0;
gridLinesContainer.forEach(el => {
if (el.style.display !== "none") {
visible++;
}
});
expect(visible).toBe(2);
});
it("hides grid lines for all telemetry objects", (done) => {
expect(plotViewComponentObject.gridLines).toBeTrue();
plotViewComponentObject.gridLines = false;
Vue.nextTick(() => {
expect(plotViewComponentObject.gridLines).toBeFalse();
let gridLinesContainer = element.querySelectorAll(".gl-plot-display-area .js-ticks");
let visible = 0;
gridLinesContainer.forEach(el => {
if (el.style.display !== "none") {
visible++;
}
});
expect(visible).toBe(0);
done();
});
});
it('plots a new series when a new telemetry object is added', (done) => {
mockComposition.emit('add', testTelemetryObject2);
Vue.nextTick(() => {
let legend = element.querySelectorAll(".plot-wrapper-collapsed-legend .plot-series-name");
expect(legend.length).toBe(2);
expect(legend[1].innerHTML).toEqual("Test Object2");
done();
});
});
it('removes plots from series when a telemetry object is removed', (done) => {
mockComposition.emit('remove', testTelemetryObject.identifier);
Vue.nextTick(() => {
expect(plotViewComponentObject.compositionObjects.length).toBe(0);
done();
});
});
it("Changes the label of the y axis when the option changes", (done) => {
let selectEl = element.querySelector('.gl-plot-y-label__select');
selectEl.value = 'Another attribute';
selectEl.dispatchEvent(new Event("change"));
Vue.nextTick(() => {
expect(config.yAxis.get('label')).toEqual('Another attribute');
done();
});
});
it("Renders a new series when added to one of the plots", (done) => {
mockComposition.emit('add', testTelemetryObject2);
Vue.nextTick(() => {
let legend = element.querySelectorAll(".plot-wrapper-collapsed-legend .plot-series-name");
expect(legend.length).toBe(2);
expect(legend[1].innerHTML).toEqual("Test Object2");
done();
});
});
it("Adds a new point to the plot", (done) => {
let originalLength = config.series.models[0].getSeriesData().length;
config.series.models[0].add({
utc: 2,
'some-key': 1,
'some-other-key': 2
});
Vue.nextTick(() => {
const seriesData = config.series.models[0].getSeriesData();
expect(seriesData.length).toEqual(originalLength + 1);
done();
});
});
it("updates the xscale", (done) => {
config.xAxis.set('displayRange', {
min: 0,
max: 10
});
Vue.nextTick(() => {
expect(plotViewComponentObject.$children[1].component.$children[0].xScale.domain()).toEqual({
min: 0,
max: 10
});
done();
});
});
it("updates the yscale", (done) => {
config.yAxis.set('displayRange', {
min: 10,
max: 20
});
Vue.nextTick(() => {
expect(plotViewComponentObject.$children[1].component.$children[0].yScale.domain()).toEqual({
min: 10,
max: 20
});
done();
});
});
it("shows styles for telemetry objects if available", (done) => {
Vue.nextTick(() => {
let conditionalStylesContainer = element.querySelectorAll(".c-plot--stacked-container .js-style-receiver");
let hasStyles = 0;
conditionalStylesContainer.forEach(el => {
if (el.style.backgroundColor !== '') {
hasStyles++;
}
});
expect(hasStyles).toBe(1);
done();
});
});
});
describe('the stacked plot inspector view', () => {
let component;
let viewComponentObject;
let mockComposition;
let testTelemetryObject;
let selection;
let config;
beforeEach((done) => {
testTelemetryObject = {
identifier: {
namespace: "",
key: "test-object"
},
type: "test-object",
name: "Test Object",
telemetry: {
values: [{
key: "utc",
format: "utc",
name: "Time",
hints: {
domain: 1
}
}, {
key: "some-key",
name: "Some attribute",
hints: {
range: 1
}
}, {
key: "some-other-key",
name: "Another attribute",
hints: {
range: 2
}
}]
}
};
selection = [
[
{
context: {
item: {
type: 'telemetry.plot.stacked',
identifier: {
key: 'some-stacked-plot',
namespace: ''
},
configuration: {
series: []
}
}
}
}
]
];
openmct.router.path = [testTelemetryObject];
mockComposition = new EventEmitter();
mockComposition.load = () => {
mockComposition.emit('add', testTelemetryObject);
return [testTelemetryObject];
};
spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
const configId = openmct.objects.makeKeyString(selection[0][0].context.item.identifier);
config = new PlotConfigurationModel({
id: configId,
domainObject: selection[0][0].context.item,
openmct: openmct
});
configStore.add(configId, config);
let viewContainer = document.createElement('div');
child.append(viewContainer);
component = new Vue({
el: viewContainer,
components: {
PlotOptions
},
provide: {
openmct: openmct,
domainObject: selection[0][0].context.item,
path: [selection[0][0].context.item]
},
template: '<plot-options/>'
});
Vue.nextTick(() => {
viewComponentObject = component.$root.$children[0];
done();
});
});
afterEach(() => {
openmct.router.path = null;
});
describe('in view only mode', () => {
let browseOptionsEl;
beforeEach(() => {
browseOptionsEl = viewComponentObject.$el.querySelector('.js-plot-options-browse');
});
it('shows legend properties', () => {
const legendPropertiesEl = browseOptionsEl.querySelector('.js-legend-properties');
expect(legendPropertiesEl).not.toBeNull();
});
it('does not show series properties', () => {
const seriesPropertiesEl = browseOptionsEl.querySelector('.c-tree');
expect(seriesPropertiesEl).toBeNull();
});
it('does not show yaxis properties', () => {
const yAxisPropertiesEl = browseOptionsEl.querySelector('.js-yaxis-properties');
expect(yAxisPropertiesEl).toBeNull();
});
});
});
describe('inspector view of stacked plot child', () => {
let component;
let viewComponentObject;
let mockComposition;
let testTelemetryObject;
let selection;
let config;
beforeEach((done) => {
testTelemetryObject = {
identifier: {
namespace: "",
key: "test-object"
},
type: "test-object",
name: "Test Object",
telemetry: {
values: [{
key: "utc",
format: "utc",
name: "Time",
hints: {
domain: 1
}
}, {
key: "some-key",
name: "Some attribute",
hints: {
range: 1
}
}, {
key: "some-other-key",
name: "Another attribute",
hints: {
range: 2
}
}]
}
};
selection = [
[
{
context: {
item: {
id: "test-object",
identifier: {
key: "test-object",
namespace: ''
},
type: "telemetry.plot.overlay",
configuration: {
series: [
{
identifier: {
key: "test-object",
namespace: ''
}
}
]
},
composition: []
}
}
},
{
context: {
item: {
type: 'telemetry.plot.stacked',
identifier: {
key: 'some-stacked-plot',
namespace: ''
},
configuration: {
series: []
}
}
}
}
]
];
openmct.router.path = [testTelemetryObject];
mockComposition = new EventEmitter();
mockComposition.load = () => {
mockComposition.emit('add', testTelemetryObject);
return [testTelemetryObject];
};
spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
const configId = openmct.objects.makeKeyString(selection[0][0].context.item.identifier);
config = new PlotConfigurationModel({
id: configId,
domainObject: selection[0][0].context.item,
openmct: openmct
});
configStore.add(configId, config);
let viewContainer = document.createElement('div');
child.append(viewContainer);
component = new Vue({
el: viewContainer,
components: {
PlotOptions
},
provide: {
openmct: openmct,
domainObject: selection[0][0].context.item,
path: [selection[0][0].context.item, selection[0][1].context.item]
},
template: '<plot-options/>'
});
Vue.nextTick(() => {
viewComponentObject = component.$root.$children[0];
done();
});
});
afterEach(() => {
openmct.router.path = null;
});
describe('in view only mode', () => {
let browseOptionsEl;
beforeEach(() => {
browseOptionsEl = viewComponentObject.$el.querySelector('.js-plot-options-browse');
});
it('hides legend properties', () => {
const legendPropertiesEl = browseOptionsEl.querySelector('.js-legend-properties');
expect(legendPropertiesEl).toBeNull();
});
it('shows series properties', () => {
const seriesPropertiesEl = browseOptionsEl.querySelector('.c-tree');
expect(seriesPropertiesEl).not.toBeNull();
});
it('shows yaxis properties', () => {
const yAxisPropertiesEl = browseOptionsEl.querySelector('.js-yaxis-properties');
expect(yAxisPropertiesEl).not.toBeNull();
});
});
});
});

View File

@ -164,7 +164,7 @@ $borderMissing: 1px dashed $colorAlert !important;
$editUIColor: $uiColor; // Base color $editUIColor: $uiColor; // Base color
$editUIColorBg: $editUIColor; $editUIColorBg: $editUIColor;
$editUIColorFg: #fff; $editUIColorFg: #fff;
$editUIColorHov: pullForward(saturate($uiColor, 10%), 20%); // Hover color when $editUIColor is applied as a base color $editUIColorHov: pullForward(saturate($uiColor, 10%), 10%); // Hover color when $editUIColor is applied as a base color
$editUIBaseColor: #344b8d; // Base color, toolbar bg $editUIBaseColor: #344b8d; // Base color, toolbar bg
$editUIBaseColorHov: pullForward($editUIBaseColor, 20%); $editUIBaseColorHov: pullForward($editUIBaseColor, 20%);
$editUIBaseColorFg: #ffffff; // Toolbar button icon colors, etc. $editUIBaseColorFg: #ffffff; // Toolbar button icon colors, etc.
@ -178,11 +178,10 @@ $editFrameColor: $browseFrameColor; // Solid or dotted border applied to non-sel
$editFrameBorder: 1px dotted $editFrameColor; $editFrameBorder: 1px dotted $editFrameColor;
$editFrameColorHov: $editUIColor; // Solid border hover on frames; hover should not be applied to selected objects $editFrameColorHov: $editUIColor; // Solid border hover on frames; hover should not be applied to selected objects
$editFrameBorderHov: 1px solid $editFrameColorHov; // Hover on selectable frames $editFrameBorderHov: 1px solid $editFrameColorHov; // Hover on selectable frames
$editFrameColorSelected: #ccc; // Border of selected frames $editFrameColorSelected: #ffefc2; // Border of selected frames while editing
$editFrameColorHandleBg: $colorBodyBg; // Resize handle 'offset' color to make handle standout $editFrameColorHandleBg: $colorBodyBg; // Resize handle 'offset' color to make handle standout
$editFrameColorHandleFg: $editFrameColorSelected; // Resize handle main color $editFrameColorHandleFg: $editFrameColorSelected; // Resize handle main color
$editFrameSelectedShdw: rgba(black, 0.4) 0 1px 5px 1px; $editFrameSelectedShdw: rgba(black, 0.4) 0 1px 5px 1px;
$editFrameSelectedBorder: 1px solid $editFrameColorHov; // Selected frame element
$editFrameMovebarColorBg: $editFrameColor; // Movebar bg color $editFrameMovebarColorBg: $editFrameColor; // Movebar bg color
$editFrameMovebarColorFg: pullForward($editFrameMovebarColorBg, 20%); // Grippy lines, container size text $editFrameMovebarColorFg: pullForward($editFrameMovebarColorBg, 20%); // Grippy lines, container size text
$editFrameHovMovebarColorBg: pullForward($editFrameMovebarColorBg, 10%); // Hover style $editFrameHovMovebarColorBg: pullForward($editFrameMovebarColorBg, 10%); // Hover style
@ -191,6 +190,7 @@ $editFrameSelectedMovebarColorBg: pullForward($editFrameMovebarColorBg, 15%); //
$editFrameSelectedMovebarColorFg: pullForward($editFrameMovebarColorFg, 15%); $editFrameSelectedMovebarColorFg: pullForward($editFrameMovebarColorFg, 15%);
$editFrameMovebarH: 10px; // Height of move bar in layout frame $editFrameMovebarH: 10px; // Height of move bar in layout frame
$editMarqueeBorder: 1px dashed $editFrameColorSelected; $editMarqueeBorder: 1px dashed $editFrameColorSelected;
$editFrameSelectedBorder: $editMarqueeBorder; // Selected frame element
// Icons // Icons
$colorIconAlias: #4af6f3; $colorIconAlias: #4af6f3;

View File

@ -168,7 +168,7 @@ $borderMissing: 1px dashed $colorAlert !important;
$editUIColor: $uiColor; // Base color $editUIColor: $uiColor; // Base color
$editUIColorBg: $editUIColor; $editUIColorBg: $editUIColor;
$editUIColorFg: #fff; $editUIColorFg: #fff;
$editUIColorHov: pullForward(saturate($uiColor, 10%), 20%); // Hover color when $editUIColor is applied as a base color $editUIColorHov: pullForward(saturate($uiColor, 10%), 10%); // Hover color when $editUIColor is applied as a base color
$editUIBaseColor: #344b8d; // Base color, toolbar bg $editUIBaseColor: #344b8d; // Base color, toolbar bg
$editUIBaseColorHov: pullForward($editUIBaseColor, 20%); $editUIBaseColorHov: pullForward($editUIBaseColor, 20%);
$editUIBaseColorFg: #ffffff; // Toolbar button icon colors, etc. $editUIBaseColorFg: #ffffff; // Toolbar button icon colors, etc.
@ -182,11 +182,10 @@ $editFrameColor: $browseFrameColor; // Solid or dotted border applied to non-sel
$editFrameBorder: 1px dotted $editFrameColor; $editFrameBorder: 1px dotted $editFrameColor;
$editFrameColorHov: $editUIColor; // Solid border hover on frames; hover should not be applied to selected objects $editFrameColorHov: $editUIColor; // Solid border hover on frames; hover should not be applied to selected objects
$editFrameBorderHov: 1px solid $editFrameColorHov; // Hover on selectable frames $editFrameBorderHov: 1px solid $editFrameColorHov; // Hover on selectable frames
$editFrameColorSelected: #ccc; // Border of selected frames $editFrameColorSelected: #ffefc2; // Border of selected frames while editing
$editFrameColorHandleBg: $colorBodyBg; // Resize handle 'offset' color to make handle standout $editFrameColorHandleBg: $colorBodyBg; // Resize handle 'offset' color to make handle standout
$editFrameColorHandleFg: $editFrameColorSelected; // Resize handle main color $editFrameColorHandleFg: $editFrameColorSelected; // Resize handle main color
$editFrameSelectedShdw: rgba(black, 0.4) 0 1px 5px 1px; $editFrameSelectedShdw: rgba(black, 0.4) 0 1px 5px 1px;
$editFrameSelectedBorder: 1px solid $editFrameColorHov; // Selected frame element
$editFrameMovebarColorBg: $editFrameColor; // Movebar bg color $editFrameMovebarColorBg: $editFrameColor; // Movebar bg color
$editFrameMovebarColorFg: pullForward($editFrameMovebarColorBg, 20%); // Grippy lines, container size text $editFrameMovebarColorFg: pullForward($editFrameMovebarColorBg, 20%); // Grippy lines, container size text
$editFrameHovMovebarColorBg: pullForward($editFrameMovebarColorBg, 10%); // Hover style $editFrameHovMovebarColorBg: pullForward($editFrameMovebarColorBg, 10%); // Hover style
@ -195,6 +194,7 @@ $editFrameSelectedMovebarColorBg: pullForward($editFrameMovebarColorBg, 15%); //
$editFrameSelectedMovebarColorFg: pullForward($editFrameMovebarColorFg, 15%); $editFrameSelectedMovebarColorFg: pullForward($editFrameMovebarColorFg, 15%);
$editFrameMovebarH: 10px; // Height of move bar in layout frame $editFrameMovebarH: 10px; // Height of move bar in layout frame
$editMarqueeBorder: 1px dashed $editFrameColorSelected; $editMarqueeBorder: 1px dashed $editFrameColorSelected;
$editFrameSelectedBorder: $editMarqueeBorder; // Selected frame element
// Icons // Icons
$colorIconAlias: #4af6f3; $colorIconAlias: #4af6f3;

View File

@ -178,11 +178,10 @@ $editFrameColor: $browseFrameColor; // Solid or dotted border applied to non-sel
$editFrameBorder: 1px dotted $editFrameColor; $editFrameBorder: 1px dotted $editFrameColor;
$editFrameColorHov: $editUIColor; // Solid border hover on frames; hover should not be applied to selected objects $editFrameColorHov: $editUIColor; // Solid border hover on frames; hover should not be applied to selected objects
$editFrameBorderHov: 1px solid $editFrameColorHov; // Hover on selectable frames $editFrameBorderHov: 1px solid $editFrameColorHov; // Hover on selectable frames
$editFrameColorSelected: #333; // Border of selected frames $editFrameColorSelected: #ff7c00; // Border of selected frames
$editFrameColorHandleBg: $colorBodyBg; // Resize handle 'offset' color to make handle standout $editFrameColorHandleBg: $colorBodyBg; // Resize handle 'offset' color to make handle standout
$editFrameColorHandleFg: $editFrameColorSelected; // Resize handle main color $editFrameColorHandleFg: $editFrameColorSelected; // Resize handle main color
$editFrameSelectedShdw: rgba(black, 0.5) 0 1px 5px 2px; $editFrameSelectedShdw: rgba(black, 0.5) 0 1px 5px 2px;
$editFrameSelectedBorder: 1px dashed $editFrameColorSelected; // Selected frame element
$editFrameMovebarColorBg: $editFrameColor; // Movebar bg color $editFrameMovebarColorBg: $editFrameColor; // Movebar bg color
$editFrameMovebarColorFg: pullForward($editFrameMovebarColorBg, 20%); // Grippy lines, container size text $editFrameMovebarColorFg: pullForward($editFrameMovebarColorBg, 20%); // Grippy lines, container size text
$editFrameHovMovebarColorBg: pullForward($editFrameMovebarColorBg, 10%); // Hover style $editFrameHovMovebarColorBg: pullForward($editFrameMovebarColorBg, 10%); // Hover style
@ -191,6 +190,7 @@ $editFrameSelectedMovebarColorBg: pullForward($editFrameMovebarColorBg, 15%); //
$editFrameSelectedMovebarColorFg: pullForward($editFrameMovebarColorFg, 15%); $editFrameSelectedMovebarColorFg: pullForward($editFrameMovebarColorFg, 15%);
$editFrameMovebarH: 10px; // Height of move bar in layout frame $editFrameMovebarH: 10px; // Height of move bar in layout frame
$editMarqueeBorder: 1px dashed $editFrameColorSelected; $editMarqueeBorder: 1px dashed $editFrameColorSelected;
$editFrameSelectedBorder: 1px dashed $editMarqueeBorder; // Selected frame element
// Icons // Icons
$colorIconAlias: #4af6f3; $colorIconAlias: #4af6f3;

View File

@ -65,7 +65,6 @@ mct-plot {
.c-plot { .c-plot {
@include abs($mainViewPad); @include abs($mainViewPad);
display: flex; display: flex;
flex-direction: column;
overflow: hidden; overflow: hidden;
min-height: $plotMinH; min-height: $plotMinH;
@ -83,11 +82,18 @@ mct-plot {
} }
.c-plot--stacked-container { .c-plot--stacked-container {
border: 1px solid transparent;
display: flex; display: flex;
flex: 1 1 auto; flex: 1 1 auto;
flex-direction: column; flex-direction: column;
min-height: $plotMinH; min-height: $plotMinH;
overflow: hidden; overflow: hidden;
&[s-selected] {
.is-editing & {
border: $editMarqueeBorder;
}
}
} }
; ;

View File

@ -7,12 +7,19 @@ const webpack = require('webpack');
const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const {VueLoaderPlugin} = require('vue-loader'); const {VueLoaderPlugin} = require('vue-loader');
const gitRevision = require('child_process') let gitRevision = 'error-retrieving-revision';
.execSync('git rev-parse HEAD') let gitBranch = 'error-retrieving-branch';
.toString().trim();
const gitBranch = require('child_process') try {
.execSync('git rev-parse --abbrev-ref HEAD') gitRevision = require('child_process')
.toString().trim(); .execSync('git rev-parse HEAD')
.toString().trim();
gitBranch = require('child_process')
.execSync('git rev-parse --abbrev-ref HEAD')
.toString().trim();
} catch (err) {
console.warn(err);
}
/** @type {import('webpack').Configuration} */ /** @type {import('webpack').Configuration} */
const config = { const config = {
@ -26,9 +33,10 @@ const config = {
maelstromTheme: './src/plugins/themes/maelstrom-theme.scss' maelstromTheme: './src/plugins/themes/maelstrom-theme.scss'
}, },
output: { output: {
globalObject: "this", globalObject: 'this',
filename: '[name].js', filename: '[name].js',
library: '[name]', path: path.resolve(__dirname, 'dist'),
library: 'openmct',
libraryTarget: 'umd', libraryTarget: 'umd',
publicPath: '', publicPath: '',
hashFunction: 'xxhash64', hashFunction: 'xxhash64',

View File

@ -16,5 +16,5 @@ module.exports = merge(common, {
__OPENMCT_ROOT_RELATIVE__: '""' __OPENMCT_ROOT_RELATIVE__: '""'
}) })
], ],
devtool: 'source-map' devtool: 'eval-source-map'
}); });