Compare commits

...

11 Commits

Author SHA1 Message Date
Jamie V
4463599487 Merge branch 'master' into display-render-test-fix 2022-06-08 17:26:40 -07:00
John Hill
815506cf17 Demote notebook tests (#5313)
Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2022-06-09 00:17:41 +00:00
Jamie V
c8c364158b Merge branch 'master' into display-render-test-fix 2022-06-08 15:22:53 -07:00
Shefali Joshi
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
Jamie V
045268f09b Merge branch 'master' into display-render-test-fix 2022-06-08 14:48:51 -07:00
Charles Hacskaylo
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
Jamie V
a281a43d1e fixing non functioning render test, boost cov also 2022-06-08 14:35:31 -07:00
Jamie V
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
Jesse Mazzella
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
Jamie V
e103ea44d8 [Fault Management] Fix class case issue not showing icon (#5298)
* fixing capital class name not triggering fault severity icon

* using computed value
2022-06-08 19:45:39 +02:00
Shefali Joshi
d13d7dc8f3 Allows drag and dropping plans into timelist (#5300)
* 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>

* Don't require a plan file for timelist
Allow dropping a plan to timelist

* Rename methods and remove unused code

* Fix typo

* Boost test coverage to get over 52%

* Adds tests for webPage plugin

* Adds more tests for filtering

* Adds more filtering tests

* Removes one test

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-06-08 09:41:25 -07:00
40 changed files with 1878 additions and 473 deletions

View File

@@ -4,15 +4,29 @@
const base = 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({
page: async ({ baseURL, page }, use) => {
const messages = [];
page.on('console', msg => messages.push(`[${msg.type()}] ${msg.text()}`));
page.on('console', (msg) => messages.push(msg));
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) => {
// Use browserless if configured
// Use browserless if configured
if (workerInfo.project.name.match(/browserless/)) {
const vBrowser = await playwright.chromium.connectOverCDP({
endpointURL: 'ws://localhost:3003'

View File

@@ -89,7 +89,7 @@
"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: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: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",

View File

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

View File

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

View File

@@ -40,7 +40,7 @@
class="c-fault-mgmt__list-severity"
:title="fault.severity"
:class="[
'is-severity-' + fault.severity
'is-severity-' + severity
]"
>
</div>

View File

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

View File

@@ -22,6 +22,7 @@
import { createOpenMct, resetApplicationState } from 'utils/testing';
import FlexibleLayout from './plugin';
import Vue from 'vue';
describe('the plugin', function () {
let element;
@@ -61,7 +62,7 @@ describe('the plugin', function () {
element.appendChild(child);
openmct.on('start', done);
openmct.startHeadless();
openmct.start(child);
});
afterEach(() => {
@@ -83,6 +84,16 @@ describe('the plugin', function () {
it('provides a view', () => {
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', () => {

View File

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

View File

@@ -22,7 +22,7 @@
export function getValidatedData(domainObject) {
let sourceMap = domainObject.sourceMap;
let body = domainObject.selectFile.body;
let body = domainObject.selectFile?.body;
let json = {};
if (typeof body === 'string') {
try {
@@ -30,7 +30,7 @@ export function getValidatedData(domainObject) {
} catch (e) {
return json;
}
} else {
} else if (body !== undefined) {
json = body;
}

View File

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

View File

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

View File

@@ -39,7 +39,7 @@ export default class SeriesCollection extends Collection {
this.modelClass = PlotSeries;
this.plot = options.plot;
this.openmct = options.openmct;
this.palette = new ColorPalette();
this.palette = options.palette || new ColorPalette();
this.listenTo(this, 'add', this.onSeriesAdd, this);
this.listenTo(this, 'remove', this.onSeriesRemove, 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 label = plotModel.configuration?.yAxis?.label;
const sampleSeries = seriesCollection.first();
if (!sampleSeries) {
if (!sampleSeries || !sampleSeries.metadata) {
if (!label) {
this.unset('label');
}

View File

@@ -24,7 +24,10 @@
v-if="loaded"
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>
<plot-options-item
v-for="series in plotSeries"
@@ -36,7 +39,10 @@
v-if="plotSeries.length"
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>
<li class="grid-row">
<div
@@ -84,7 +90,10 @@
<div class="grid-cell value">{{ rangeMax }}</div>
</li>
</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>
<li class="grid-row">
<div
@@ -144,7 +153,7 @@ export default {
components: {
PlotOptionsItem
},
inject: ['openmct', 'domainObject'],
inject: ['openmct', 'domainObject', 'path'],
data() {
return {
config: {},
@@ -167,12 +176,21 @@ export default {
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() {
eventHelpers.extend(this);
this.config = this.getConfig();
this.registerListeners();
this.initConfiguration();
this.loaded = true;
},
beforeDestroy() {
this.stopListening();

View File

@@ -24,21 +24,31 @@
v-if="loaded"
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>
<li
v-for="series in plotSeries"
:key="series.key"
>
<series-form :series="series" />
<series-form
:series="series"
@seriesUpdated="updateSeriesConfigForObject"
/>
</li>
</ul>
<y-axis-form
v-if="plotSeries.length"
v-if="plotSeries.length && !isStackedPlotObject"
class="grid-properties"
: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>
<legend-form
v-if="plotSeries.length"
@@ -61,7 +71,7 @@ export default {
SeriesForm,
YAxisForm
},
inject: ['openmct', 'domainObject'],
inject: ['openmct', 'domainObject', 'path'],
data() {
return {
config: {},
@@ -69,6 +79,14 @@ export default {
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() {
eventHelpers.extend(this);
this.config = this.getConfig();
@@ -98,6 +116,24 @@ export default {
resetAllSeries() {
this.plotSeries = [];
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;
return object
&& object.type === 'telemetry.plot.overlay';
const isOverlayPlotObject = object && object.type === 'telemetry.plot.overlay';
const isStackedPlotObject = object && object.type === 'telemetry.plot.stacked';
return isStackedPlotObject || isOverlayPlotObject;
},
view: function (selection) {
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);
const getPath = this.dynamicPathForKey('color');
const seriesColorPath = getPath(this.domainObject, this.series);
if (!this.domainObject.configuration || !this.domainObject.configuration.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.domainObject,
seriesColorPath,
color.asHexString()
);
this.openmct.objects.mutate(
this.domainObject,
seriesColorPath,
color.asHexString()
);
}
if (otherSeriesWithColor) {
otherSeriesWithColor.set('color', oldColor);
const otherSeriesColorPath = getPath(
this.domainObject,
otherSeriesWithColor
);
if (!this.domainObject.configuration || !this.domainObject.configuration.series) {
this.$emit('seriesUpdated', {
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.domainObject,
otherSeriesColorPath,
oldColor.asHexString()
);
this.openmct.objects.mutate(
this.domainObject,
otherSeriesColorPath,
oldColor.asHexString()
);
}
}
},
toggleExpanded() {
@@ -343,11 +360,19 @@ export default {
if (!_.isEqual(coerce(newVal, formField.coerce), coerce(oldVal, formField.coerce))) {
this.series.set(formKey, coerce(newVal, formField.coerce));
if (path) {
this.openmct.objects.mutate(
this.domainObject,
path(this.domainObject, this.series),
coerce(newVal, formField.coerce)
);
if (!this.domainObject.configuration || !this.domainObject.configuration.series) {
this.$emit('seriesUpdated', {
identifier: this.domainObject.identifier,
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?
this.yAxis.set(formKey, newVal);
if (path) {
this.openmct.objects.mutate(
this.domainObject,
path(this.domainObject, this.yAxis),
newVal
);
if (!this.domainObject.configuration || !this.domainObject.configuration.series) {
this.$emit('seriesUpdated', {
identifier: this.domainObject.identifier,
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."
></div>
<plot-legend-item-collapsed
v-for="seriesObject in series"
:key="seriesObject.keyString"
v-for="(seriesObject, seriesIndex) in series"
:key="`seriesObject.keyString-${seriesIndex}`"
:highlights="highlights"
:value-to-show-when-collapsed="legend.get('valueToShowWhenCollapsed')"
:series-object="seriesObject"
@@ -95,8 +95,8 @@
</thead>
<tbody>
<plot-legend-item-expanded
v-for="seriesObject in series"
:key="seriesObject.keyString"
v-for="(seriesObject, seriesIndex) in series"
:key="`seriesObject.keyString-${seriesIndex}`"
:series-object="seriesObject"
:highlights="highlights"
:legend="legend"

View File

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

View File

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

View File

@@ -23,7 +23,6 @@
import {createMouseEvent, createOpenMct, resetApplicationState, spyOnBuiltins} from "utils/testing";
import PlotVuePlugin from "./plugin";
import Vue from "vue";
import StackedPlot from "./stackedPlot/StackedPlot.vue";
import configStore from "./configuration/ConfigStore";
import EventEmitter from "EventEmitter";
import PlotOptions from "./inspector/PlotOptions.vue";
@@ -348,14 +347,20 @@ describe("the plugin", function () {
}
};
openmct.router.path = [testTelemetryObject];
applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath);
plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === "plot-single");
plotView = plotViewProvider.view(testTelemetryObject, [testTelemetryObject]);
plotView = plotViewProvider.view(testTelemetryObject, []);
plotView.show(child, true);
return Vue.nextTick();
});
afterEach(() => {
openmct.router.path = null;
});
it("Makes only one request for telemetry on load", () => {
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', () => {
let component;
let viewComponentObject;
@@ -955,6 +606,7 @@ describe("the plugin", function () {
]
];
openmct.router.path = [testTelemetryObject];
mockComposition = new EventEmitter();
mockComposition.load = () => {
mockComposition.emit('add', testTelemetryObject);
@@ -993,6 +645,10 @@ describe("the plugin", function () {
});
});
afterEach(() => {
openmct.router.path = null;
});
describe('in view only mode', () => {
let browseOptionsEl;
let editOptionsEl;
@@ -1096,5 +752,24 @@ describe("the plugin", function () {
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>
<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">
<stacked-plot-item
v-for="object in compositionObjects"
:key="object.id"
class="c-plot--stacked-container"
:object="object"
:child-object="object"
:options="options"
:grid-lines="gridLines"
:color-palette="colorPalette"
:cursor-guide="cursorGuide"
:show-limit-line-labels="showLimitLineLabels"
:plot-tick-width="maxTickWidth"
@plotTickWidth="onTickWidthChange"
@loadingUpdated="loadingUpdated"
@cursorGuide="onCursorGuideChange"
@gridLines="onGridLinesChange"
@lockHighlightPoint="lockHighlightPointUpdated"
@highlights="highlightsUpdated"
@configLoaded="registerSeriesListeners"
/>
</div>
</div>
@@ -43,12 +59,19 @@
<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 ImageExporter from '../../../exporters/ImageExporter';
import eventHelpers from "@/plugins/plot/lib/eventHelpers";
export default {
components: {
StackedPlotItem
StackedPlotItem,
PlotLegend
},
inject: ['openmct', 'domainObject', 'composition', 'path'],
props: {
@@ -60,16 +83,35 @@ export default {
}
},
data() {
this.seriesConfig = {};
return {
hideExportButtons: false,
cursorGuide: false,
gridLines: true,
loading: false,
compositionObjects: [],
tickWidthMap: {}
tickWidthMap: {},
legend: {},
loaded: false,
lockHighlightPoint: false,
highlights: [],
seriesModels: [],
showLimitLineLabels: undefined,
colorPalette: new ColorPalette()
};
},
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() {
return Math.max(...Object.values(this.tickWidthMap));
}
@@ -78,6 +120,13 @@ export default {
this.destroy();
},
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.composition.on('add', this.addChild);
@@ -86,10 +135,29 @@ export default {
this.composition.load();
},
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) {
this.loading = loaded;
},
destroy() {
this.stopListening();
configStore.deleteStore(this.config.id);
this.composition.off('add', this.addChild);
this.composition.off('remove', this.removeChild);
this.composition.off('reorder', this.compositionReorder);
@@ -99,6 +167,19 @@ export default {
const id = this.openmct.objects.makeKeyString(child.identifier);
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);
},
@@ -107,6 +188,13 @@ export default {
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 identifier = this.openmct.objects.makeKeyString(c.identifier);
@@ -158,6 +246,34 @@ export default {
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) {
this.cursorGuide = cursorGuide === true;
},

View File

@@ -27,12 +27,14 @@
import MctPlot from '../MctPlot.vue';
import Vue from "vue";
import conditionalStylesMixin from "./mixins/objectStyles-mixin";
import configStore from "@/plugins/plot/configuration/ConfigStore";
import PlotConfigurationModel from "@/plugins/plot/configuration/PlotConfigurationModel";
export default {
mixins: [conditionalStylesMixin],
inject: ['openmct', 'domainObject', 'path'],
props: {
object: {
childObject: {
type: Object,
default() {
return {};
@@ -56,6 +58,18 @@ export default {
return true;
}
},
showLimitLineLabels: {
type: Object,
default() {
return {};
}
},
colorPalette: {
type: Object,
default() {
return undefined;
}
},
plotTickWidth: {
type: Number,
default() {
@@ -72,12 +86,22 @@ export default {
},
plotTickWidth(width) {
this.updateComponentProp('plotTickWidth', width);
},
showLimitLineLabels: {
handler(data) {
this.updateComponentProp('limitLineLabels', data);
},
deep: true
}
},
mounted() {
this.updateView();
},
beforeDestroy() {
if (this.removeSelectable) {
this.removeSelectable();
}
if (this.component) {
this.component.$destroy();
}
@@ -96,15 +120,19 @@ export default {
}
const onTickWidthChange = this.onTickWidthChange;
const onLockHighlightPointUpdated = this.onLockHighlightPointUpdated;
const onHighlightsUpdated = this.onHighlightsUpdated;
const onConfigLoaded = this.onConfigLoaded;
const onCursorGuideChange = this.onCursorGuideChange;
const onGridLinesChange = this.onGridLinesChange;
const loadingUpdated = this.loadingUpdated;
const setStatus = this.setStatus;
const openmct = this.openmct;
const object = this.object;
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;
let viewContainer = document.createElement('div');
this.$el.append(viewContainer);
@@ -123,14 +151,28 @@ export default {
return {
...getProps(),
onTickWidthChange,
onLockHighlightPointUpdated,
onHighlightsUpdated,
onConfigLoaded,
onCursorGuideChange,
onGridLinesChange,
loadingUpdated,
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() {
this.$emit('plotTickWidth', ...arguments);
@@ -145,19 +187,73 @@ export default {
this.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) {
this.loading = loaded;
this.updateComponentProp('loading', loaded);
},
getProps() {
return {
limitLineLabels: this.showLimitLineLabels,
gridLines: this.gridLines,
cursorGuide: this.cursorGuide,
plotTickWidth: this.plotTickWidth,
loading: this.loading,
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() {
this.objectStyles = this.getObjectStyleForItem(this.object.configuration);
this.objectStyles = this.getObjectStyleForItem(this.childObject.configuration);
this.initObjectStyles();
},
beforeDestroy() {
@@ -62,18 +62,18 @@ export default {
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
this.styleRuleManager.updateObjectStyleConfig(newObjectStyle);
});
if (this.object && this.object.configuration && this.object.configuration.fontStyle) {
const { fontSize, font } = this.object.configuration.fontStyle;
if (this.childObject && this.childObject.configuration && this.childObject.configuration.fontStyle) {
const { fontSize, font } = this.childObject.configuration.fontStyle;
this.setFontSize(fontSize);
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.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

@@ -27,17 +27,18 @@
:show-ucontents="item.domainObject.type === 'plan'"
:span-rows-count="item.rowCount"
>
<template slot="label">
<template #label>
{{ item.domainObject.name }}
</template>
<object-view
ref="objectView"
slot="object"
class="u-contents"
:default-object="item.domainObject"
:object-path="item.objectPath"
@change-action-collection="setActionCollection"
/>
<template #object>
<object-view
ref="objectView"
class="u-contents"
:default-object="item.domainObject"
:object-path="item.objectPath"
@change-action-collection="setActionCollection"
/>
</template>
</swim-lane>
</template>

View File

@@ -29,10 +29,10 @@
v-for="timeSystemItem in timeSystems"
:key="timeSystemItem.timeSystem.key"
>
<template slot="label">
<template #label>
{{ timeSystemItem.timeSystem.name }}
</template>
<template slot="object">
<template #object>
<timeline-axis
:bounds="timeSystemItem.bounds"
:time-system="timeSystemItem.timeSystem"
@@ -50,7 +50,7 @@
<timeline-object-view
v-for="item in items"
:key="item.keyString"
class="c-timeline__content"
class="c-timeline__content js-timeline__content"
:item="item"
/>
</div>
@@ -93,15 +93,15 @@ export default {
this.stopFollowingTimeContext();
},
mounted() {
this.items = [];
this.setTimeContext();
if (this.composition) {
this.composition.on('add', this.addItem);
this.composition.on('remove', this.removeItem);
this.composition.on('reorder', this.reorder);
this.composition.load();
}
this.setTimeContext();
this.getTimeSystems();
},
methods: {
addItem(domainObject) {
@@ -165,6 +165,7 @@ export default {
this.stopFollowingTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
this.getTimeSystems();
this.updateViewBounds(this.timeContext.bounds());
this.timeContext.on('bounds', this.updateViewBounds);
},

View File

@@ -20,9 +20,10 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { createOpenMct, resetApplicationState } from "utils/testing";
import { createOpenMct, resetApplicationState } from "@/utils/testing";
import TimelinePlugin from "./plugin";
import Vue from 'vue';
import EventEmitter from "EventEmitter";
describe('the plugin', function () {
let objectDef;
@@ -30,6 +31,37 @@ describe('the plugin', function () {
let child;
let openmct;
let mockObjectPath;
let mockCompositionForTimelist;
let planObject = {
identifier: {
key: 'test-plan-object',
namespace: ''
},
type: 'plan',
id: "test-plan-object",
selectFile: {
body: JSON.stringify({
"TEST-GROUP": [
{
"name": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua",
"start": 1597170002854,
"end": 1597171032854,
"type": "TEST-GROUP",
"color": "fuchsia",
"textColor": "black"
},
{
"name": "Sed ut perspiciatis",
"start": 1597171132854,
"end": 1597171232854,
"type": "TEST-GROUP",
"color": "fuchsia",
"textColor": "black"
}
]
})
}
};
beforeEach((done) => {
mockObjectPath = [
@@ -107,7 +139,23 @@ describe('the plugin', function () {
key: "test-object",
namespace: ''
},
type: "time-strip"
type: "time-strip",
configuration: {
useIndependentTime: false,
timeOptions: {
mode: {
key: 'fixed'
},
fixedOffsets: {
start: 10,
end: 11
},
clockOffsets: {
start: -(30 * 60 * 1000),
end: (30 * 60 * 1000)
}
}
}
};
const applicableViews = openmct.objectViews.get(testViewObject, mockObjectPath);
@@ -133,6 +181,58 @@ describe('the plugin', function () {
});
});
describe('the timeline composition', () => {
let timelineDomainObject;
let timelineView;
beforeEach(() => {
timelineDomainObject = {
identifier: {
key: 'test-object',
namespace: ''
},
type: 'time-strip',
id: "test-object",
configuration: {
useIndependentTime: false
},
composition: [
{
identifier: {
key: 'test-plan-object',
namespace: ''
}
}
]
};
mockCompositionForTimelist = new EventEmitter();
mockCompositionForTimelist.load = () => {
mockCompositionForTimelist.emit('add', planObject);
return [planObject];
};
spyOn(openmct.composition, 'get').withArgs(timelineDomainObject).and.returnValue(mockCompositionForTimelist);
openmct.router.path = [timelineDomainObject];
const applicableViews = openmct.objectViews.get(timelineDomainObject, [timelineDomainObject]);
timelineView = applicableViews.find((viewProvider) => viewProvider.key === 'time-strip.view');
let view = timelineView.view(timelineDomainObject, [timelineDomainObject]);
view.show(child, true);
return Vue.nextTick();
});
it('loads the plan from composition', () => {
return Vue.nextTick(() => {
const items = element.querySelectorAll('.js-timeline__content');
expect(items.length).toEqual(1);
});
});
});
describe('the independent time conductor', () => {
let timelineView;
let testViewObject = {
@@ -181,7 +281,7 @@ describe('the plugin', function () {
});
});
describe('the independent time conductor', () => {
describe('the independent time conductor - fixed', () => {
let timelineView;
let testViewObject2 = {
id: "test-object2",

View File

@@ -96,8 +96,10 @@ export default {
components: {
ListView
},
inject: ['openmct', 'domainObject', 'path'],
inject: ['openmct', 'domainObject', 'path', 'composition'],
data() {
this.planObjects = [];
return {
viewBounds: undefined,
height: 0,
@@ -111,7 +113,7 @@ export default {
this.timestamp = Date.now();
this.getPlanDataAndSetConfig(this.domainObject);
this.unlisten = this.openmct.objects.observe(this.domainObject, 'selectFile', this.getPlanDataAndSetConfig);
this.unlisten = this.openmct.objects.observe(this.domainObject, 'selectFile', this.planFileUpdated);
this.unlistenConfig = this.openmct.objects.observe(this.domainObject, 'configuration', this.setViewFromConfig);
this.removeStatusListener = this.openmct.status.observe(this.domainObject.identifier, this.setStatus);
this.status = this.openmct.status.get(this.domainObject.identifier);
@@ -120,6 +122,12 @@ export default {
this.deferAutoScroll = _.debounce(this.deferAutoScroll, 500);
this.$el.parentElement.addEventListener('scroll', this.deferAutoScroll, true);
if (this.composition) {
this.composition.on('add', this.addToComposition);
this.composition.on('remove', this.removeItem);
this.composition.load();
}
},
beforeDestroy() {
if (this.unlisten) {
@@ -144,8 +152,19 @@ export default {
if (this.clearAutoScrollDisabledTimer) {
clearTimeout(this.clearAutoScrollDisabledTimer);
}
if (this.composition) {
this.composition.off('add', this.addToComposition);
this.composition.off('remove', this.removeItem);
}
},
methods: {
planFileUpdated(selectFile) {
this.getPlanData({
selectFile,
sourceMap: this.domainObject.sourceMap
});
},
getPlanDataAndSetConfig(mutatedObject) {
this.getPlanData(mutatedObject);
this.setViewFromConfig(mutatedObject.configuration);
@@ -163,6 +182,58 @@ export default {
this.listActivities();
}
},
addItem(domainObject) {
this.planObjects = [domainObject];
this.resetPlanData();
if (domainObject.type === 'plan') {
this.getPlanDataAndSetConfig({
...this.domainObject,
selectFile: domainObject.selectFile
});
}
},
addToComposition(telemetryObject) {
if (this.planObjects.length > 0) {
this.confirmReplacePlan(telemetryObject);
} else {
this.addItem(telemetryObject);
}
},
confirmReplacePlan(telemetryObject) {
const dialog = this.openmct.overlays.dialog({
iconClass: 'alert',
message: 'This action will replace the current plan. Do you want to continue?',
buttons: [
{
label: 'Ok',
emphasis: true,
callback: () => {
const oldTelemetryObject = this.planObjects[0];
this.removeFromComposition(oldTelemetryObject);
this.addItem(telemetryObject);
dialog.dismiss();
}
},
{
label: 'Cancel',
callback: () => {
this.removeFromComposition(telemetryObject);
dialog.dismiss();
}
}
]
});
},
removeFromComposition(telemetryObject) {
this.composition.remove(telemetryObject);
},
removeItem() {
this.planObjects = [];
this.resetPlanData();
},
resetPlanData() {
this.planData = {};
},
getPlanData(domainObject) {
this.planData = getValidatedData(domainObject);
},
@@ -176,7 +247,7 @@ export default {
const futureEventsDurationIndex = this.domainObject.configuration.futureEventsDurationIndex;
if (pastEventsIndex === 0 && futureEventsIndex === 0 && currentEventsIndex === 0) {
//show all events
//don't show all events
this.showAll = false;
this.viewBounds = undefined;
this.hideAll = true;

View File

@@ -0,0 +1,34 @@
/*****************************************************************************
* 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 {TIMELIST_TYPE} from "@/plugins/timelist/constants";
export default function TimelistCompositionPolicy(openmct) {
return {
allow: function (parent, child) {
if (parent.type === TIMELIST_TYPE && child.type !== 'plan') {
return false;
}
return true;
}
};
}

View File

@@ -52,7 +52,8 @@ export default function TimelistViewProvider(openmct) {
provide: {
openmct,
domainObject,
path: objectPath
path: objectPath,
composition: openmct.composition.get(domainObject)
},
template: '<timelist></timelist>'
});

View File

@@ -23,6 +23,7 @@
import TimelistViewProvider from './TimelistViewProvider';
import { TIMELIST_TYPE } from './constants';
import TimeListInspectorViewProvider from "./inspector/TimeListInspectorViewProvider";
import TimelistCompositionPolicy from "@/plugins/timelist/TimelistCompositionPolicy";
export default function () {
return function install(openmct) {
@@ -37,7 +38,6 @@ export default function () {
name: 'Upload Plan (JSON File)',
key: 'selectFile',
control: 'file-input',
required: true,
text: 'Select File...',
type: 'application/json',
property: [
@@ -59,10 +59,12 @@ export default function () {
pastEventsDuration: 20,
filter: ''
};
domainObject.composition = [];
}
});
openmct.objectViews.addProvider(new TimelistViewProvider(openmct));
openmct.inspectorViews.addProvider(new TimeListInspectorViewProvider(openmct));
openmct.composition.addPolicy(new TimelistCompositionPolicy(openmct).allow);
};
}

View File

@@ -25,6 +25,7 @@ import TimelistPlugin from "./plugin";
import { TIMELIST_TYPE } from "./constants";
import Vue from 'vue';
import moment from "moment";
import EventEmitter from "EventEmitter";
const LIST_ITEM_CLASS = '.js-table__body .js-list-item';
const LIST_ITEM_VALUE_CLASS = '.js-list-item__value';
@@ -37,6 +38,41 @@ describe('the plugin', function () {
let openmct;
let appHolder;
let originalRouterPath;
let mockComposition;
let now = Date.now();
let twoHoursPast = now - (1000 * 60 * 60 * 2);
let oneHourPast = now - (1000 * 60 * 60);
let twoHoursFuture = now + (1000 * 60 * 60 * 2);
let planObject = {
identifier: {
key: 'test-plan-object',
namespace: ''
},
type: 'plan',
id: "test-plan-object",
selectFile: {
body: JSON.stringify({
"TEST-GROUP": [
{
"name": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua",
"start": twoHoursPast,
"end": oneHourPast,
"type": "TEST-GROUP",
"color": "fuchsia",
"textColor": "black"
},
{
"name": "Sed ut perspiciatis",
"start": now,
"end": twoHoursFuture,
"type": "TEST-GROUP",
"color": "fuchsia",
"textColor": "black"
}
]
})
}
};
beforeEach((done) => {
appHolder = document.createElement('div');
@@ -58,6 +94,15 @@ describe('the plugin', function () {
originalRouterPath = openmct.router.path;
mockComposition = new EventEmitter();
mockComposition.load = () => {
mockComposition.emit('add', planObject);
return Promise.resolve([planObject]);
};
spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
openmct.on('start', done);
openmct.start(appHolder);
});
@@ -112,13 +157,13 @@ describe('the plugin', function () {
sortOrderIndex: 0,
futureEventsIndex: 1,
futureEventsDurationIndex: 0,
futureEventsDuration: 20,
futureEventsDuration: 0,
currentEventsIndex: 1,
currentEventsDurationIndex: 0,
currentEventsDuration: 20,
currentEventsDuration: 0,
pastEventsIndex: 1,
pastEventsDurationIndex: 0,
pastEventsDuration: 20,
pastEventsDuration: 0,
filter: ''
},
selectFile: {
@@ -126,16 +171,16 @@ describe('the plugin', function () {
"TEST-GROUP": [
{
"name": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua",
"start": 1597170002854,
"end": 1597171032854,
"start": twoHoursPast,
"end": oneHourPast,
"type": "TEST-GROUP",
"color": "fuchsia",
"textColor": "black"
},
{
"name": "Sed ut perspiciatis",
"start": 1597171132854,
"end": 1597171232854,
"start": now,
"end": twoHoursFuture,
"type": "TEST-GROUP",
"color": "fuchsia",
"textColor": "black"
@@ -171,11 +216,164 @@ describe('the plugin', function () {
const itemValues = itemEls[0].querySelectorAll(LIST_ITEM_VALUE_CLASS);
expect(itemValues.length).toEqual(4);
expect(itemValues[3].innerHTML.trim()).toEqual('Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua');
expect(itemValues[0].innerHTML.trim()).toEqual(`${moment(1597170002854).format('YYYY-MM-DD HH:mm:ss:SSS')}Z`);
expect(itemValues[1].innerHTML.trim()).toEqual(`${moment(1597171032854).format('YYYY-MM-DD HH:mm:ss:SSS')}Z`);
expect(itemValues[0].innerHTML.trim()).toEqual(`${moment(twoHoursPast).format('YYYY-MM-DD HH:mm:ss:SSS')}Z`);
expect(itemValues[1].innerHTML.trim()).toEqual(`${moment(oneHourPast).format('YYYY-MM-DD HH:mm:ss:SSS')}Z`);
done();
});
});
});
describe('the timelist composition', () => {
let timelistDomainObject;
let timelistView;
beforeEach(() => {
timelistDomainObject = {
identifier: {
key: 'test-object',
namespace: ''
},
type: TIMELIST_TYPE,
id: "test-object",
configuration: {
sortOrderIndex: 0,
futureEventsIndex: 1,
futureEventsDurationIndex: 0,
futureEventsDuration: 0,
currentEventsIndex: 1,
currentEventsDurationIndex: 0,
currentEventsDuration: 0,
pastEventsIndex: 1,
pastEventsDurationIndex: 0,
pastEventsDuration: 0,
filter: ''
},
composition: [{
identifier: {
key: 'test-plan-object',
namespace: ''
}
}]
};
openmct.router.path = [timelistDomainObject];
const applicableViews = openmct.objectViews.get(timelistDomainObject, [timelistDomainObject]);
timelistView = applicableViews.find((viewProvider) => viewProvider.key === 'timelist.view');
let view = timelistView.view(timelistDomainObject, [timelistDomainObject]);
view.show(child, true);
return Vue.nextTick();
});
it('loads the plan from composition', () => {
return Vue.nextTick(() => {
const items = element.querySelectorAll(LIST_ITEM_CLASS);
expect(items.length).toEqual(2);
});
});
});
describe('filters', () => {
let timelistDomainObject;
let timelistView;
beforeEach(() => {
timelistDomainObject = {
identifier: {
key: 'test-object',
namespace: ''
},
type: TIMELIST_TYPE,
id: "test-object",
configuration: {
sortOrderIndex: 0,
futureEventsIndex: 1,
futureEventsDurationIndex: 0,
futureEventsDuration: 0,
currentEventsIndex: 1,
currentEventsDurationIndex: 0,
currentEventsDuration: 0,
pastEventsIndex: 1,
pastEventsDurationIndex: 0,
pastEventsDuration: 0,
filter: 'perspiciatis'
},
composition: [{
identifier: {
key: 'test-plan-object',
namespace: ''
}
}]
};
openmct.router.path = [timelistDomainObject];
const applicableViews = openmct.objectViews.get(timelistDomainObject, [timelistDomainObject]);
timelistView = applicableViews.find((viewProvider) => viewProvider.key === 'timelist.view');
let view = timelistView.view(timelistDomainObject, [timelistDomainObject]);
view.show(child, true);
return Vue.nextTick();
});
it('activities', () => {
return Vue.nextTick(() => {
const items = element.querySelectorAll(LIST_ITEM_CLASS);
expect(items.length).toEqual(1);
});
});
});
describe('time filtering - past', () => {
let timelistDomainObject;
let timelistView;
beforeEach(() => {
timelistDomainObject = {
identifier: {
key: 'test-object',
namespace: ''
},
type: TIMELIST_TYPE,
id: "test-object",
configuration: {
sortOrderIndex: 0,
futureEventsIndex: 1,
futureEventsDurationIndex: 0,
futureEventsDuration: 0,
currentEventsIndex: 1,
currentEventsDurationIndex: 0,
currentEventsDuration: 0,
pastEventsIndex: 0,
pastEventsDurationIndex: 0,
pastEventsDuration: 0,
filter: ''
},
composition: [{
identifier: {
key: 'test-plan-object',
namespace: ''
}
}]
};
openmct.router.path = [timelistDomainObject];
const applicableViews = openmct.objectViews.get(timelistDomainObject, [timelistDomainObject]);
timelistView = applicableViews.find((viewProvider) => viewProvider.key === 'timelist.view');
let view = timelistView.view(timelistDomainObject, [timelistDomainObject]);
view.show(child, true);
return Vue.nextTick();
});
it('hides past events', () => {
return Vue.nextTick(() => {
const items = element.querySelectorAll(LIST_ITEM_CLASS);
expect(items.length).toEqual(1);
});
});
});
});

View File

@@ -0,0 +1,106 @@
/*****************************************************************************
* 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 { createOpenMct, resetApplicationState } from "utils/testing";
import WebPagePlugin from "./plugin";
function getView(openmct, domainObj, objectPath) {
const applicableViews = openmct.objectViews.get(domainObj, objectPath);
const webpageView = applicableViews.find((viewProvider) => viewProvider.key === 'webPage');
return webpageView.view(domainObj);
}
function destroyView(view) {
return view.destroy();
}
describe("The web page plugin", function () {
let mockDomainObject;
let mockDomainObjectPath;
let openmct;
let element;
let child;
let view;
beforeEach((done) => {
mockDomainObjectPath = [
{
name: 'mock webpage',
type: 'webpage',
identifier: {
key: 'mock-webpage',
namespace: ''
}
}
];
mockDomainObject = {
displayFormat: "",
name: "Unnamed WebPage",
type: "webPage",
location: "f69c21ac-24ef-450c-8e2f-3d527087d285",
modified: 1627483839783,
url: "123",
displayText: "123",
persisted: 1627483839783,
id: "3d9c243d-dffb-446b-8474-d9931a99d679",
identifier: {
namespace: "",
key: "3d9c243d-dffb-446b-8474-d9931a99d679"
}
};
openmct = createOpenMct();
openmct.install(new WebPagePlugin());
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);
openmct.on('start', done);
openmct.startHeadless();
});
afterEach(() => {
destroyView(view);
return resetApplicationState(openmct);
});
describe('the view', () => {
beforeEach(() => {
view = getView(openmct, mockDomainObject, mockDomainObjectPath);
view.show(child, true);
});
it('provides a view', () => {
expect(view).toBeDefined();
});
});
});

View File

@@ -164,7 +164,7 @@ $borderMissing: 1px dashed $colorAlert !important;
$editUIColor: $uiColor; // Base color
$editUIColorBg: $editUIColor;
$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
$editUIBaseColorHov: pullForward($editUIBaseColor, 20%);
$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;
$editFrameColorHov: $editUIColor; // Solid border hover on frames; hover should not be applied to selected objects
$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
$editFrameColorHandleFg: $editFrameColorSelected; // Resize handle main color
$editFrameSelectedShdw: rgba(black, 0.4) 0 1px 5px 1px;
$editFrameSelectedBorder: 1px solid $editFrameColorHov; // Selected frame element
$editFrameMovebarColorBg: $editFrameColor; // Movebar bg color
$editFrameMovebarColorFg: pullForward($editFrameMovebarColorBg, 20%); // Grippy lines, container size text
$editFrameHovMovebarColorBg: pullForward($editFrameMovebarColorBg, 10%); // Hover style
@@ -191,6 +190,7 @@ $editFrameSelectedMovebarColorBg: pullForward($editFrameMovebarColorBg, 15%); //
$editFrameSelectedMovebarColorFg: pullForward($editFrameMovebarColorFg, 15%);
$editFrameMovebarH: 10px; // Height of move bar in layout frame
$editMarqueeBorder: 1px dashed $editFrameColorSelected;
$editFrameSelectedBorder: $editMarqueeBorder; // Selected frame element
// Icons
$colorIconAlias: #4af6f3;

View File

@@ -168,7 +168,7 @@ $borderMissing: 1px dashed $colorAlert !important;
$editUIColor: $uiColor; // Base color
$editUIColorBg: $editUIColor;
$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
$editUIBaseColorHov: pullForward($editUIBaseColor, 20%);
$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;
$editFrameColorHov: $editUIColor; // Solid border hover on frames; hover should not be applied to selected objects
$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
$editFrameColorHandleFg: $editFrameColorSelected; // Resize handle main color
$editFrameSelectedShdw: rgba(black, 0.4) 0 1px 5px 1px;
$editFrameSelectedBorder: 1px solid $editFrameColorHov; // Selected frame element
$editFrameMovebarColorBg: $editFrameColor; // Movebar bg color
$editFrameMovebarColorFg: pullForward($editFrameMovebarColorBg, 20%); // Grippy lines, container size text
$editFrameHovMovebarColorBg: pullForward($editFrameMovebarColorBg, 10%); // Hover style
@@ -195,6 +194,7 @@ $editFrameSelectedMovebarColorBg: pullForward($editFrameMovebarColorBg, 15%); //
$editFrameSelectedMovebarColorFg: pullForward($editFrameMovebarColorFg, 15%);
$editFrameMovebarH: 10px; // Height of move bar in layout frame
$editMarqueeBorder: 1px dashed $editFrameColorSelected;
$editFrameSelectedBorder: $editMarqueeBorder; // Selected frame element
// Icons
$colorIconAlias: #4af6f3;

View File

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

View File

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