diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 5fe33cf1ba..7ba460118a 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -40,6 +40,7 @@ assignees: ''
- [ ] Is there a workaround available?
- [ ] Does this impact a critical component?
- [ ] Is this just a visual bug with no functional impact?
+- [ ] Does this block the execution of e2e tests?
#### Additional Information
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 89fb12a9e3..8cbde6e73d 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -16,7 +16,7 @@ Closes
+
+
+
+
+
+
diff --git a/src/plugins/charts/scatter/ScatterPlotViewProvider.js b/src/plugins/charts/scatter/ScatterPlotViewProvider.js
new file mode 100644
index 0000000000..338d2eb3e3
--- /dev/null
+++ b/src/plugins/charts/scatter/ScatterPlotViewProvider.js
@@ -0,0 +1,79 @@
+/*****************************************************************************
+ * Open MCT, Copyright (c) 2014-2021, 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 ScatterPlotView from './ScatterPlotView.vue';
+import { SCATTER_PLOT_KEY, SCATTER_PLOT_VIEW, TIME_STRIP_KEY } from './scatterPlotConstants.js';
+import Vue from 'vue';
+
+export default function ScatterPlotViewProvider(openmct) {
+ function isCompactView(objectPath) {
+ let isChildOfTimeStrip = objectPath.find(object => object.type === TIME_STRIP_KEY);
+
+ return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath);
+ }
+
+ return {
+ key: SCATTER_PLOT_VIEW,
+ name: 'Scatter Plot',
+ cssClass: 'icon-telemetry',
+ canView(domainObject, objectPath) {
+ return domainObject && domainObject.type === SCATTER_PLOT_KEY;
+ },
+
+ canEdit(domainObject, objectPath) {
+ return domainObject && domainObject.type === SCATTER_PLOT_KEY;
+ },
+
+ view: function (domainObject, objectPath) {
+ let component;
+
+ return {
+ show: function (element) {
+ let isCompact = isCompactView(objectPath);
+ component = new Vue({
+ el: element,
+ components: {
+ ScatterPlotView
+ },
+ provide: {
+ openmct,
+ domainObject,
+ path: objectPath
+ },
+ data() {
+ return {
+ options: {
+ compact: isCompact
+ }
+ };
+ },
+ template: ''
+ });
+ },
+ destroy: function () {
+ component.$destroy();
+ component = undefined;
+ }
+ };
+ }
+ };
+}
diff --git a/src/plugins/charts/scatter/ScatterPlotWithUnderlay.vue b/src/plugins/charts/scatter/ScatterPlotWithUnderlay.vue
new file mode 100644
index 0000000000..796a252ac7
--- /dev/null
+++ b/src/plugins/charts/scatter/ScatterPlotWithUnderlay.vue
@@ -0,0 +1,393 @@
+
+
+
+
+
diff --git a/src/plugins/charts/scatter/inspector/PlotOptions.vue b/src/plugins/charts/scatter/inspector/PlotOptions.vue
new file mode 100644
index 0000000000..a72fcb8c9a
--- /dev/null
+++ b/src/plugins/charts/scatter/inspector/PlotOptions.vue
@@ -0,0 +1,64 @@
+
+
+
+
+
+
diff --git a/src/plugins/charts/scatter/inspector/PlotOptionsBrowse.vue b/src/plugins/charts/scatter/inspector/PlotOptionsBrowse.vue
new file mode 100644
index 0000000000..c7af21973c
--- /dev/null
+++ b/src/plugins/charts/scatter/inspector/PlotOptionsBrowse.vue
@@ -0,0 +1,153 @@
+
+
+
+
+ Settings
+ -
+
X Axis
+ {{ xKeyLabel }}
+
+ -
+
Y Axis
+ {{ yKeyLabel }}
+
+
+
+
+
+
+
diff --git a/src/plugins/charts/scatter/inspector/PlotOptionsEdit.vue b/src/plugins/charts/scatter/inspector/PlotOptionsEdit.vue
new file mode 100644
index 0000000000..6781a27777
--- /dev/null
+++ b/src/plugins/charts/scatter/inspector/PlotOptionsEdit.vue
@@ -0,0 +1,262 @@
+
+
+
+
+
diff --git a/src/plugins/charts/scatter/inspector/ScatterPlotInspectorViewProvider.js b/src/plugins/charts/scatter/inspector/ScatterPlotInspectorViewProvider.js
new file mode 100644
index 0000000000..54487dfe37
--- /dev/null
+++ b/src/plugins/charts/scatter/inspector/ScatterPlotInspectorViewProvider.js
@@ -0,0 +1,48 @@
+import { SCATTER_PLOT_INSPECTOR_KEY, SCATTER_PLOT_KEY } from '../scatterPlotConstants';
+import Vue from 'vue';
+import PlotOptions from "./PlotOptions.vue";
+
+export default function ScatterPlotInspectorViewProvider(openmct) {
+ return {
+ key: SCATTER_PLOT_INSPECTOR_KEY,
+ name: 'Bar Graph Inspector View',
+ canView: function (selection) {
+ if (selection.length === 0 || selection[0].length === 0) {
+ return false;
+ }
+
+ let object = selection[0][0].context.item;
+
+ return object
+ && object.type === SCATTER_PLOT_KEY;
+ },
+ view: function (selection) {
+ let component;
+
+ return {
+ show: function (element) {
+ component = new Vue({
+ el: element,
+ components: {
+ PlotOptions
+ },
+ provide: {
+ openmct,
+ domainObject: selection[0][0].context.item
+ },
+ template: ''
+ });
+ },
+ destroy: function () {
+ if (component) {
+ component.$destroy();
+ component = undefined;
+ }
+ }
+ };
+ },
+ priority: function () {
+ return 1;
+ }
+ };
+}
diff --git a/src/plugins/charts/scatter/plugin.js b/src/plugins/charts/scatter/plugin.js
new file mode 100644
index 0000000000..600c2970fd
--- /dev/null
+++ b/src/plugins/charts/scatter/plugin.js
@@ -0,0 +1,127 @@
+/*****************************************************************************
+ * 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 { SCATTER_PLOT_KEY } from './scatterPlotConstants.js';
+import ScatterPlotViewProvider from './ScatterPlotViewProvider';
+import ScatterPlotInspectorViewProvider from './inspector/ScatterPlotInspectorViewProvider';
+import ScatterPlotCompositionPolicy from './ScatterPlotCompositionPolicy';
+import Vue from "vue";
+import ScatterPlotForm from "./ScatterPlotForm.vue";
+
+export default function () {
+ return function install(openmct) {
+ openmct.forms.addNewFormControl('scatter-plot-form-control', getScatterPlotFormControl(openmct));
+
+ openmct.types.addType(SCATTER_PLOT_KEY, {
+ key: SCATTER_PLOT_KEY,
+ name: "Scatter Plot",
+ cssClass: "icon-plot-scatter",
+ description: "View data as a scatter plot.",
+ creatable: true,
+ initialize: function (domainObject) {
+ domainObject.composition = [];
+ domainObject.configuration = {
+ styles: {},
+ axes: {},
+ ranges: {}
+ };
+ },
+ form: [
+ {
+ name: 'Underlay data (JSON file)',
+ key: 'selectFile',
+ control: 'file-input',
+ text: 'Select File...',
+ type: 'application/json',
+ removable: true,
+ hideFromInspector: true,
+ property: [
+ "selectFile"
+ ]
+ },
+ {
+ name: "Underlay ranges",
+ control: "scatter-plot-form-control",
+ cssClass: "l-input",
+ key: "scatterPlotForm",
+ required: false,
+ hideFromInspector: false,
+ property: [
+ "configuration",
+ "ranges"
+ ],
+ validate: ({ value }, callback) => {
+ const { rangeMin, rangeMax, domainMin, domainMax } = value;
+ const valid = {
+ rangeMin,
+ rangeMax,
+ domainMin,
+ domainMax
+ };
+
+ if (callback) {
+ callback(valid);
+ }
+
+ const values = Object.values(valid);
+ const hasAllValues = values.every(rangeValue => rangeValue !== undefined);
+ const hasNoValues = values.every(rangeValue => rangeValue === undefined);
+
+ return hasAllValues || hasNoValues;
+ }
+ }
+ ],
+ priority: 891
+ });
+
+ openmct.objectViews.addProvider(new ScatterPlotViewProvider(openmct));
+
+ openmct.inspectorViews.addProvider(new ScatterPlotInspectorViewProvider(openmct));
+
+ openmct.composition.addPolicy(new ScatterPlotCompositionPolicy(openmct).allow);
+ };
+
+ function getScatterPlotFormControl(openmct) {
+ return {
+ show(element, model, onChange) {
+ const rowComponent = new Vue({
+ el: element,
+ components: {
+ ScatterPlotForm
+ },
+ provide: {
+ openmct
+ },
+ data() {
+ return {
+ model,
+ onChange
+ };
+ },
+ template: ``
+ });
+
+ return rowComponent;
+ }
+ };
+ }
+}
+
diff --git a/src/plugins/charts/scatter/pluginSpec.js b/src/plugins/charts/scatter/pluginSpec.js
new file mode 100644
index 0000000000..2eb17c7a45
--- /dev/null
+++ b/src/plugins/charts/scatter/pluginSpec.js
@@ -0,0 +1,421 @@
+/*****************************************************************************
+ * 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 Vue from "vue";
+import ScatterPlotPlugin from "./plugin";
+import ScatterPlot from './ScatterPlotView.vue';
+import EventEmitter from "EventEmitter";
+import { SCATTER_PLOT_VIEW, SCATTER_PLOT_KEY } from './scatterPlotConstants';
+
+describe("the plugin", function () {
+ let element;
+ let child;
+ let openmct;
+ let telemetryPromise;
+ let telemetryPromiseResolve;
+ let mockObjectPath;
+
+ beforeEach((done) => {
+ mockObjectPath = [
+ {
+ name: 'mock folder',
+ type: 'fake-folder',
+ identifier: {
+ key: 'mock-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'
+ }
+ ];
+
+ openmct = createOpenMct();
+
+ telemetryPromise = new Promise((resolve) => {
+ telemetryPromiseResolve = resolve;
+ });
+
+ spyOn(openmct.telemetry, 'request').and.callFake(() => {
+ telemetryPromiseResolve(testTelemetry);
+
+ return telemetryPromise;
+ });
+
+ openmct.install(new ScatterPlotPlugin());
+
+ 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.time.timeSystem("utc", {
+ start: 0,
+ end: 4
+ });
+
+ openmct.types.addType("test-object", {
+ creatable: true
+ });
+
+ openmct.on("start", done);
+ openmct.startHeadless();
+ });
+
+ afterEach((done) => {
+ openmct.time.timeSystem('utc', {
+ start: 0,
+ end: 1
+ });
+ resetApplicationState(openmct).then(done).catch(done);
+ });
+
+ describe("The scatter plot view", () => {
+ let testDomainObject;
+ let scatterPlotObject;
+ // eslint-disable-next-line no-unused-vars
+ let component;
+ let mockComposition;
+
+ beforeEach(async () => {
+ scatterPlotObject = {
+ identifier: {
+ namespace: "",
+ key: "test-plot"
+ },
+ type: "telemetry.plot.scatter-plot",
+ name: "Test Scatter Plot",
+ configuration: {
+ axes: {},
+ styles: {}
+ }
+ };
+
+ testDomainObject = {
+ 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
+ }
+ }]
+ }
+ };
+
+ mockComposition = new EventEmitter();
+ mockComposition.load = () => {
+ mockComposition.emit('add', testDomainObject);
+
+ return [testDomainObject];
+ };
+
+ spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
+
+ let viewContainer = document.createElement("div");
+ child.append(viewContainer);
+ component = new Vue({
+ el: viewContainer,
+ components: {
+ ScatterPlot
+ },
+ provide: {
+ openmct: openmct,
+ domainObject: scatterPlotObject,
+ composition: openmct.composition.get(scatterPlotObject)
+ },
+ template: ""
+ });
+
+ await Vue.nextTick();
+ });
+
+ it("provides a scatter plot view", () => {
+ const applicableViews = openmct.objectViews.get(scatterPlotObject, mockObjectPath);
+ const plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === SCATTER_PLOT_VIEW);
+ expect(plotViewProvider).toBeDefined();
+ });
+
+ it("Renders plotly scatter plot", () => {
+ let scatterPlotElement = element.querySelectorAll(".plotly");
+ expect(scatterPlotElement.length).toBe(1);
+ });
+ });
+
+ describe("the scatter plot objects", () => {
+ const mockObject = {
+ name: 'A very nice scatter plot',
+ key: SCATTER_PLOT_KEY,
+ creatable: true
+ };
+
+ it('defines a scatter plot object type with the correct key', () => {
+ const objectDef = openmct.types.get(SCATTER_PLOT_KEY).definition;
+ expect(objectDef.key).toEqual(mockObject.key);
+ });
+
+ it('is creatable', () => {
+ const objectDef = openmct.types.get(SCATTER_PLOT_KEY).definition;
+ expect(objectDef.creatable).toEqual(mockObject.creatable);
+ });
+ });
+
+ describe("The scatter plot composition policy", () => {
+ it("allows composition for telemetry that contain at least 2 ranges", () => {
+ const parent = {
+ "composition": [],
+ "configuration": {
+ axes: {},
+ styles: {}
+ },
+ "name": "Some Scatter Plot",
+ "type": "telemetry.plot.scatter-plot",
+ "location": "mine",
+ "modified": 1631005183584,
+ "persisted": 1631005183502,
+ "identifier": {
+ "namespace": "",
+ "key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9"
+ }
+ };
+ const testTelemetryObject = {
+ identifier: {
+ namespace: "",
+ key: "test-object"
+ },
+ type: "test-object",
+ name: "Test Object",
+ telemetry: {
+ values: [{
+ key: "some-key",
+ name: "Some attribute",
+ hints: {
+ domain: 1
+ }
+ }, {
+ key: "some-other-key",
+ name: "Another attribute",
+ hints: {
+ range: 1
+ }
+ }, {
+ key: "some-other-key2",
+ name: "Another attribute2",
+ hints: {
+ range: 2
+ }
+ }]
+ }
+ };
+ const composition = openmct.composition.get(parent);
+ expect(() => {
+ composition.add(testTelemetryObject);
+ }).not.toThrow();
+ expect(parent.composition.length).toBe(1);
+ });
+
+ it("disallows composition for telemetry that don't contain at least 2 range hints", () => {
+ const parent = {
+ "composition": [],
+ "configuration": {
+ axes: {},
+ styles: {}
+ },
+ "name": "Some Scatter Plot",
+ "type": "telemetry.plot.scatter-plot",
+ "location": "mine",
+ "modified": 1631005183584,
+ "persisted": 1631005183502,
+ "identifier": {
+ "namespace": "",
+ "key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9"
+ }
+ };
+ const testTelemetryObject = {
+ identifier: {
+ namespace: "",
+ key: "test-object"
+ },
+ type: "test-object",
+ name: "Test Object",
+ telemetry: {
+ values: [{
+ key: "some-key",
+ name: "Some attribute",
+ hints: {
+ domain: 1
+ }
+ }, {
+ key: "some-other-key",
+ name: "Another attribute",
+ hints: {
+ range: 1
+ }
+ }]
+ }
+ };
+ const composition = openmct.composition.get(parent);
+ expect(() => {
+ composition.add(testTelemetryObject);
+ }).toThrow();
+ expect(parent.composition.length).toBe(0);
+ });
+ });
+ describe('the inspector view', () => {
+ let mockComposition;
+ let testDomainObject;
+ let selection;
+ let plotInspectorView;
+ let viewContainer;
+ let optionsElement;
+ beforeEach(async () => {
+ testDomainObject = {
+ 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.scatter-plot",
+ configuration: {
+ axes: {},
+ styles: {
+ }
+ },
+ composition: [
+ {
+ key: '~Some~foo.scatter'
+ }
+ ]
+ }
+ }
+ }
+ ]
+ ];
+
+ mockComposition = new EventEmitter();
+ mockComposition.load = () => {
+ mockComposition.emit('add', testDomainObject);
+
+ return [testDomainObject];
+ };
+
+ spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
+
+ viewContainer = document.createElement('div');
+ child.append(viewContainer);
+
+ const applicableViews = openmct.inspectorViews.get(selection);
+ plotInspectorView = applicableViews[0];
+ plotInspectorView.show(viewContainer);
+
+ await Vue.nextTick();
+ optionsElement = element.querySelector('.c-scatter-plot-options');
+ });
+
+ afterEach(() => {
+ plotInspectorView.destroy();
+ });
+
+ it('it renders the options', () => {
+ expect(optionsElement).toBeDefined();
+ });
+ });
+});
diff --git a/src/plugins/charts/scatter/scatterPlotConstants.js b/src/plugins/charts/scatter/scatterPlotConstants.js
new file mode 100644
index 0000000000..e458be37c6
--- /dev/null
+++ b/src/plugins/charts/scatter/scatterPlotConstants.js
@@ -0,0 +1,4 @@
+export const SCATTER_PLOT_VIEW = 'scatter-plot.view';
+export const SCATTER_PLOT_KEY = 'telemetry.plot.scatter-plot';
+export const SCATTER_PLOT_INSPECTOR_KEY = 'telemetry.plot.scatter-plot.inspector';
+export const TIME_STRIP_KEY = 'time-strip';
diff --git a/src/plugins/formActions/CreateActionSpec.js b/src/plugins/formActions/CreateActionSpec.js
new file mode 100644
index 0000000000..2071da4710
--- /dev/null
+++ b/src/plugins/formActions/CreateActionSpec.js
@@ -0,0 +1,128 @@
+/*****************************************************************************
+ * 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 CreateAction from './CreateAction';
+
+import {
+ createOpenMct,
+ resetApplicationState
+} from 'utils/testing';
+
+import { debounce } from 'lodash';
+
+let parentObject;
+let parentObjectPath;
+let unObserve;
+
+describe("The create action plugin", () => {
+ let openmct;
+
+ const TYPES = [
+ 'clock',
+ 'conditionWidget',
+ 'conditionWidget',
+ 'example.imagery',
+ 'example.state-generator',
+ 'flexible-layout',
+ 'folder',
+ 'generator',
+ 'hyperlink',
+ 'LadTable',
+ 'LadTableSet',
+ 'layout',
+ 'mmgis',
+ 'notebook',
+ 'plan',
+ 'table',
+ 'tabs',
+ 'telemetry-mean',
+ 'telemetry.plot.bar-graph',
+ 'telemetry.plot.overlay',
+ 'telemetry.plot.stacked',
+ 'time-strip',
+ 'timer',
+ 'webpage'
+ ];
+
+ beforeEach((done) => {
+ openmct = createOpenMct();
+
+ openmct.on('start', done);
+ openmct.startHeadless();
+ });
+
+ afterEach(() => {
+ return resetApplicationState(openmct);
+ });
+
+ describe('creates new objects for a', () => {
+ beforeEach(() => {
+ parentObject = {
+ name: 'mock folder',
+ type: 'folder',
+ identifier: {
+ key: 'mock-folder',
+ namespace: ''
+ },
+ composition: []
+ };
+ parentObjectPath = [parentObject];
+
+ spyOn(openmct.objects, 'save');
+ openmct.objects.save.and.callThrough();
+ spyOn(openmct.forms, 'showForm');
+ openmct.forms.showForm.and.callFake(formStructure => {
+ return Promise.resolve({
+ name: 'test',
+ notes: 'test notes',
+ location: parentObjectPath
+ });
+ });
+ });
+
+ afterEach(() => {
+ parentObject = null;
+ unObserve();
+ });
+
+ TYPES.forEach(type => {
+ it(`type ${type}`, (done) => {
+ function callback(newObject) {
+ const composition = newObject.composition;
+
+ openmct.objects.get(composition[0])
+ .then(object => {
+ expect(object.type).toEqual(type);
+ expect(object.location).toEqual(openmct.objects.makeKeyString(parentObject.identifier));
+
+ done();
+ });
+ }
+
+ const deBouncedCallback = debounce(callback, 300);
+ unObserve = openmct.objects.observe(parentObject, '*', deBouncedCallback);
+
+ const createAction = new CreateAction(openmct, type, parentObject);
+ createAction.invoke();
+ });
+ });
+ });
+});
diff --git a/src/plugins/formActions/EditPropertiesAction.js b/src/plugins/formActions/EditPropertiesAction.js
index 4937b2ab3d..53afadfe90 100644
--- a/src/plugins/formActions/EditPropertiesAction.js
+++ b/src/plugins/formActions/EditPropertiesAction.js
@@ -45,7 +45,7 @@ export default class EditPropertiesAction extends PropertiesAction {
}
invoke(objectPath) {
- this._showEditForm(objectPath);
+ return this._showEditForm(objectPath);
}
/**
@@ -86,7 +86,7 @@ export default class EditPropertiesAction extends PropertiesAction {
const formStructure = createWizard.getFormStructure(false);
formStructure.title = 'Edit ' + this.domainObject.name;
- this.openmct.forms.showForm(formStructure)
+ return this.openmct.forms.showForm(formStructure)
.then(this._onSave.bind(this));
}
}
diff --git a/src/plugins/formActions/pluginSpec.js b/src/plugins/formActions/pluginSpec.js
new file mode 100644
index 0000000000..9c4cbb2cc0
--- /dev/null
+++ b/src/plugins/formActions/pluginSpec.js
@@ -0,0 +1,222 @@
+/*****************************************************************************
+ * 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
+} from 'utils/testing';
+
+import { debounce } from 'lodash';
+
+describe('EditPropertiesAction plugin', () => {
+ let editPropertiesAction;
+ let openmct;
+ let element;
+
+ beforeEach((done) => {
+ element = document.createElement('div');
+ element.style.display = 'block';
+ element.style.width = '1920px';
+ element.style.height = '1080px';
+
+ openmct = createOpenMct();
+ openmct.on('start', done);
+ openmct.startHeadless(element);
+
+ editPropertiesAction = openmct.actions.getAction('properties');
+ });
+
+ afterEach(() => {
+ editPropertiesAction = null;
+
+ return resetApplicationState(openmct);
+ });
+
+ it('editPropertiesAction exists', () => {
+ expect(editPropertiesAction.key).toEqual('properties');
+ });
+
+ it('edit properties action applies to only persistable objects', () => {
+ spyOn(openmct.objects, 'isPersistable').and.returnValue(true);
+
+ const domainObject = {
+ name: 'mock folder',
+ type: 'folder',
+ identifier: {
+ key: 'mock-folder',
+ namespace: ''
+ },
+ composition: []
+ };
+ const isApplicableTo = editPropertiesAction.appliesTo([domainObject]);
+ expect(isApplicableTo).toBe(true);
+ });
+
+ it('edit properties action does not apply to non persistable objects', () => {
+ spyOn(openmct.objects, 'isPersistable').and.returnValue(false);
+
+ const domainObject = {
+ name: 'mock folder',
+ type: 'folder',
+ identifier: {
+ key: 'mock-folder',
+ namespace: ''
+ },
+ composition: []
+ };
+ const isApplicableTo = editPropertiesAction.appliesTo([domainObject]);
+ expect(isApplicableTo).toBe(false);
+ });
+
+ it('edit properties action when invoked shows form', (done) => {
+ const domainObject = {
+ name: 'mock folder',
+ notes: 'mock notes',
+ type: 'folder',
+ identifier: {
+ key: 'mock-folder',
+ namespace: ''
+ },
+ modified: 1643065068597,
+ persisted: 1643065068600,
+ composition: []
+ };
+
+ const deBouncedFormChange = debounce(handleFormPropertyChange, 500);
+ openmct.forms.on('onFormPropertyChange', deBouncedFormChange);
+
+ function handleFormPropertyChange(data) {
+ const form = document.querySelector('.js-form');
+ const title = form.querySelector('input');
+ expect(title.value).toEqual(domainObject.name);
+
+ const notes = form.querySelector('textArea');
+ expect(notes.value).toEqual(domainObject.notes);
+
+ const buttons = form.querySelectorAll('button');
+ expect(buttons[0].textContent.trim()).toEqual('OK');
+ expect(buttons[1].textContent.trim()).toEqual('Cancel');
+
+ const clickEvent = createMouseEvent('click');
+ buttons[1].dispatchEvent(clickEvent);
+
+ openmct.forms.off('onFormPropertyChange', deBouncedFormChange);
+ }
+
+ editPropertiesAction.invoke([domainObject])
+ .catch(() => {
+ done();
+ });
+ });
+
+ it('edit properties action saves changes', (done) => {
+ const oldName = 'mock folder';
+ const newName = 'renamed mock folder';
+ const domainObject = {
+ name: oldName,
+ notes: 'mock notes',
+ type: 'folder',
+ identifier: {
+ key: 'mock-folder',
+ namespace: ''
+ },
+ modified: 1643065068597,
+ persisted: 1643065068600,
+ composition: []
+ };
+ let unObserve;
+
+ function callback(newObject) {
+ expect(newObject.name).not.toEqual(oldName);
+ expect(newObject.name).toEqual(newName);
+
+ unObserve();
+ done();
+ }
+
+ const deBouncedCallback = debounce(callback, 300);
+ unObserve = openmct.objects.observe(domainObject, '*', deBouncedCallback);
+
+ let changed = false;
+ const deBouncedFormChange = debounce(handleFormPropertyChange, 500);
+ openmct.forms.on('onFormPropertyChange', deBouncedFormChange);
+
+ function handleFormPropertyChange(data) {
+ const form = document.querySelector('.js-form');
+ const title = form.querySelector('input');
+ const notes = form.querySelector('textArea');
+
+ const buttons = form.querySelectorAll('button');
+ expect(buttons[0].textContent.trim()).toEqual('OK');
+ expect(buttons[1].textContent.trim()).toEqual('Cancel');
+
+ if (!changed) {
+ expect(title.value).toEqual(domainObject.name);
+ expect(notes.value).toEqual(domainObject.notes);
+
+ // change input field value and dispatch event for it
+ title.focus();
+ title.value = newName;
+ title.dispatchEvent(new Event('input'));
+ title.blur();
+
+ changed = true;
+ } else {
+ // click ok to save form changes
+ const clickEvent = createMouseEvent('click');
+ buttons[0].dispatchEvent(clickEvent);
+
+ openmct.forms.off('onFormPropertyChange', deBouncedFormChange);
+ }
+ }
+
+ editPropertiesAction.invoke([domainObject]);
+ });
+
+ it('edit properties action discards changes', (done) => {
+ const name = 'mock folder';
+ const domainObject = {
+ name,
+ notes: 'mock notes',
+ type: 'folder',
+ identifier: {
+ key: 'mock-folder',
+ namespace: ''
+ },
+ modified: 1643065068597,
+ persisted: 1643065068600,
+ composition: []
+ };
+
+ editPropertiesAction.invoke([domainObject])
+ .catch(() => {
+ expect(domainObject.name).toEqual(name);
+
+ done();
+ });
+
+ const form = document.querySelector('.js-form');
+ const buttons = form.querySelectorAll('button');
+ const clickEvent = createMouseEvent('click');
+ buttons[1].dispatchEvent(clickEvent);
+ });
+});
diff --git a/src/plugins/imagery/components/ImageryView.vue b/src/plugins/imagery/components/ImageryView.vue
index a0ef2b8e4e..ad63b623df 100644
--- a/src/plugins/imagery/components/ImageryView.vue
+++ b/src/plugins/imagery/components/ImageryView.vue
@@ -55,7 +55,7 @@
{{formatImageAltText}}
+ >{{ formatImageAltText }}
{
});
describe("when moving an object to a new parent and removing from the old parent", () => {
+ let unObserve;
beforeEach((done) => {
openmct.router.path = [];
@@ -104,7 +105,7 @@ describe("The Move Action plugin", () => {
});
});
- openmct.objects.observe(parentObject, '*', (newObject) => {
+ unObserve = openmct.objects.observe(parentObject, '*', (newObject) => {
done();
});
@@ -113,6 +114,10 @@ describe("The Move Action plugin", () => {
moveAction.invoke([childObject, parentObject]);
});
+ afterEach(() => {
+ unObserve();
+ });
+
it("the child object's identifier should be in the new parent's composition", () => {
let newParentChild = anotherParentObject.composition[0];
expect(newParentChild).toEqual(childObject.identifier);
diff --git a/src/plugins/notebook/components/NotebookEntry.vue b/src/plugins/notebook/components/NotebookEntry.vue
index 9261063a02..6f45cd389a 100644
--- a/src/plugins/notebook/components/NotebookEntry.vue
+++ b/src/plugins/notebook/components/NotebookEntry.vue
@@ -28,12 +28,16 @@
@drop.prevent="dropOnEntry"
>
-
-
- {{ entry.createdBy }}
-
-
{{ createdOnDate }}
-
{{ createdOnTime }}
+
+ {{ createdOnDate }}
+ {{ createdOnTime }}
+
+
+ {{ entry.createdBy }}
+
diff --git a/src/plugins/plan/Plan.vue b/src/plugins/plan/Plan.vue
index 1f34e97a6f..89ad45bdd8 100644
--- a/src/plugins/plan/Plan.vue
+++ b/src/plugins/plan/Plan.vue
@@ -49,7 +49,7 @@
import * as d3Scale from 'd3-scale';
import TimelineAxis from "../../ui/components/TimeSystemAxis.vue";
import SwimLane from "@/ui/components/swim-lane/SwimLane.vue";
-import { getValidatedPlan } from "./util";
+import { getValidatedData } from "./util";
import Vue from "vue";
const PADDING = 1;
@@ -161,7 +161,7 @@ export default {
return clientWidth - 200;
},
getPlanData(domainObject) {
- this.planData = getValidatedPlan(domainObject);
+ this.planData = getValidatedData(domainObject);
},
updateViewBounds(bounds) {
if (bounds) {
diff --git a/src/plugins/plan/util.js b/src/plugins/plan/util.js
index cf119412f4..c5e3cc6b83 100644
--- a/src/plugins/plan/util.js
+++ b/src/plugins/plan/util.js
@@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
-export function getValidatedPlan(domainObject) {
+export function getValidatedData(domainObject) {
let sourceMap = domainObject.sourceMap;
let body = domainObject.selectFile.body;
let json = {};
diff --git a/src/plugins/plugins.js b/src/plugins/plugins.js
index 6b20e3c318..7beccebd54 100644
--- a/src/plugins/plugins.js
+++ b/src/plugins/plugins.js
@@ -37,7 +37,8 @@ define([
'./URLIndicatorPlugin/URLIndicatorPlugin',
'./telemetryMean/plugin',
'./plot/plugin',
- './charts/plugin',
+ './charts/bar/plugin',
+ './charts/scatter/plugin',
'./telemetryTable/plugin',
'./staticRootPlugin/plugin',
'./notebook/plugin',
@@ -96,7 +97,8 @@ define([
URLIndicatorPlugin,
TelemetryMean,
PlotPlugin,
- ChartPlugin,
+ BarChartPlugin,
+ ScatterPlotPlugin,
TelemetryTablePlugin,
StaticRootPlugin,
Notebook,
@@ -172,7 +174,8 @@ define([
plugins.ImageryPlugin = ImageryPlugin;
plugins.Plot = PlotPlugin.default;
- plugins.Chart = ChartPlugin.default;
+ plugins.BarChart = BarChartPlugin.default;
+ plugins.ScatterPlot = ScatterPlotPlugin.default;
plugins.TelemetryTable = TelemetryTablePlugin;
plugins.SummaryWidget = SummaryWidget;
diff --git a/src/plugins/timeline/TimelineViewLayout.vue b/src/plugins/timeline/TimelineViewLayout.vue
index b1764ec1ab..cc327b22a8 100644
--- a/src/plugins/timeline/TimelineViewLayout.vue
+++ b/src/plugins/timeline/TimelineViewLayout.vue
@@ -61,7 +61,7 @@
import TimelineObjectView from './TimelineObjectView.vue';
import TimelineAxis from '../../ui/components/TimeSystemAxis.vue';
import SwimLane from "@/ui/components/swim-lane/SwimLane.vue";
-import { getValidatedPlan } from "../plan/util";
+import { getValidatedData } from "../plan/util";
const unknownObjectType = {
definition: {
@@ -110,7 +110,7 @@ export default {
let objectPath = [domainObject].concat(this.objectPath.slice());
let rowCount = 0;
if (domainObject.type === 'plan') {
- rowCount = Object.keys(getValidatedPlan(domainObject)).length;
+ rowCount = Object.keys(getValidatedData(domainObject)).length;
}
let height = domainObject.type === 'telemetry.plot.stacked' ? `${domainObject.composition.length * 100}px` : '100px';
diff --git a/src/plugins/timelist/Timelist.vue b/src/plugins/timelist/Timelist.vue
index 517822cd6b..188c958008 100644
--- a/src/plugins/timelist/Timelist.vue
+++ b/src/plugins/timelist/Timelist.vue
@@ -35,7 +35,7 @@