chore: add prettier (2/3): apply formatting, re-enable lint ci step (#6682)

* style: apply prettier formatting

* fix: re-enable lint ci check
This commit is contained in:
Jesse Mazzella
2023-05-18 14:54:46 -07:00
committed by GitHub
parent 172e0b23fd
commit caa7bc6fae
976 changed files with 115922 additions and 114693 deletions

View File

@@ -21,390 +21,392 @@
*****************************************************************************/
/* eslint-disable no-undef */
define([
'EventEmitter',
'./api/api',
'./api/overlays/OverlayAPI',
'./selection/Selection',
'./plugins/plugins',
'./ui/registries/ViewRegistry',
'./plugins/imagery/plugin',
'./ui/registries/InspectorViewRegistry',
'./ui/registries/ToolbarRegistry',
'./ui/router/ApplicationRouter',
'./ui/router/Browse',
'./ui/layout/Layout.vue',
'./ui/preview/plugin',
'./api/Branding',
'./plugins/licenses/plugin',
'./plugins/remove/plugin',
'./plugins/move/plugin',
'./plugins/linkAction/plugin',
'./plugins/duplicate/plugin',
'./plugins/importFromJSONAction/plugin',
'./plugins/exportAsJSONAction/plugin',
'./ui/components/components',
'vue'
'EventEmitter',
'./api/api',
'./api/overlays/OverlayAPI',
'./selection/Selection',
'./plugins/plugins',
'./ui/registries/ViewRegistry',
'./plugins/imagery/plugin',
'./ui/registries/InspectorViewRegistry',
'./ui/registries/ToolbarRegistry',
'./ui/router/ApplicationRouter',
'./ui/router/Browse',
'./ui/layout/Layout.vue',
'./ui/preview/plugin',
'./api/Branding',
'./plugins/licenses/plugin',
'./plugins/remove/plugin',
'./plugins/move/plugin',
'./plugins/linkAction/plugin',
'./plugins/duplicate/plugin',
'./plugins/importFromJSONAction/plugin',
'./plugins/exportAsJSONAction/plugin',
'./ui/components/components',
'vue'
], function (
EventEmitter,
api,
OverlayAPI,
Selection,
plugins,
ViewRegistry,
ImageryPlugin,
InspectorViewRegistry,
ToolbarRegistry,
ApplicationRouter,
Browse,
Layout,
PreviewPlugin,
BrandingAPI,
LicensesPlugin,
RemoveActionPlugin,
MoveActionPlugin,
LinkActionPlugin,
DuplicateActionPlugin,
ImportFromJSONAction,
ExportAsJSONAction,
components,
Vue
EventEmitter,
api,
OverlayAPI,
Selection,
plugins,
ViewRegistry,
ImageryPlugin,
InspectorViewRegistry,
ToolbarRegistry,
ApplicationRouter,
Browse,
Layout,
PreviewPlugin,
BrandingAPI,
LicensesPlugin,
RemoveActionPlugin,
MoveActionPlugin,
LinkActionPlugin,
DuplicateActionPlugin,
ImportFromJSONAction,
ExportAsJSONAction,
components,
Vue
) {
/**
* Open MCT is an extensible web application for building mission
* control user interfaces. This module is itself an instance of
* [MCT]{@link module:openmct.MCT}, which provides an interface for
* configuring and executing the application.
*
* @exports openmct
*/
/**
* Open MCT is an extensible web application for building mission
* control user interfaces. This module is itself an instance of
* [MCT]{@link module:openmct.MCT}, which provides an interface for
* configuring and executing the application.
*
* @exports openmct
*/
/**
* The Open MCT application. This may be configured by installing plugins
* or registering extensions before the application is started.
* @constructor
* @memberof module:openmct
*/
function MCT() {
EventEmitter.call(this);
this.buildInfo = {
version: __OPENMCT_VERSION__,
buildDate: __OPENMCT_BUILD_DATE__,
revision: __OPENMCT_REVISION__,
branch: __OPENMCT_BUILD_BRANCH__
};
this.destroy = this.destroy.bind(this);
[
/**
* Tracks current selection state of the application.
* @private
*/
['selection', () => new Selection.default(this)],
/**
* MCT's time conductor, which may be used to synchronize view contents
* for telemetry- or time-based views.
* @type {module:openmct.TimeConductor}
* @memberof module:openmct.MCT#
* @name conductor
*/
['time', () => new api.TimeAPI(this)],
/**
* An interface for interacting with the composition of domain objects.
* The composition of a domain object is the list of other domain
* objects it "contains" (for instance, that should be displayed
* beneath it in the tree.)
*
* `composition` may be called as a function, in which case it acts
* as [`composition.get`]{@link module:openmct.CompositionAPI#get}.
*
* @type {module:openmct.CompositionAPI}
* @memberof module:openmct.MCT#
* @name composition
*/
['composition', () => new api.CompositionAPI.default(this)],
/**
* Registry for views of domain objects which should appear in the
* main viewing area.
*
* @type {module:openmct.ViewRegistry}
* @memberof module:openmct.MCT#
* @name objectViews
*/
['objectViews', () => new ViewRegistry()],
/**
* Registry for views which should appear in the Inspector area.
* These views will be chosen based on the selection state.
*
* @type {module:openmct.InspectorViewRegistry}
* @memberof module:openmct.MCT#
* @name inspectorViews
*/
['inspectorViews', () => new InspectorViewRegistry.default()],
/**
* Registry for views which should appear in Edit Properties
* dialogs, and similar user interface elements used for
* modifying domain objects external to its regular views.
*
* @type {module:openmct.ViewRegistry}
* @memberof module:openmct.MCT#
* @name propertyEditors
*/
['propertyEditors', () => new ViewRegistry()],
/**
* Registry for views which should appear in the toolbar area while
* editing. These views will be chosen based on the selection state.
*
* @type {module:openmct.ToolbarRegistry}
* @memberof module:openmct.MCT#
* @name toolbars
*/
['toolbars', () => new ToolbarRegistry()],
/**
* Registry for domain object types which may exist within this
* instance of Open MCT.
*
* @type {module:openmct.TypeRegistry}
* @memberof module:openmct.MCT#
* @name types
*/
['types', () => new api.TypeRegistry()],
/**
* An interface for interacting with domain objects and the domain
* object hierarchy.
*
* @type {module:openmct.ObjectAPI}
* @memberof module:openmct.MCT#
* @name objects
*/
['objects', () => new api.ObjectAPI.default(this.types, this)],
/**
* An interface for retrieving and interpreting telemetry data associated
* with a domain object.
*
* @type {module:openmct.TelemetryAPI}
* @memberof module:openmct.MCT#
* @name telemetry
*/
['telemetry', () => new api.TelemetryAPI.default(this)],
/**
* An interface for creating new indicators and changing them dynamically.
*
* @type {module:openmct.IndicatorAPI}
* @memberof module:openmct.MCT#
* @name indicators
*/
['indicators', () => new api.IndicatorAPI(this)],
/**
* MCT's user awareness management, to enable user and
* role specific functionality.
* @type {module:openmct.UserAPI}
* @memberof module:openmct.MCT#
* @name user
*/
['user', () => new api.UserAPI(this)],
['notifications', () => new api.NotificationAPI()],
['editor', () => new api.EditorAPI.default(this)],
['overlays', () => new OverlayAPI.default()],
['menus', () => new api.MenuAPI(this)],
['actions', () => new api.ActionsAPI(this)],
['status', () => new api.StatusAPI(this)],
['priority', () => api.PriorityAPI],
['router', () => new ApplicationRouter(this)],
['faults', () => new api.FaultManagementAPI.default(this)],
['forms', () => new api.FormsAPI.default(this)],
['branding', () => BrandingAPI.default],
/**
* MCT's annotation API that enables
* human-created comments and categorization linked to data products
* @type {module:openmct.AnnotationAPI}
* @memberof module:openmct.MCT#
* @name annotation
*/
['annotation', () => new api.AnnotationAPI(this)]
].forEach((apiEntry) => {
const apiName = apiEntry[0];
const apiObject = apiEntry[1]();
Object.defineProperty(this, apiName, {
value: apiObject,
enumerable: false,
configurable: false,
writable: true
});
});
/**
* The Open MCT application. This may be configured by installing plugins
* or registering extensions before the application is started.
* @constructor
* @memberof module:openmct
* MCT's annotation API that enables
* human-created comments and categorization linked to data products
* @type {module:openmct.AnnotationAPI}
* @memberof module:openmct.MCT#
* @name annotation
*/
function MCT() {
EventEmitter.call(this);
this.buildInfo = {
version: __OPENMCT_VERSION__,
buildDate: __OPENMCT_BUILD_DATE__,
revision: __OPENMCT_REVISION__,
branch: __OPENMCT_BUILD_BRANCH__
};
this.annotation = new api.AnnotationAPI(this);
this.destroy = this.destroy.bind(this);
[
/**
* Tracks current selection state of the application.
* @private
*/
['selection', () => new Selection.default(this)],
// Plugins that are installed by default
this.install(this.plugins.Plot());
this.install(this.plugins.TelemetryTable.default());
this.install(PreviewPlugin.default());
this.install(LicensesPlugin.default());
this.install(RemoveActionPlugin.default());
this.install(MoveActionPlugin.default());
this.install(LinkActionPlugin.default());
this.install(DuplicateActionPlugin.default());
this.install(ExportAsJSONAction.default());
this.install(ImportFromJSONAction.default());
this.install(this.plugins.FormActions.default());
this.install(this.plugins.FolderView());
this.install(this.plugins.Tabs());
this.install(ImageryPlugin.default());
this.install(this.plugins.FlexibleLayout());
this.install(this.plugins.GoToOriginalAction());
this.install(this.plugins.OpenInNewTabAction());
this.install(this.plugins.WebPage());
this.install(this.plugins.Condition());
this.install(this.plugins.ConditionWidget());
this.install(this.plugins.URLTimeSettingsSynchronizer());
this.install(this.plugins.NotificationIndicator());
this.install(this.plugins.NewFolderAction());
this.install(this.plugins.ViewDatumAction());
this.install(this.plugins.ViewLargeAction());
this.install(this.plugins.ObjectInterceptors());
this.install(this.plugins.DeviceClassifier());
this.install(this.plugins.UserIndicator());
this.install(this.plugins.Gauge());
this.install(this.plugins.InspectorViews());
}
/**
* MCT's time conductor, which may be used to synchronize view contents
* for telemetry- or time-based views.
* @type {module:openmct.TimeConductor}
* @memberof module:openmct.MCT#
* @name conductor
*/
['time', () => new api.TimeAPI(this)],
MCT.prototype = Object.create(EventEmitter.prototype);
/**
* An interface for interacting with the composition of domain objects.
* The composition of a domain object is the list of other domain
* objects it "contains" (for instance, that should be displayed
* beneath it in the tree.)
*
* `composition` may be called as a function, in which case it acts
* as [`composition.get`]{@link module:openmct.CompositionAPI#get}.
*
* @type {module:openmct.CompositionAPI}
* @memberof module:openmct.MCT#
* @name composition
*/
['composition', () => new api.CompositionAPI.default(this)],
MCT.prototype.MCT = MCT;
/**
* Registry for views of domain objects which should appear in the
* main viewing area.
*
* @type {module:openmct.ViewRegistry}
* @memberof module:openmct.MCT#
* @name objectViews
*/
['objectViews', () => new ViewRegistry()],
/**
* Set path to where assets are hosted. This should be the path to main.js.
* @memberof module:openmct.MCT#
* @method setAssetPath
*/
MCT.prototype.setAssetPath = function (assetPath) {
this._assetPath = assetPath;
};
/**
* Registry for views which should appear in the Inspector area.
* These views will be chosen based on the selection state.
*
* @type {module:openmct.InspectorViewRegistry}
* @memberof module:openmct.MCT#
* @name inspectorViews
*/
['inspectorViews', () => new InspectorViewRegistry.default()],
/**
* Registry for views which should appear in Edit Properties
* dialogs, and similar user interface elements used for
* modifying domain objects external to its regular views.
*
* @type {module:openmct.ViewRegistry}
* @memberof module:openmct.MCT#
* @name propertyEditors
*/
['propertyEditors', () => new ViewRegistry()],
/**
* Registry for views which should appear in the toolbar area while
* editing. These views will be chosen based on the selection state.
*
* @type {module:openmct.ToolbarRegistry}
* @memberof module:openmct.MCT#
* @name toolbars
*/
['toolbars', () => new ToolbarRegistry()],
/**
* Registry for domain object types which may exist within this
* instance of Open MCT.
*
* @type {module:openmct.TypeRegistry}
* @memberof module:openmct.MCT#
* @name types
*/
['types', () => new api.TypeRegistry()],
/**
* An interface for interacting with domain objects and the domain
* object hierarchy.
*
* @type {module:openmct.ObjectAPI}
* @memberof module:openmct.MCT#
* @name objects
*/
['objects', () => new api.ObjectAPI.default(this.types, this)],
/**
* An interface for retrieving and interpreting telemetry data associated
* with a domain object.
*
* @type {module:openmct.TelemetryAPI}
* @memberof module:openmct.MCT#
* @name telemetry
*/
['telemetry', () => new api.TelemetryAPI.default(this)],
/**
* An interface for creating new indicators and changing them dynamically.
*
* @type {module:openmct.IndicatorAPI}
* @memberof module:openmct.MCT#
* @name indicators
*/
['indicators', () => new api.IndicatorAPI(this)],
/**
* MCT's user awareness management, to enable user and
* role specific functionality.
* @type {module:openmct.UserAPI}
* @memberof module:openmct.MCT#
* @name user
*/
['user', () => new api.UserAPI(this)],
['notifications', () => new api.NotificationAPI()],
['editor', () => new api.EditorAPI.default(this)],
['overlays', () => new OverlayAPI.default()],
['menus', () => new api.MenuAPI(this)],
['actions', () => new api.ActionsAPI(this)],
['status', () => new api.StatusAPI(this)],
['priority', () => api.PriorityAPI],
['router', () => new ApplicationRouter(this)],
['faults', () => new api.FaultManagementAPI.default(this)],
['forms', () => new api.FormsAPI.default(this)],
['branding', () => BrandingAPI.default],
/**
* MCT's annotation API that enables
* human-created comments and categorization linked to data products
* @type {module:openmct.AnnotationAPI}
* @memberof module:openmct.MCT#
* @name annotation
*/
['annotation', () => new api.AnnotationAPI(this)]
].forEach(apiEntry => {
const apiName = apiEntry[0];
const apiObject = apiEntry[1]();
Object.defineProperty(this, apiName, {
value: apiObject,
enumerable: false,
configurable: false,
writable: true
});
});
/**
* MCT's annotation API that enables
* human-created comments and categorization linked to data products
* @type {module:openmct.AnnotationAPI}
* @memberof module:openmct.MCT#
* @name annotation
*/
this.annotation = new api.AnnotationAPI(this);
// Plugins that are installed by default
this.install(this.plugins.Plot());
this.install(this.plugins.TelemetryTable.default());
this.install(PreviewPlugin.default());
this.install(LicensesPlugin.default());
this.install(RemoveActionPlugin.default());
this.install(MoveActionPlugin.default());
this.install(LinkActionPlugin.default());
this.install(DuplicateActionPlugin.default());
this.install(ExportAsJSONAction.default());
this.install(ImportFromJSONAction.default());
this.install(this.plugins.FormActions.default());
this.install(this.plugins.FolderView());
this.install(this.plugins.Tabs());
this.install(ImageryPlugin.default());
this.install(this.plugins.FlexibleLayout());
this.install(this.plugins.GoToOriginalAction());
this.install(this.plugins.OpenInNewTabAction());
this.install(this.plugins.WebPage());
this.install(this.plugins.Condition());
this.install(this.plugins.ConditionWidget());
this.install(this.plugins.URLTimeSettingsSynchronizer());
this.install(this.plugins.NotificationIndicator());
this.install(this.plugins.NewFolderAction());
this.install(this.plugins.ViewDatumAction());
this.install(this.plugins.ViewLargeAction());
this.install(this.plugins.ObjectInterceptors());
this.install(this.plugins.DeviceClassifier());
this.install(this.plugins.UserIndicator());
this.install(this.plugins.Gauge());
this.install(this.plugins.InspectorViews());
/**
* Get path to where assets are hosted.
* @memberof module:openmct.MCT#
* @method getAssetPath
*/
MCT.prototype.getAssetPath = function () {
const assetPathLength = this._assetPath && this._assetPath.length;
if (!assetPathLength) {
return '/';
}
MCT.prototype = Object.create(EventEmitter.prototype);
if (this._assetPath[assetPathLength - 1] !== '/') {
return this._assetPath + '/';
}
MCT.prototype.MCT = MCT;
return this._assetPath;
};
/**
* Start running Open MCT. This should be called only after any plugins
* have been installed.
* @fires module:openmct.MCT~start
* @memberof module:openmct.MCT#
* @method start
* @param {HTMLElement} [domElement] the DOM element in which to run
* MCT; if undefined, MCT will be run in the body of the document
*/
MCT.prototype.start = function (domElement = document.body, isHeadlessMode = false) {
if (this.types.get('layout') === undefined) {
this.install(
this.plugins.DisplayLayout({
showAsView: ['summary-widget']
})
);
}
this.element = domElement;
this.router.route(/^\/$/, () => {
this.router.setPath('/browse/');
});
/**
* Set path to where assets are hosted. This should be the path to main.js.
* @memberof module:openmct.MCT#
* @method setAssetPath
* Fired by [MCT]{@link module:openmct.MCT} when the application
* is started.
* @event start
* @memberof module:openmct.MCT~
*/
MCT.prototype.setAssetPath = function (assetPath) {
this._assetPath = assetPath;
};
/**
* Get path to where assets are hosted.
* @memberof module:openmct.MCT#
* @method getAssetPath
*/
MCT.prototype.getAssetPath = function () {
const assetPathLength = this._assetPath && this._assetPath.length;
if (!assetPathLength) {
return '/';
}
if (!isHeadlessMode) {
const appLayout = new Vue({
components: {
Layout: Layout.default
},
provide: {
openmct: this
},
template: '<Layout ref="layout"></Layout>'
});
domElement.appendChild(appLayout.$mount().$el);
if (this._assetPath[assetPathLength - 1] !== '/') {
return this._assetPath + '/';
}
this.layout = appLayout.$refs.layout;
Browse(this);
}
return this._assetPath;
};
window.addEventListener('beforeunload', this.destroy);
/**
* Start running Open MCT. This should be called only after any plugins
* have been installed.
* @fires module:openmct.MCT~start
* @memberof module:openmct.MCT#
* @method start
* @param {HTMLElement} [domElement] the DOM element in which to run
* MCT; if undefined, MCT will be run in the body of the document
*/
MCT.prototype.start = function (domElement = document.body, isHeadlessMode = false) {
if (this.types.get('layout') === undefined) {
this.install(this.plugins.DisplayLayout({
showAsView: ['summary-widget']
}));
}
this.router.start();
this.emit('start');
};
this.element = domElement;
MCT.prototype.startHeadless = function () {
let unreachableNode = document.createElement('div');
this.router.route(/^\/$/, () => {
this.router.setPath('/browse/');
});
return this.start(unreachableNode, true);
};
/**
* Fired by [MCT]{@link module:openmct.MCT} when the application
* is started.
* @event start
* @memberof module:openmct.MCT~
*/
/**
* Install a plugin in MCT.
*
* @param {Function} plugin a plugin install function which will be
* invoked with the mct instance.
* @memberof module:openmct.MCT#
*/
MCT.prototype.install = function (plugin) {
plugin(this);
};
if (!isHeadlessMode) {
const appLayout = new Vue({
components: {
'Layout': Layout.default
},
provide: {
openmct: this
},
template: '<Layout ref="layout"></Layout>'
});
domElement.appendChild(appLayout.$mount().$el);
MCT.prototype.destroy = function () {
window.removeEventListener('beforeunload', this.destroy);
this.emit('destroy');
this.router.destroy();
};
this.layout = appLayout.$refs.layout;
Browse(this);
}
MCT.prototype.plugins = plugins;
MCT.prototype.components = components.default;
window.addEventListener('beforeunload', this.destroy);
this.router.start();
this.emit('start');
};
MCT.prototype.startHeadless = function () {
let unreachableNode = document.createElement('div');
return this.start(unreachableNode, true);
};
/**
* Install a plugin in MCT.
*
* @param {Function} plugin a plugin install function which will be
* invoked with the mct instance.
* @memberof module:openmct.MCT#
*/
MCT.prototype.install = function (plugin) {
plugin(this);
};
MCT.prototype.destroy = function () {
window.removeEventListener('beforeunload', this.destroy);
this.emit('destroy');
this.router.destroy();
};
MCT.prototype.plugins = plugins;
MCT.prototype.components = components.default;
return MCT;
return MCT;
});

View File

@@ -20,99 +20,96 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'./plugins/plugins',
'utils/testing'
], function (plugins, testUtils) {
describe("MCT", function () {
let openmct;
let mockPlugin;
let mockPlugin2;
let mockListener;
define(['./plugins/plugins', 'utils/testing'], function (plugins, testUtils) {
describe('MCT', function () {
let openmct;
let mockPlugin;
let mockPlugin2;
let mockListener;
beforeEach(function () {
mockPlugin = jasmine.createSpy('plugin');
mockPlugin2 = jasmine.createSpy('plugin2');
mockListener = jasmine.createSpy('listener');
beforeEach(function () {
mockPlugin = jasmine.createSpy('plugin');
mockPlugin2 = jasmine.createSpy('plugin2');
mockListener = jasmine.createSpy('listener');
openmct = testUtils.createOpenMct();
openmct = testUtils.createOpenMct();
openmct.install(mockPlugin);
openmct.install(mockPlugin2);
openmct.on('start', mockListener);
});
// Clean up the dirty singleton.
afterEach(function () {
return testUtils.resetApplicationState(openmct);
});
it("exposes plugins", function () {
expect(openmct.plugins).toEqual(plugins);
});
it("does not issue a start event before started", function () {
expect(mockListener).not.toHaveBeenCalled();
});
describe("start", function () {
let appHolder;
beforeEach(function (done) {
appHolder = document.createElement("div");
openmct.on('start', done);
openmct.start(appHolder);
});
it("calls plugins for configuration", function () {
expect(mockPlugin).toHaveBeenCalledWith(openmct);
expect(mockPlugin2).toHaveBeenCalledWith(openmct);
});
it("emits a start event", function () {
expect(mockListener).toHaveBeenCalled();
});
it("Renders the application into the provided container element", function () {
let openMctShellElements = appHolder.querySelectorAll('div.l-shell');
expect(openMctShellElements.length).toBe(1);
});
});
describe("startHeadless", function () {
beforeEach(function (done) {
openmct.on('start', done);
openmct.startHeadless();
});
it("calls plugins for configuration", function () {
expect(mockPlugin).toHaveBeenCalledWith(openmct);
expect(mockPlugin2).toHaveBeenCalledWith(openmct);
});
it("emits a start event", function () {
expect(mockListener).toHaveBeenCalled();
});
it("Does not render Open MCT", function () {
let openMctShellElements = document.body.querySelectorAll('div.l-shell');
expect(openMctShellElements.length).toBe(0);
});
});
describe("setAssetPath", function () {
let testAssetPath;
it("configures the path for assets", function () {
testAssetPath = "some/path/";
openmct.setAssetPath(testAssetPath);
expect(openmct.getAssetPath()).toBe(testAssetPath);
});
it("adds a trailing /", function () {
testAssetPath = "some/path";
openmct.setAssetPath(testAssetPath);
expect(openmct.getAssetPath()).toBe(testAssetPath + "/");
});
});
openmct.install(mockPlugin);
openmct.install(mockPlugin2);
openmct.on('start', mockListener);
});
// Clean up the dirty singleton.
afterEach(function () {
return testUtils.resetApplicationState(openmct);
});
it('exposes plugins', function () {
expect(openmct.plugins).toEqual(plugins);
});
it('does not issue a start event before started', function () {
expect(mockListener).not.toHaveBeenCalled();
});
describe('start', function () {
let appHolder;
beforeEach(function (done) {
appHolder = document.createElement('div');
openmct.on('start', done);
openmct.start(appHolder);
});
it('calls plugins for configuration', function () {
expect(mockPlugin).toHaveBeenCalledWith(openmct);
expect(mockPlugin2).toHaveBeenCalledWith(openmct);
});
it('emits a start event', function () {
expect(mockListener).toHaveBeenCalled();
});
it('Renders the application into the provided container element', function () {
let openMctShellElements = appHolder.querySelectorAll('div.l-shell');
expect(openMctShellElements.length).toBe(1);
});
});
describe('startHeadless', function () {
beforeEach(function (done) {
openmct.on('start', done);
openmct.startHeadless();
});
it('calls plugins for configuration', function () {
expect(mockPlugin).toHaveBeenCalledWith(openmct);
expect(mockPlugin2).toHaveBeenCalledWith(openmct);
});
it('emits a start event', function () {
expect(mockListener).toHaveBeenCalled();
});
it('Does not render Open MCT', function () {
let openMctShellElements = document.body.querySelectorAll('div.l-shell');
expect(openMctShellElements.length).toBe(0);
});
});
describe('setAssetPath', function () {
let testAssetPath;
it('configures the path for assets', function () {
testAssetPath = 'some/path/';
openmct.setAssetPath(testAssetPath);
expect(openmct.getAssetPath()).toBe(testAssetPath);
});
it('adds a trailing /', function () {
testAssetPath = 'some/path';
openmct.setAssetPath(testAssetPath);
expect(openmct.getAssetPath()).toBe(testAssetPath + '/');
});
});
});
});

View File

@@ -37,9 +37,9 @@ let brandingOptions = {};
* @param {BrandingOptions} options
*/
export default function Branding(options) {
if (arguments.length === 1) {
brandingOptions = options;
}
if (arguments.length === 1) {
brandingOptions = options;
}
return brandingOptions;
return brandingOptions;
}

View File

@@ -1,4 +1,3 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, United States Government
* as represented by the Administrator of the National Aeronautics and Space
@@ -24,65 +23,66 @@
import EventEmitter from 'EventEmitter';
export default class Editor extends EventEmitter {
constructor(openmct) {
super();
this.editing = false;
this.openmct = openmct;
constructor(openmct) {
super();
this.editing = false;
this.openmct = openmct;
}
/**
* Initiate an editing session. This will start a transaction during
* which any persist operations will be deferred until either save()
* or finish() are called.
*/
edit() {
if (this.editing === true) {
throw 'Already editing';
}
/**
* Initiate an editing session. This will start a transaction during
* which any persist operations will be deferred until either save()
* or finish() are called.
*/
edit() {
if (this.editing === true) {
throw "Already editing";
}
this.editing = true;
this.emit('isEditing', true);
this.openmct.objects.startTransaction();
}
this.editing = true;
this.emit('isEditing', true);
this.openmct.objects.startTransaction();
}
/**
* @returns {boolean} true if the application is in edit mode, false otherwise.
*/
isEditing() {
return this.editing;
}
/**
* @returns {boolean} true if the application is in edit mode, false otherwise.
*/
isEditing() {
return this.editing;
}
/**
* Save any unsaved changes from this editing session. This will
* end the current transaction.
*/
async save() {
const transaction = this.openmct.objects.getActiveTransaction();
await transaction.commit();
this.editing = false;
this.emit('isEditing', false);
this.openmct.objects.endTransaction();
}
/**
* Save any unsaved changes from this editing session. This will
* end the current transaction.
*/
async save() {
const transaction = this.openmct.objects.getActiveTransaction();
await transaction.commit();
this.editing = false;
this.emit('isEditing', false);
this.openmct.objects.endTransaction();
}
/**
* End the currently active transaction and discard unsaved changes.
*/
cancel() {
this.editing = false;
this.emit('isEditing', false);
/**
* End the currently active transaction and discard unsaved changes.
*/
cancel() {
this.editing = false;
this.emit('isEditing', false);
return new Promise((resolve, reject) => {
const transaction = this.openmct.objects.getActiveTransaction();
if (!transaction) {
return resolve();
}
return new Promise((resolve, reject) => {
const transaction = this.openmct.objects.getActiveTransaction();
if (!transaction) {
return resolve();
}
transaction.cancel()
.then(resolve)
.catch(reject)
.finally(() => {
this.openmct.objects.endTransaction();
});
transaction
.cancel()
.then(resolve)
.catch(reject)
.finally(() => {
this.openmct.objects.endTransaction();
});
}
});
}
}

View File

@@ -20,61 +20,49 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import {
createOpenMct, resetApplicationState
} from '../utils/testing';
import { createOpenMct, resetApplicationState } from '../utils/testing';
describe('The Editor API', () => {
let openmct;
let openmct;
beforeEach((done) => {
openmct = createOpenMct();
openmct.on('start', done);
beforeEach((done) => {
openmct = createOpenMct();
openmct.on('start', done);
spyOn(openmct.objects, 'endTransaction');
spyOn(openmct.objects, 'endTransaction');
openmct.startHeadless();
openmct.startHeadless();
});
afterEach(() => {
return resetApplicationState(openmct);
});
it('opens a transaction on edit', () => {
expect(openmct.objects.isTransactionActive()).toBeFalse();
openmct.editor.edit();
expect(openmct.objects.isTransactionActive()).toBeTrue();
});
it('closes an open transaction on successful save', async () => {
spyOn(openmct.objects, 'getActiveTransaction').and.returnValue({
commit: () => Promise.resolve(true)
});
afterEach(() => {
return resetApplicationState(openmct);
openmct.editor.edit();
await openmct.editor.save();
expect(openmct.objects.endTransaction).toHaveBeenCalled();
});
it('does not close an open transaction on failed save', async () => {
spyOn(openmct.objects, 'getActiveTransaction').and.returnValue({
commit: () => Promise.reject()
});
it('opens a transaction on edit', () => {
expect(
openmct.objects.isTransactionActive()
).toBeFalse();
openmct.editor.edit();
expect(
openmct.objects.isTransactionActive()
).toBeTrue();
});
openmct.editor.edit();
await openmct.editor.save().catch(() => {});
it('closes an open transaction on successful save', async () => {
spyOn(openmct.objects, 'getActiveTransaction')
.and.returnValue({
commit: () => Promise.resolve(true)
});
openmct.editor.edit();
await openmct.editor.save();
expect(
openmct.objects.endTransaction
).toHaveBeenCalled();
});
it('does not close an open transaction on failed save', async () => {
spyOn(openmct.objects, 'getActiveTransaction')
.and.returnValue({
commit: () => Promise.reject()
});
openmct.editor.edit();
await openmct.editor.save().catch(() => {});
expect(
openmct.objects.endTransaction
).not.toHaveBeenCalled();
});
expect(openmct.objects.endTransaction).not.toHaveBeenCalled();
});
});

View File

@@ -24,154 +24,161 @@ import EventEmitter from 'EventEmitter';
import _ from 'lodash';
class ActionCollection extends EventEmitter {
constructor(applicableActions, objectPath, view, openmct, skipEnvironmentObservers) {
super();
constructor(applicableActions, objectPath, view, openmct, skipEnvironmentObservers) {
super();
this.applicableActions = applicableActions;
this.openmct = openmct;
this.objectPath = objectPath;
this.view = view;
this.skipEnvironmentObservers = skipEnvironmentObservers;
this.objectUnsubscribes = [];
this.applicableActions = applicableActions;
this.openmct = openmct;
this.objectPath = objectPath;
this.view = view;
this.skipEnvironmentObservers = skipEnvironmentObservers;
this.objectUnsubscribes = [];
let debounceOptions = {
leading: false,
trailing: true
};
let debounceOptions = {
leading: false,
trailing: true
};
this._updateActions = _.debounce(this._updateActions.bind(this), 150, debounceOptions);
this._update = _.debounce(this._update.bind(this), 150, debounceOptions);
this._updateActions = _.debounce(this._updateActions.bind(this), 150, debounceOptions);
this._update = _.debounce(this._update.bind(this), 150, debounceOptions);
if (!skipEnvironmentObservers) {
this._observeObjectPath();
this.openmct.editor.on('isEditing', this._updateActions);
}
if (!skipEnvironmentObservers) {
this._observeObjectPath();
this.openmct.editor.on('isEditing', this._updateActions);
}
}
disable(actionKeys) {
actionKeys.forEach((actionKey) => {
if (this.applicableActions[actionKey]) {
this.applicableActions[actionKey].isDisabled = true;
}
});
this._update();
}
enable(actionKeys) {
actionKeys.forEach((actionKey) => {
if (this.applicableActions[actionKey]) {
this.applicableActions[actionKey].isDisabled = false;
}
});
this._update();
}
hide(actionKeys) {
actionKeys.forEach((actionKey) => {
if (this.applicableActions[actionKey]) {
this.applicableActions[actionKey].isHidden = true;
}
});
this._update();
}
show(actionKeys) {
actionKeys.forEach((actionKey) => {
if (this.applicableActions[actionKey]) {
this.applicableActions[actionKey].isHidden = false;
}
});
this._update();
}
destroy() {
if (!this.skipEnvironmentObservers) {
this.objectUnsubscribes.forEach((unsubscribe) => {
unsubscribe();
});
this.openmct.editor.off('isEditing', this._updateActions);
}
disable(actionKeys) {
actionKeys.forEach(actionKey => {
if (this.applicableActions[actionKey]) {
this.applicableActions[actionKey].isDisabled = true;
}
});
this._update();
this.emit('destroy', this.view);
this.removeAllListeners();
}
getVisibleActions() {
let actionsArray = Object.keys(this.applicableActions);
let visibleActions = [];
actionsArray.forEach((actionKey) => {
let action = this.applicableActions[actionKey];
if (!action.isHidden) {
visibleActions.push(action);
}
});
return visibleActions;
}
getStatusBarActions() {
let actionsArray = Object.keys(this.applicableActions);
let statusBarActions = [];
actionsArray.forEach((actionKey) => {
let action = this.applicableActions[actionKey];
if (action.showInStatusBar && !action.isDisabled && !action.isHidden) {
statusBarActions.push(action);
}
});
return statusBarActions;
}
getActionsObject() {
return this.applicableActions;
}
_update() {
this.emit('update', this.applicableActions);
}
_observeObjectPath() {
let actionCollection = this;
function updateObject(oldObject, newObject) {
Object.assign(oldObject, newObject);
actionCollection._updateActions();
}
enable(actionKeys) {
actionKeys.forEach(actionKey => {
if (this.applicableActions[actionKey]) {
this.applicableActions[actionKey].isDisabled = false;
}
});
this._update();
}
this.objectPath.forEach((object) => {
if (object) {
let unsubscribe = this.openmct.objects.observe(
object,
'*',
updateObject.bind(this, object)
);
hide(actionKeys) {
actionKeys.forEach(actionKey => {
if (this.applicableActions[actionKey]) {
this.applicableActions[actionKey].isHidden = true;
}
});
this._update();
}
this.objectUnsubscribes.push(unsubscribe);
}
});
}
show(actionKeys) {
actionKeys.forEach(actionKey => {
if (this.applicableActions[actionKey]) {
this.applicableActions[actionKey].isHidden = false;
}
});
this._update();
}
_updateActions() {
let newApplicableActions = this.openmct.actions._applicableActions(this.objectPath, this.view);
destroy() {
if (!this.skipEnvironmentObservers) {
this.objectUnsubscribes.forEach(unsubscribe => {
unsubscribe();
});
this.applicableActions = this._mergeOldAndNewActions(
this.applicableActions,
newApplicableActions
);
this._update();
}
this.openmct.editor.off('isEditing', this._updateActions);
}
_mergeOldAndNewActions(oldActions, newActions) {
let mergedActions = {};
Object.keys(newActions).forEach((key) => {
if (oldActions[key]) {
mergedActions[key] = oldActions[key];
} else {
mergedActions[key] = newActions[key];
}
});
this.emit('destroy', this.view);
this.removeAllListeners();
}
getVisibleActions() {
let actionsArray = Object.keys(this.applicableActions);
let visibleActions = [];
actionsArray.forEach(actionKey => {
let action = this.applicableActions[actionKey];
if (!action.isHidden) {
visibleActions.push(action);
}
});
return visibleActions;
}
getStatusBarActions() {
let actionsArray = Object.keys(this.applicableActions);
let statusBarActions = [];
actionsArray.forEach(actionKey => {
let action = this.applicableActions[actionKey];
if (action.showInStatusBar && !action.isDisabled && !action.isHidden) {
statusBarActions.push(action);
}
});
return statusBarActions;
}
getActionsObject() {
return this.applicableActions;
}
_update() {
this.emit('update', this.applicableActions);
}
_observeObjectPath() {
let actionCollection = this;
function updateObject(oldObject, newObject) {
Object.assign(oldObject, newObject);
actionCollection._updateActions();
}
this.objectPath.forEach(object => {
if (object) {
let unsubscribe = this.openmct.objects.observe(object, '*', updateObject.bind(this, object));
this.objectUnsubscribes.push(unsubscribe);
}
});
}
_updateActions() {
let newApplicableActions = this.openmct.actions._applicableActions(this.objectPath, this.view);
this.applicableActions = this._mergeOldAndNewActions(this.applicableActions, newApplicableActions);
this._update();
}
_mergeOldAndNewActions(oldActions, newActions) {
let mergedActions = {};
Object.keys(newActions).forEach(key => {
if (oldActions[key]) {
mergedActions[key] = oldActions[key];
} else {
mergedActions[key] = newActions[key];
}
});
return mergedActions;
}
return mergedActions;
}
}
export default ActionCollection;

View File

@@ -24,208 +24,211 @@ import ActionCollection from './ActionCollection';
import { createOpenMct, resetApplicationState } from '../../utils/testing';
describe('The ActionCollection', () => {
let openmct;
let actionCollection;
let mockApplicableActions;
let mockObjectPath;
let mockView;
let openmct;
let actionCollection;
let mockApplicableActions;
let mockObjectPath;
let mockView;
beforeEach(() => {
openmct = createOpenMct();
beforeEach(() => {
openmct = createOpenMct();
mockObjectPath = [
{
name: 'mock folder',
type: 'fake-folder',
identifier: {
key: 'mock-folder',
namespace: ''
}
},
{
name: 'mock parent folder',
type: 'fake-folder',
identifier: {
key: 'mock-parent-folder',
namespace: ''
}
}
];
openmct.objects.addProvider('', jasmine.createSpyObj('mockMutableObjectProvider', [
'create',
'update'
]));
mockView = {
getViewContext: () => {
return {
onlyAppliesToTestCase: true
};
}
mockObjectPath = [
{
name: 'mock folder',
type: 'fake-folder',
identifier: {
key: 'mock-folder',
namespace: ''
}
},
{
name: 'mock parent folder',
type: 'fake-folder',
identifier: {
key: 'mock-parent-folder',
namespace: ''
}
}
];
openmct.objects.addProvider(
'',
jasmine.createSpyObj('mockMutableObjectProvider', ['create', 'update'])
);
mockView = {
getViewContext: () => {
return {
onlyAppliesToTestCase: true
};
mockApplicableActions = {
'test-action-object-path': {
name: 'Test Action Object Path',
key: 'test-action-object-path',
cssClass: 'test-action-object-path',
description: 'This is a test action for object path',
group: 'action',
priority: 9,
appliesTo: (objectPath) => {
if (objectPath.length) {
return objectPath[0].type === 'fake-folder';
}
}
};
mockApplicableActions = {
'test-action-object-path': {
name: 'Test Action Object Path',
key: 'test-action-object-path',
cssClass: 'test-action-object-path',
description: 'This is a test action for object path',
group: 'action',
priority: 9,
appliesTo: (objectPath) => {
if (objectPath.length) {
return objectPath[0].type === 'fake-folder';
}
return false;
},
invoke: () => {
}
},
'test-action-view': {
name: 'Test Action View',
key: 'test-action-view',
cssClass: 'test-action-view',
description: 'This is a test action for view',
group: 'action',
priority: 9,
showInStatusBar: true,
appliesTo: (objectPath, view = {}) => {
if (view.getViewContext) {
let viewContext = view.getViewContext();
return false;
},
invoke: () => {}
},
'test-action-view': {
name: 'Test Action View',
key: 'test-action-view',
cssClass: 'test-action-view',
description: 'This is a test action for view',
group: 'action',
priority: 9,
showInStatusBar: true,
appliesTo: (objectPath, view = {}) => {
if (view.getViewContext) {
let viewContext = view.getViewContext();
return viewContext.onlyAppliesToTestCase;
}
return viewContext.onlyAppliesToTestCase;
}
return false;
},
invoke: () => {
}
}
};
return false;
},
invoke: () => {}
}
};
actionCollection = new ActionCollection(mockApplicableActions, mockObjectPath, mockView, openmct);
actionCollection = new ActionCollection(
mockApplicableActions,
mockObjectPath,
mockView,
openmct
);
});
afterEach(() => {
actionCollection.destroy();
return resetApplicationState(openmct);
});
describe('disable method invoked with action keys', () => {
it('marks those actions as isDisabled', () => {
let actionKey = 'test-action-object-path';
let actionsObject = actionCollection.getActionsObject();
let action = actionsObject[actionKey];
expect(action.isDisabled).toBeFalsy();
actionCollection.disable([actionKey]);
actionsObject = actionCollection.getActionsObject();
action = actionsObject[actionKey];
expect(action.isDisabled).toBeTrue();
});
});
afterEach(() => {
actionCollection.destroy();
describe('enable method invoked with action keys', () => {
it('marks the isDisabled property as false', () => {
let actionKey = 'test-action-object-path';
return resetApplicationState(openmct);
actionCollection.disable([actionKey]);
let actionsObject = actionCollection.getActionsObject();
let action = actionsObject[actionKey];
expect(action.isDisabled).toBeTrue();
actionCollection.enable([actionKey]);
actionsObject = actionCollection.getActionsObject();
action = actionsObject[actionKey];
expect(action.isDisabled).toBeFalse();
});
});
describe("disable method invoked with action keys", () => {
it("marks those actions as isDisabled", () => {
let actionKey = 'test-action-object-path';
let actionsObject = actionCollection.getActionsObject();
let action = actionsObject[actionKey];
describe('hide method invoked with action keys', () => {
it('marks those actions as isHidden', () => {
let actionKey = 'test-action-object-path';
let actionsObject = actionCollection.getActionsObject();
let action = actionsObject[actionKey];
expect(action.isDisabled).toBeFalsy();
expect(action.isHidden).toBeFalsy();
actionCollection.disable([actionKey]);
actionsObject = actionCollection.getActionsObject();
action = actionsObject[actionKey];
actionCollection.hide([actionKey]);
actionsObject = actionCollection.getActionsObject();
action = actionsObject[actionKey];
expect(action.isDisabled).toBeTrue();
});
expect(action.isHidden).toBeTrue();
});
});
describe("enable method invoked with action keys", () => {
it("marks the isDisabled property as false", () => {
let actionKey = 'test-action-object-path';
describe('show method invoked with action keys', () => {
it('marks the isHidden property as false', () => {
let actionKey = 'test-action-object-path';
actionCollection.disable([actionKey]);
actionCollection.hide([actionKey]);
let actionsObject = actionCollection.getActionsObject();
let action = actionsObject[actionKey];
let actionsObject = actionCollection.getActionsObject();
let action = actionsObject[actionKey];
expect(action.isDisabled).toBeTrue();
expect(action.isHidden).toBeTrue();
actionCollection.enable([actionKey]);
actionsObject = actionCollection.getActionsObject();
action = actionsObject[actionKey];
actionCollection.show([actionKey]);
actionsObject = actionCollection.getActionsObject();
action = actionsObject[actionKey];
expect(action.isDisabled).toBeFalse();
});
expect(action.isHidden).toBeFalse();
});
});
describe("hide method invoked with action keys", () => {
it("marks those actions as isHidden", () => {
let actionKey = 'test-action-object-path';
let actionsObject = actionCollection.getActionsObject();
let action = actionsObject[actionKey];
describe('getVisibleActions method', () => {
it('returns an array of non hidden actions', () => {
let action1Key = 'test-action-object-path';
let action2Key = 'test-action-view';
expect(action.isHidden).toBeFalsy();
actionCollection.hide([action1Key]);
actionCollection.hide([actionKey]);
actionsObject = actionCollection.getActionsObject();
action = actionsObject[actionKey];
let visibleActions = actionCollection.getVisibleActions();
expect(action.isHidden).toBeTrue();
});
expect(Array.isArray(visibleActions)).toBeTrue();
expect(visibleActions.length).toEqual(1);
expect(visibleActions[0].key).toEqual(action2Key);
actionCollection.show([action1Key]);
visibleActions = actionCollection.getVisibleActions();
expect(visibleActions.length).toEqual(2);
});
});
describe("show method invoked with action keys", () => {
it("marks the isHidden property as false", () => {
let actionKey = 'test-action-object-path';
describe('getStatusBarActions method', () => {
it('returns an array of non disabled, non hidden statusBar actions', () => {
let action2Key = 'test-action-view';
actionCollection.hide([actionKey]);
let statusBarActions = actionCollection.getStatusBarActions();
let actionsObject = actionCollection.getActionsObject();
let action = actionsObject[actionKey];
expect(Array.isArray(statusBarActions)).toBeTrue();
expect(statusBarActions.length).toEqual(1);
expect(statusBarActions[0].key).toEqual(action2Key);
expect(action.isHidden).toBeTrue();
actionCollection.disable([action2Key]);
statusBarActions = actionCollection.getStatusBarActions();
actionCollection.show([actionKey]);
actionsObject = actionCollection.getActionsObject();
action = actionsObject[actionKey];
expect(statusBarActions.length).toEqual(0);
expect(action.isHidden).toBeFalse();
});
});
describe("getVisibleActions method", () => {
it("returns an array of non hidden actions", () => {
let action1Key = 'test-action-object-path';
let action2Key = 'test-action-view';
actionCollection.hide([action1Key]);
let visibleActions = actionCollection.getVisibleActions();
expect(Array.isArray(visibleActions)).toBeTrue();
expect(visibleActions.length).toEqual(1);
expect(visibleActions[0].key).toEqual(action2Key);
actionCollection.show([action1Key]);
visibleActions = actionCollection.getVisibleActions();
expect(visibleActions.length).toEqual(2);
});
});
describe("getStatusBarActions method", () => {
it("returns an array of non disabled, non hidden statusBar actions", () => {
let action2Key = 'test-action-view';
let statusBarActions = actionCollection.getStatusBarActions();
expect(Array.isArray(statusBarActions)).toBeTrue();
expect(statusBarActions.length).toEqual(1);
expect(statusBarActions[0].key).toEqual(action2Key);
actionCollection.disable([action2Key]);
statusBarActions = actionCollection.getStatusBarActions();
expect(statusBarActions.length).toEqual(0);
actionCollection.enable([action2Key]);
statusBarActions = actionCollection.getStatusBarActions();
expect(statusBarActions.length).toEqual(1);
expect(statusBarActions[0].key).toEqual(action2Key);
actionCollection.hide([action2Key]);
statusBarActions = actionCollection.getStatusBarActions();
expect(statusBarActions.length).toEqual(0);
});
actionCollection.enable([action2Key]);
statusBarActions = actionCollection.getStatusBarActions();
expect(statusBarActions.length).toEqual(1);
expect(statusBarActions[0].key).toEqual(action2Key);
actionCollection.hide([action2Key]);
statusBarActions = actionCollection.getStatusBarActions();
expect(statusBarActions.length).toEqual(0);
});
});
});

View File

@@ -24,122 +24,131 @@ import ActionCollection from './ActionCollection';
import _ from 'lodash';
class ActionsAPI extends EventEmitter {
constructor(openmct) {
super();
constructor(openmct) {
super();
this._allActions = {};
this._actionCollections = new WeakMap();
this._openmct = openmct;
this._allActions = {};
this._actionCollections = new WeakMap();
this._openmct = openmct;
this._groupOrder = ['windowing', 'undefined', 'view', 'action', 'export', 'import'];
this._groupOrder = ['windowing', 'undefined', 'view', 'action', 'export', 'import'];
this.register = this.register.bind(this);
this.getActionsCollection = this.getActionsCollection.bind(this);
this._applicableActions = this._applicableActions.bind(this);
this._updateCachedActionCollections = this._updateCachedActionCollections.bind(this);
this.register = this.register.bind(this);
this.getActionsCollection = this.getActionsCollection.bind(this);
this._applicableActions = this._applicableActions.bind(this);
this._updateCachedActionCollections = this._updateCachedActionCollections.bind(this);
}
register(actionDefinition) {
this._allActions[actionDefinition.key] = actionDefinition;
}
getAction(key) {
return this._allActions[key];
}
getActionsCollection(objectPath, view) {
if (view) {
return (
this._getCachedActionCollection(objectPath, view) ||
this._newActionCollection(objectPath, view, true)
);
} else {
return this._newActionCollection(objectPath, view, true);
}
}
updateGroupOrder(groupArray) {
this._groupOrder = groupArray;
}
_getCachedActionCollection(objectPath, view) {
return this._actionCollections.get(view);
}
_newActionCollection(objectPath, view, skipEnvironmentObservers) {
let applicableActions = this._applicableActions(objectPath, view);
const actionCollection = new ActionCollection(
applicableActions,
objectPath,
view,
this._openmct,
skipEnvironmentObservers
);
if (view) {
this._cacheActionCollection(view, actionCollection);
}
register(actionDefinition) {
this._allActions[actionDefinition.key] = actionDefinition;
return actionCollection;
}
_cacheActionCollection(view, actionCollection) {
this._actionCollections.set(view, actionCollection);
actionCollection.on('destroy', this._updateCachedActionCollections);
}
_updateCachedActionCollections(key) {
if (this._actionCollections.has(key)) {
let actionCollection = this._actionCollections.get(key);
actionCollection.off('destroy', this._updateCachedActionCollections);
this._actionCollections.delete(key);
}
}
_applicableActions(objectPath, view) {
let actionsObject = {};
let keys = Object.keys(this._allActions).filter((key) => {
let actionDefinition = this._allActions[key];
if (actionDefinition.appliesTo === undefined) {
return true;
} else {
return actionDefinition.appliesTo(objectPath, view);
}
});
keys.forEach((key) => {
let action = _.clone(this._allActions[key]);
actionsObject[key] = action;
});
return actionsObject;
}
_groupAndSortActions(actionsArray = []) {
if (!Array.isArray(actionsArray) && typeof actionsArray === 'object') {
actionsArray = Object.keys(actionsArray).map((key) => actionsArray[key]);
}
getAction(key) {
return this._allActions[key];
let actionsObject = {};
let groupedSortedActionsArray = [];
function sortDescending(a, b) {
return b.priority - a.priority;
}
getActionsCollection(objectPath, view) {
if (view) {
return this._getCachedActionCollection(objectPath, view) || this._newActionCollection(objectPath, view, true);
} else {
return this._newActionCollection(objectPath, view, true);
}
}
actionsArray.forEach((action) => {
if (actionsObject[action.group] === undefined) {
actionsObject[action.group] = [action];
} else {
actionsObject[action.group].push(action);
}
});
updateGroupOrder(groupArray) {
this._groupOrder = groupArray;
}
this._groupOrder.forEach((group) => {
let groupArray = actionsObject[group];
_getCachedActionCollection(objectPath, view) {
return this._actionCollections.get(view);
}
if (groupArray) {
groupedSortedActionsArray.push(groupArray.sort(sortDescending));
}
});
_newActionCollection(objectPath, view, skipEnvironmentObservers) {
let applicableActions = this._applicableActions(objectPath, view);
const actionCollection = new ActionCollection(applicableActions, objectPath, view, this._openmct, skipEnvironmentObservers);
if (view) {
this._cacheActionCollection(view, actionCollection);
}
return actionCollection;
}
_cacheActionCollection(view, actionCollection) {
this._actionCollections.set(view, actionCollection);
actionCollection.on('destroy', this._updateCachedActionCollections);
}
_updateCachedActionCollections(key) {
if (this._actionCollections.has(key)) {
let actionCollection = this._actionCollections.get(key);
actionCollection.off('destroy', this._updateCachedActionCollections);
this._actionCollections.delete(key);
}
}
_applicableActions(objectPath, view) {
let actionsObject = {};
let keys = Object.keys(this._allActions).filter(key => {
let actionDefinition = this._allActions[key];
if (actionDefinition.appliesTo === undefined) {
return true;
} else {
return actionDefinition.appliesTo(objectPath, view);
}
});
keys.forEach(key => {
let action = _.clone(this._allActions[key]);
actionsObject[key] = action;
});
return actionsObject;
}
_groupAndSortActions(actionsArray = []) {
if (!Array.isArray(actionsArray) && typeof actionsArray === 'object') {
actionsArray = Object.keys(actionsArray).map(key => actionsArray[key]);
}
let actionsObject = {};
let groupedSortedActionsArray = [];
function sortDescending(a, b) {
return b.priority - a.priority;
}
actionsArray.forEach(action => {
if (actionsObject[action.group] === undefined) {
actionsObject[action.group] = [action];
} else {
actionsObject[action.group].push(action);
}
});
this._groupOrder.forEach(group => {
let groupArray = actionsObject[group];
if (groupArray) {
groupedSortedActionsArray.push(groupArray.sort(sortDescending));
}
});
return groupedSortedActionsArray;
}
return groupedSortedActionsArray;
}
}
export default ActionsAPI;

View File

@@ -25,129 +25,127 @@ import { createOpenMct, resetApplicationState } from '../../utils/testing';
import ActionCollection from './ActionCollection';
describe('The Actions API', () => {
let openmct;
let actionsAPI;
let mockAction;
let mockObjectPath;
let mockObjectPathAction;
let mockViewContext1;
let openmct;
let actionsAPI;
let mockAction;
let mockObjectPath;
let mockObjectPathAction;
let mockViewContext1;
beforeEach(() => {
openmct = createOpenMct();
actionsAPI = new ActionsAPI(openmct);
mockObjectPathAction = {
name: 'Test Action Object Path',
key: 'test-action-object-path',
cssClass: 'test-action-object-path',
description: 'This is a test action for object path',
group: 'action',
priority: 9,
appliesTo: (objectPath) => {
if (objectPath.length) {
return objectPath[0].type === 'fake-folder';
}
return false;
},
invoke: () => {}
};
mockAction = {
name: 'Test Action View',
key: 'test-action-view',
cssClass: 'test-action-view',
description: 'This is a test action for view',
group: 'action',
priority: 9,
appliesTo: (objectPath, view = {}) => {
if (view.getViewContext) {
let viewContext = view.getViewContext();
return viewContext.onlyAppliesToTestCase;
}
return false;
},
invoke: () => {}
};
mockObjectPath = [
{
name: 'mock folder',
type: 'fake-folder',
identifier: {
key: 'mock-folder',
namespace: ''
}
},
{
name: 'mock parent folder',
type: 'fake-folder',
identifier: {
key: 'mock-parent-folder',
namespace: ''
}
}
];
mockViewContext1 = {
getViewContext: () => {
return {
onlyAppliesToTestCase: true
};
}
};
});
afterEach(() => {
return resetApplicationState(openmct);
});
describe('register method', () => {
it('adds action to ActionsAPI', () => {
actionsAPI.register(mockAction);
let actionCollection = actionsAPI.getActionsCollection(mockObjectPath, mockViewContext1);
let action = actionCollection.getActionsObject()[mockAction.key];
expect(action.key).toEqual(mockAction.key);
expect(action.name).toEqual(mockAction.name);
});
});
describe('get method', () => {
beforeEach(() => {
openmct = createOpenMct();
actionsAPI = new ActionsAPI(openmct);
mockObjectPathAction = {
name: 'Test Action Object Path',
key: 'test-action-object-path',
cssClass: 'test-action-object-path',
description: 'This is a test action for object path',
group: 'action',
priority: 9,
appliesTo: (objectPath) => {
if (objectPath.length) {
return objectPath[0].type === 'fake-folder';
}
return false;
},
invoke: () => {
}
};
mockAction = {
name: 'Test Action View',
key: 'test-action-view',
cssClass: 'test-action-view',
description: 'This is a test action for view',
group: 'action',
priority: 9,
appliesTo: (objectPath, view = {}) => {
if (view.getViewContext) {
let viewContext = view.getViewContext();
return viewContext.onlyAppliesToTestCase;
}
return false;
},
invoke: () => {
}
};
mockObjectPath = [
{
name: 'mock folder',
type: 'fake-folder',
identifier: {
key: 'mock-folder',
namespace: ''
}
},
{
name: 'mock parent folder',
type: 'fake-folder',
identifier: {
key: 'mock-parent-folder',
namespace: ''
}
}
];
mockViewContext1 = {
getViewContext: () => {
return {
onlyAppliesToTestCase: true
};
}
};
actionsAPI.register(mockAction);
actionsAPI.register(mockObjectPathAction);
});
afterEach(() => {
return resetApplicationState(openmct);
it('returns an ActionCollection when invoked with an objectPath only', () => {
let actionCollection = actionsAPI.getActionsCollection(mockObjectPath);
let instanceOfActionCollection = actionCollection instanceof ActionCollection;
expect(instanceOfActionCollection).toBeTrue();
});
describe("register method", () => {
it("adds action to ActionsAPI", () => {
actionsAPI.register(mockAction);
it('returns an ActionCollection when invoked with an objectPath and view', () => {
let actionCollection = actionsAPI.getActionsCollection(mockObjectPath, mockViewContext1);
let instanceOfActionCollection = actionCollection instanceof ActionCollection;
let actionCollection = actionsAPI.getActionsCollection(mockObjectPath, mockViewContext1);
let action = actionCollection.getActionsObject()[mockAction.key];
expect(action.key).toEqual(mockAction.key);
expect(action.name).toEqual(mockAction.name);
});
expect(instanceOfActionCollection).toBeTrue();
});
describe("get method", () => {
beforeEach(() => {
actionsAPI.register(mockAction);
actionsAPI.register(mockObjectPathAction);
});
it('returns relevant actions when invoked with objectPath only', () => {
let actionCollection = actionsAPI.getActionsCollection(mockObjectPath);
let action = actionCollection.getActionsObject()[mockObjectPathAction.key];
it("returns an ActionCollection when invoked with an objectPath only", () => {
let actionCollection = actionsAPI.getActionsCollection(mockObjectPath);
let instanceOfActionCollection = actionCollection instanceof ActionCollection;
expect(instanceOfActionCollection).toBeTrue();
});
it("returns an ActionCollection when invoked with an objectPath and view", () => {
let actionCollection = actionsAPI.getActionsCollection(mockObjectPath, mockViewContext1);
let instanceOfActionCollection = actionCollection instanceof ActionCollection;
expect(instanceOfActionCollection).toBeTrue();
});
it("returns relevant actions when invoked with objectPath only", () => {
let actionCollection = actionsAPI.getActionsCollection(mockObjectPath);
let action = actionCollection.getActionsObject()[mockObjectPathAction.key];
expect(action.key).toEqual(mockObjectPathAction.key);
expect(action.name).toEqual(mockObjectPathAction.name);
});
it("returns relevant actions when invoked with objectPath and view", () => {
let actionCollection = actionsAPI.getActionsCollection(mockObjectPath, mockViewContext1);
let action = actionCollection.getActionsObject()[mockAction.key];
expect(action.key).toEqual(mockAction.key);
expect(action.name).toEqual(mockAction.name);
});
expect(action.key).toEqual(mockObjectPathAction.key);
expect(action.name).toEqual(mockObjectPathAction.name);
});
it('returns relevant actions when invoked with objectPath and view', () => {
let actionCollection = actionsAPI.getActionsCollection(mockObjectPath, mockViewContext1);
let action = actionCollection.getActionsObject()[mockAction.key];
expect(action.key).toEqual(mockAction.key);
expect(action.name).toEqual(mockAction.name);
});
});
});

View File

@@ -34,11 +34,11 @@ import _ from 'lodash';
* @property {String} PLOT_SPATIAL The plot-spatial annotation type
*/
const ANNOTATION_TYPES = Object.freeze({
NOTEBOOK: 'NOTEBOOK',
GEOSPATIAL: 'GEOSPATIAL',
PIXEL_SPATIAL: 'PIXEL_SPATIAL',
TEMPORAL: 'TEMPORAL',
PLOT_SPATIAL: 'PLOT_SPATIAL'
NOTEBOOK: 'NOTEBOOK',
GEOSPATIAL: 'GEOSPATIAL',
PIXEL_SPATIAL: 'PIXEL_SPATIAL',
TEMPORAL: 'TEMPORAL',
PLOT_SPATIAL: 'PLOT_SPATIAL'
});
const ANNOTATION_TYPE = 'annotation';
@@ -76,357 +76,388 @@ const ANNOTATION_LAST_CREATED = 'annotationLastCreated';
* @constructor
*/
export default class AnnotationAPI extends EventEmitter {
/**
* @param {OpenMCT} openmct
*/
constructor(openmct) {
super();
this.openmct = openmct;
this.availableTags = {};
this.namespaceToSaveAnnotations = '';
/**
* @param {OpenMCT} openmct
*/
constructor(openmct) {
super();
this.openmct = openmct;
this.availableTags = {};
this.namespaceToSaveAnnotations = '';
this.ANNOTATION_TYPES = ANNOTATION_TYPES;
this.ANNOTATION_TYPE = ANNOTATION_TYPE;
this.ANNOTATION_LAST_CREATED = ANNOTATION_LAST_CREATED;
this.ANNOTATION_TYPES = ANNOTATION_TYPES;
this.ANNOTATION_TYPE = ANNOTATION_TYPE;
this.ANNOTATION_LAST_CREATED = ANNOTATION_LAST_CREATED;
this.openmct.types.addType(ANNOTATION_TYPE, {
name: 'Annotation',
description: 'A user created note or comment about time ranges, pixel space, and geospatial features.',
creatable: false,
cssClass: 'icon-notebook',
initialize: function (domainObject) {
domainObject.targets = domainObject.targets || {};
domainObject._deleted = domainObject._deleted || false;
domainObject.originalContextPath = domainObject.originalContextPath || '';
domainObject.tags = domainObject.tags || [];
domainObject.contentText = domainObject.contentText || '';
domainObject.annotationType = domainObject.annotationType || 'plotspatial';
}
});
this.openmct.types.addType(ANNOTATION_TYPE, {
name: 'Annotation',
description:
'A user created note or comment about time ranges, pixel space, and geospatial features.',
creatable: false,
cssClass: 'icon-notebook',
initialize: function (domainObject) {
domainObject.targets = domainObject.targets || {};
domainObject._deleted = domainObject._deleted || false;
domainObject.originalContextPath = domainObject.originalContextPath || '';
domainObject.tags = domainObject.tags || [];
domainObject.contentText = domainObject.contentText || '';
domainObject.annotationType = domainObject.annotationType || 'plotspatial';
}
});
}
/**
* Creates an annotation on a given domain object (e.g., a plot) and a set of targets (e.g., telemetry objects)
* @typedef {Object} CreateAnnotationOptions
* @property {String} name a name for the new annotation (e.g., "Plot annnotation")
* @property {DomainObject} domainObject the domain object this annotation was created with
* @property {ANNOTATION_TYPES} annotationType the type of annotation to create (e.g., PLOT_SPATIAL)
* @property {Tag[]} tags tags to add to the annotation, e.g., SCIENCE for science related annotations
* @property {String} contentText Some text to add to the annotation, e.g. ("This annotation is about science")
* @property {Object<string, Object>} targets The targets ID keystrings and their specific properties.
* For plots, this will be a bounding box, e.g.: {maxY: 100, minY: 0, maxX: 100, minX: 0}
* For notebooks, this will be an entry ID, e.g.: {entryId: "entry-ecb158f5-d23c-45e1-a704-649b382622ba"}
* @property {DomainObject>} targetDomainObjects the targets ID keystrings and the domain objects this annotation points to (e.g., telemetry objects for a plot)
*/
/**
* @method create
* @param {CreateAnnotationOptions} options
* @returns {Promise<DomainObject>} a promise which will resolve when the domain object
* has been created, or be rejected if it cannot be saved
*/
async create({
name,
domainObject,
annotationType,
tags,
contentText,
targets,
targetDomainObjects
}) {
if (!Object.keys(this.ANNOTATION_TYPES).includes(annotationType)) {
throw new Error(`Unknown annotation type: ${annotationType}`);
}
/**
* Creates an annotation on a given domain object (e.g., a plot) and a set of targets (e.g., telemetry objects)
* @typedef {Object} CreateAnnotationOptions
* @property {String} name a name for the new annotation (e.g., "Plot annnotation")
* @property {DomainObject} domainObject the domain object this annotation was created with
* @property {ANNOTATION_TYPES} annotationType the type of annotation to create (e.g., PLOT_SPATIAL)
* @property {Tag[]} tags tags to add to the annotation, e.g., SCIENCE for science related annotations
* @property {String} contentText Some text to add to the annotation, e.g. ("This annotation is about science")
* @property {Object<string, Object>} targets The targets ID keystrings and their specific properties.
* For plots, this will be a bounding box, e.g.: {maxY: 100, minY: 0, maxX: 100, minX: 0}
* For notebooks, this will be an entry ID, e.g.: {entryId: "entry-ecb158f5-d23c-45e1-a704-649b382622ba"}
* @property {DomainObject>} targetDomainObjects the targets ID keystrings and the domain objects this annotation points to (e.g., telemetry objects for a plot)
*/
/**
* @method create
* @param {CreateAnnotationOptions} options
* @returns {Promise<DomainObject>} a promise which will resolve when the domain object
* has been created, or be rejected if it cannot be saved
*/
async create({name, domainObject, annotationType, tags, contentText, targets, targetDomainObjects}) {
if (!Object.keys(this.ANNOTATION_TYPES).includes(annotationType)) {
throw new Error(`Unknown annotation type: ${annotationType}`);
}
if (!Object.keys(targets).length) {
throw new Error(`At least one target is required to create an annotation`);
}
if (!Object.keys(targets).length) {
throw new Error(`At least one target is required to create an annotation`);
}
if (!Object.keys(targetDomainObjects).length) {
throw new Error(`At least one targetDomainObject is required to create an annotation`);
}
if (!Object.keys(targetDomainObjects).length) {
throw new Error(`At least one targetDomainObject is required to create an annotation`);
}
const domainObjectKeyString = this.openmct.objects.makeKeyString(domainObject.identifier);
const originalPathObjects = await this.openmct.objects.getOriginalPath(domainObjectKeyString);
const originalContextPath = this.openmct.objects.getRelativePath(originalPathObjects);
const namespace = this.namespaceToSaveAnnotations;
const type = 'annotation';
const typeDefinition = this.openmct.types.get(type);
const definition = typeDefinition.definition;
const domainObjectKeyString = this.openmct.objects.makeKeyString(domainObject.identifier);
const originalPathObjects = await this.openmct.objects.getOriginalPath(domainObjectKeyString);
const originalContextPath = this.openmct.objects.getRelativePath(originalPathObjects);
const namespace = this.namespaceToSaveAnnotations;
const type = 'annotation';
const typeDefinition = this.openmct.types.get(type);
const definition = typeDefinition.definition;
const createdObject = {
name,
type,
identifier: {
key: uuid(),
namespace
},
tags,
_deleted: false,
annotationType,
contentText,
originalContextPath
const createdObject = {
name,
type,
identifier: {
key: uuid(),
namespace
},
tags,
_deleted: false,
annotationType,
contentText,
originalContextPath
};
if (definition.initialize) {
definition.initialize(createdObject);
}
createdObject.targets = targets;
createdObject.originalContextPath = originalContextPath;
const success = await this.openmct.objects.save(createdObject);
if (success) {
this.emit('annotationCreated', createdObject);
Object.values(targetDomainObjects).forEach((targetDomainObject) => {
this.#updateAnnotationModified(targetDomainObject);
});
return createdObject;
} else {
throw new Error('Failed to create object');
}
}
#updateAnnotationModified(targetDomainObject) {
// As certain telemetry objects are immutable, we'll need to check here first
// to see if we can add the annotation last created property.
// TODO: This should be removed once we have a better way to handle immutable telemetry objects
if (targetDomainObject.isMutable) {
this.openmct.objects.mutate(targetDomainObject, this.ANNOTATION_LAST_CREATED, Date.now());
} else {
this.emit('targetDomainObjectAnnotated', targetDomainObject);
}
}
/**
* @method defineTag
* @param {String} key a unique identifier for the tag
* @param {Tag} tagsDefinition the definition of the tag to add
*/
defineTag(tagKey, tagsDefinition) {
this.availableTags[tagKey] = tagsDefinition;
}
/**
* @method setNamespaceToSaveAnnotations
* @param {String} namespace the namespace to save new annotations to
*/
setNamespaceToSaveAnnotations(namespace) {
this.namespaceToSaveAnnotations = namespace;
}
/**
* @method isAnnotation
* @param {DomainObject} domainObject the domainObject in question
* @returns {Boolean} Returns true if the domain object is an annotation
*/
isAnnotation(domainObject) {
return domainObject && domainObject.type === ANNOTATION_TYPE;
}
/**
* @method getAvailableTags
* @returns {Tag[]} Returns an array of the available tags that have been loaded
*/
getAvailableTags() {
if (this.availableTags) {
const rearrangedToArray = Object.keys(this.availableTags).map((tagKey) => {
return {
id: tagKey,
...this.availableTags[tagKey]
};
});
if (definition.initialize) {
definition.initialize(createdObject);
}
return rearrangedToArray;
} else {
return [];
}
}
createdObject.targets = targets;
createdObject.originalContextPath = originalContextPath;
/**
* @method getAnnotations
* @param {Identifier} domainObjectIdentifier - The domain object identifier to use to search for annotations. For example, a telemetry object identifier.
* @returns {DomainObject[]} Returns an array of annotations that match the search query
*/
async getAnnotations(domainObjectIdentifier) {
const keyStringQuery = this.openmct.objects.makeKeyString(domainObjectIdentifier);
const searchResults = (
await Promise.all(
this.openmct.objects.search(
keyStringQuery,
null,
this.openmct.objects.SEARCH_TYPES.ANNOTATIONS
)
)
).flat();
const success = await this.openmct.objects.save(createdObject);
if (success) {
this.emit('annotationCreated', createdObject);
Object.values(targetDomainObjects).forEach(targetDomainObject => {
this.#updateAnnotationModified(targetDomainObject);
});
return searchResults;
}
return createdObject;
} else {
throw new Error('Failed to create object');
}
/**
* @method deleteAnnotations
* @param {DomainObject[]} existingAnnotation - An array of annotations to delete (set _deleted to true)
*/
deleteAnnotations(annotations) {
if (!annotations) {
throw new Error('Asked to delete null annotations! 🙅‍♂️');
}
#updateAnnotationModified(targetDomainObject) {
// As certain telemetry objects are immutable, we'll need to check here first
// to see if we can add the annotation last created property.
// TODO: This should be removed once we have a better way to handle immutable telemetry objects
if (targetDomainObject.isMutable) {
this.openmct.objects.mutate(targetDomainObject, this.ANNOTATION_LAST_CREATED, Date.now());
} else {
this.emit('targetDomainObjectAnnotated', targetDomainObject);
}
annotations.forEach((annotation) => {
if (!annotation._deleted) {
this.openmct.objects.mutate(annotation, '_deleted', true);
}
});
}
/**
* @method deleteAnnotations
* @param {DomainObject} annotation - An annotation to undelete (set _deleted to false)
*/
unDeleteAnnotation(annotation) {
if (!annotation) {
throw new Error('Asked to undelete null annotation! 🙅‍♂️');
}
/**
* @method defineTag
* @param {String} key a unique identifier for the tag
* @param {Tag} tagsDefinition the definition of the tag to add
*/
defineTag(tagKey, tagsDefinition) {
this.availableTags[tagKey] = tagsDefinition;
this.openmct.objects.mutate(annotation, '_deleted', false);
}
getTagsFromAnnotations(annotations, filterDuplicates = true) {
if (!annotations) {
return [];
}
/**
* @method setNamespaceToSaveAnnotations
* @param {String} namespace the namespace to save new annotations to
*/
setNamespaceToSaveAnnotations(namespace) {
this.namespaceToSaveAnnotations = namespace;
let tagsFromAnnotations = annotations.flatMap((annotation) => {
if (annotation._deleted) {
return [];
} else {
return annotation.tags;
}
});
if (filterDuplicates) {
tagsFromAnnotations = tagsFromAnnotations.filter((tag, index, tagArray) => {
return tagArray.indexOf(tag) === index;
});
}
/**
* @method isAnnotation
* @param {DomainObject} domainObject the domainObject in question
* @returns {Boolean} Returns true if the domain object is an annotation
*/
isAnnotation(domainObject) {
return domainObject && (domainObject.type === ANNOTATION_TYPE);
const fullTagModels = this.#addTagMetaInformationToTags(tagsFromAnnotations);
return fullTagModels;
}
#addTagMetaInformationToTags(tags) {
return tags.map((tagKey) => {
const tagModel = this.availableTags[tagKey];
tagModel.tagID = tagKey;
return tagModel;
});
}
#getMatchingTags(query) {
if (!query) {
return [];
}
/**
* @method getAvailableTags
* @returns {Tag[]} Returns an array of the available tags that have been loaded
*/
getAvailableTags() {
if (this.availableTags) {
const rearrangedToArray = Object.keys(this.availableTags).map(tagKey => {
return {
id: tagKey,
...this.availableTags[tagKey]
};
});
const matchingTags = Object.keys(this.availableTags).filter((tagKey) => {
if (this.availableTags[tagKey] && this.availableTags[tagKey].label) {
return this.availableTags[tagKey].label.toLowerCase().includes(query.toLowerCase());
}
return rearrangedToArray;
} else {
return [];
}
}
return false;
});
/**
* @method getAnnotations
* @param {Identifier} domainObjectIdentifier - The domain object identifier to use to search for annotations. For example, a telemetry object identifier.
* @returns {DomainObject[]} Returns an array of annotations that match the search query
*/
async getAnnotations(domainObjectIdentifier) {
const keyStringQuery = this.openmct.objects.makeKeyString(domainObjectIdentifier);
const searchResults = (await Promise.all(this.openmct.objects.search(keyStringQuery, null, this.openmct.objects.SEARCH_TYPES.ANNOTATIONS))).flat();
return matchingTags;
}
return searchResults;
}
#addTagMetaInformationToResults(results, matchingTagKeys) {
const tagsAddedToResults = results.map((result) => {
const fullTagModels = this.#addTagMetaInformationToTags(result.tags);
/**
* @method deleteAnnotations
* @param {DomainObject[]} existingAnnotation - An array of annotations to delete (set _deleted to true)
*/
deleteAnnotations(annotations) {
if (!annotations) {
throw new Error('Asked to delete null annotations! 🙅‍♂️');
}
return {
fullTagModels,
matchingTagKeys,
...result
};
});
annotations.forEach(annotation => {
if (!annotation._deleted) {
this.openmct.objects.mutate(annotation, '_deleted', true);
}
});
}
return tagsAddedToResults;
}
/**
* @method deleteAnnotations
* @param {DomainObject} annotation - An annotation to undelete (set _deleted to false)
*/
unDeleteAnnotation(annotation) {
if (!annotation) {
throw new Error('Asked to undelete null annotation! 🙅‍♂️');
}
this.openmct.objects.mutate(annotation, '_deleted', false);
}
getTagsFromAnnotations(annotations, filterDuplicates = true) {
if (!annotations) {
return [];
}
let tagsFromAnnotations = annotations.flatMap((annotation) => {
if (annotation._deleted) {
return [];
} else {
return annotation.tags;
}
});
if (filterDuplicates) {
tagsFromAnnotations = tagsFromAnnotations.filter((tag, index, tagArray) => {
return tagArray.indexOf(tag) === index;
});
}
const fullTagModels = this.#addTagMetaInformationToTags(tagsFromAnnotations);
return fullTagModels;
}
#addTagMetaInformationToTags(tags) {
return tags.map(tagKey => {
const tagModel = this.availableTags[tagKey];
tagModel.tagID = tagKey;
return tagModel;
});
}
#getMatchingTags(query) {
if (!query) {
return [];
}
const matchingTags = Object.keys(this.availableTags).filter(tagKey => {
if (this.availableTags[tagKey] && this.availableTags[tagKey].label) {
return this.availableTags[tagKey].label.toLowerCase().includes(query.toLowerCase());
}
return false;
});
return matchingTags;
}
#addTagMetaInformationToResults(results, matchingTagKeys) {
const tagsAddedToResults = results.map(result => {
const fullTagModels = this.#addTagMetaInformationToTags(result.tags);
async #addTargetModelsToResults(results) {
const modelAddedToResults = await Promise.all(
results.map(async (result) => {
const targetModels = await Promise.all(
Object.keys(result.targets).map(async (targetID) => {
const targetModel = await this.openmct.objects.get(targetID);
const targetKeyString = this.openmct.objects.makeKeyString(targetModel.identifier);
const originalPathObjects = await this.openmct.objects.getOriginalPath(targetKeyString);
return {
fullTagModels,
matchingTagKeys,
...result
originalPath: originalPathObjects,
...targetModel
};
});
})
);
return tagsAddedToResults;
return {
targetModels,
...result
};
})
);
return modelAddedToResults;
}
#combineSameTargets(results) {
const combinedResults = [];
results.forEach((currentAnnotation) => {
const existingAnnotation = combinedResults.find((annotationToFind) => {
return _.isEqual(currentAnnotation.targets, annotationToFind.targets);
});
if (!existingAnnotation) {
combinedResults.push(currentAnnotation);
} else {
existingAnnotation.tags.push(...currentAnnotation.tags);
}
});
return combinedResults;
}
/**
* @method #breakApartSeparateTargets
* @param {Array} results A set of search results that could have the multiple targets for the same result
* @returns {Array} The same set of results, but with each target separated out into its own result
*/
#breakApartSeparateTargets(results) {
const separateResults = [];
results.forEach((result) => {
Object.keys(result.targets).forEach((targetID) => {
const separatedResult = {
...result
};
separatedResult.targets = {
[targetID]: result.targets[targetID]
};
separatedResult.targetModels = result.targetModels.filter((targetModel) => {
const targetKeyString = this.openmct.objects.makeKeyString(targetModel.identifier);
return targetKeyString === targetID;
});
separateResults.push(separatedResult);
});
});
return separateResults;
}
/**
* @method searchForTags
* @param {String} query A query to match against tags. E.g., "dr" will match the tags "drilling" and "driving"
* @param {Object} [abortController] An optional abort method to stop the query
* @returns {Promise} returns a model of matching tags with their target domain objects attached
*/
async searchForTags(query, abortController) {
const matchingTagKeys = this.#getMatchingTags(query);
if (!matchingTagKeys.length) {
return [];
}
async #addTargetModelsToResults(results) {
const modelAddedToResults = await Promise.all(results.map(async result => {
const targetModels = await Promise.all(Object.keys(result.targets).map(async (targetID) => {
const targetModel = await this.openmct.objects.get(targetID);
const targetKeyString = this.openmct.objects.makeKeyString(targetModel.identifier);
const originalPathObjects = await this.openmct.objects.getOriginalPath(targetKeyString);
const searchResults = (
await Promise.all(
this.openmct.objects.search(
matchingTagKeys,
abortController,
this.openmct.objects.SEARCH_TYPES.TAGS
)
)
).flat();
const filteredDeletedResults = searchResults.filter((result) => {
return !result._deleted;
});
const combinedSameTargets = this.#combineSameTargets(filteredDeletedResults);
const appliedTagSearchResults = this.#addTagMetaInformationToResults(
combinedSameTargets,
matchingTagKeys
);
const appliedTargetsModels = await this.#addTargetModelsToResults(appliedTagSearchResults);
const resultsWithValidPath = appliedTargetsModels.filter((result) => {
return this.openmct.objects.isReachable(result.targetModels?.[0]?.originalPath);
});
const breakApartSeparateTargets = this.#breakApartSeparateTargets(resultsWithValidPath);
return {
originalPath: originalPathObjects,
...targetModel
};
}));
return {
targetModels,
...result
};
}));
return modelAddedToResults;
}
#combineSameTargets(results) {
const combinedResults = [];
results.forEach(currentAnnotation => {
const existingAnnotation = combinedResults.find((annotationToFind) => {
return _.isEqual(currentAnnotation.targets, annotationToFind.targets);
});
if (!existingAnnotation) {
combinedResults.push(currentAnnotation);
} else {
existingAnnotation.tags.push(...currentAnnotation.tags);
}
});
return combinedResults;
}
/**
* @method #breakApartSeparateTargets
* @param {Array} results A set of search results that could have the multiple targets for the same result
* @returns {Array} The same set of results, but with each target separated out into its own result
*/
#breakApartSeparateTargets(results) {
const separateResults = [];
results.forEach(result => {
Object.keys(result.targets).forEach(targetID => {
const separatedResult = {
...result
};
separatedResult.targets = {
[targetID]: result.targets[targetID]
};
separatedResult.targetModels = result.targetModels.filter(targetModel => {
const targetKeyString = this.openmct.objects.makeKeyString(targetModel.identifier);
return targetKeyString === targetID;
});
separateResults.push(separatedResult);
});
});
return separateResults;
}
/**
* @method searchForTags
* @param {String} query A query to match against tags. E.g., "dr" will match the tags "drilling" and "driving"
* @param {Object} [abortController] An optional abort method to stop the query
* @returns {Promise} returns a model of matching tags with their target domain objects attached
*/
async searchForTags(query, abortController) {
const matchingTagKeys = this.#getMatchingTags(query);
if (!matchingTagKeys.length) {
return [];
}
const searchResults = (await Promise.all(this.openmct.objects.search(matchingTagKeys, abortController, this.openmct.objects.SEARCH_TYPES.TAGS))).flat();
const filteredDeletedResults = searchResults.filter((result) => {
return !(result._deleted);
});
const combinedSameTargets = this.#combineSameTargets(filteredDeletedResults);
const appliedTagSearchResults = this.#addTagMetaInformationToResults(combinedSameTargets, matchingTagKeys);
const appliedTargetsModels = await this.#addTargetModelsToResults(appliedTagSearchResults);
const resultsWithValidPath = appliedTargetsModels.filter(result => {
return this.openmct.objects.isReachable(result.targetModels?.[0]?.originalPath);
});
const breakApartSeparateTargets = this.#breakApartSeparateTargets(resultsWithValidPath);
return breakApartSeparateTargets;
}
return breakApartSeparateTargets;
}
}

View File

@@ -21,247 +21,248 @@
*****************************************************************************/
import { createOpenMct, resetApplicationState } from '../../utils/testing';
import ExampleTagsPlugin from "../../../example/exampleTags/plugin";
import ExampleTagsPlugin from '../../../example/exampleTags/plugin';
describe("The Annotation API", () => {
let openmct;
let mockObjectProvider;
let mockImmutableObjectProvider;
let mockDomainObject;
let mockFolderObject;
let mockAnnotationObject;
describe('The Annotation API', () => {
let openmct;
let mockObjectProvider;
let mockImmutableObjectProvider;
let mockDomainObject;
let mockFolderObject;
let mockAnnotationObject;
beforeEach((done) => {
openmct = createOpenMct();
openmct.install(new ExampleTagsPlugin());
const availableTags = openmct.annotation.getAvailableTags();
mockFolderObject = {
type: 'root',
name: 'folderFoo',
location: '',
identifier: {
key: 'someParent',
namespace: 'fooNameSpace'
}
beforeEach((done) => {
openmct = createOpenMct();
openmct.install(new ExampleTagsPlugin());
const availableTags = openmct.annotation.getAvailableTags();
mockFolderObject = {
type: 'root',
name: 'folderFoo',
location: '',
identifier: {
key: 'someParent',
namespace: 'fooNameSpace'
}
};
mockDomainObject = {
type: 'notebook',
name: 'fooRabbitNotebook',
location: 'fooNameSpace:someParent',
identifier: {
key: 'some-object',
namespace: 'fooNameSpace'
}
};
mockAnnotationObject = {
type: 'annotation',
name: 'Some Notebook Annotation',
annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
tags: [availableTags[0].id, availableTags[1].id],
identifier: {
key: 'anAnnotationKey',
namespace: 'fooNameSpace'
},
targets: {
'fooNameSpace:some-object': {
entryId: 'fooBarEntry'
}
}
};
mockObjectProvider = jasmine.createSpyObj('mock provider', ['create', 'update', 'get']);
// eslint-disable-next-line require-await
mockObjectProvider.get = async (identifier) => {
if (identifier.key === mockDomainObject.identifier.key) {
return mockDomainObject;
} else if (identifier.key === mockAnnotationObject.identifier.key) {
return mockAnnotationObject;
} else if (identifier.key === mockFolderObject.identifier.key) {
return mockFolderObject;
} else {
return null;
}
};
mockObjectProvider.create.and.returnValue(Promise.resolve(true));
mockObjectProvider.update.and.returnValue(Promise.resolve(true));
mockImmutableObjectProvider = jasmine.createSpyObj('mock immutable provider', ['get']);
// eslint-disable-next-line require-await
mockImmutableObjectProvider.get = async (identifier) => {
if (identifier.key === mockDomainObject.identifier.key) {
return mockDomainObject;
} else if (identifier.key === mockAnnotationObject.identifier.key) {
return mockAnnotationObject;
} else if (identifier.key === mockFolderObject.identifier.key) {
return mockFolderObject;
} else {
return null;
}
};
openmct.objects.addProvider('immutableProvider', mockImmutableObjectProvider);
openmct.objects.addProvider('fooNameSpace', mockObjectProvider);
openmct.on('start', done);
openmct.startHeadless();
});
afterEach(async () => {
await resetApplicationState(openmct);
});
it('is defined', () => {
expect(openmct.annotation).toBeDefined();
});
describe('Creation', () => {
it('can create annotations', async () => {
const annotationCreationArguments = {
name: 'Test Annotation',
domainObject: mockDomainObject,
annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
tags: ['sometag'],
contentText: 'fooContext',
targetDomainObjects: [mockDomainObject],
targets: { fooTarget: {} }
};
const annotationObject = await openmct.annotation.create(annotationCreationArguments);
expect(annotationObject).toBeDefined();
expect(annotationObject.type).toEqual('annotation');
});
it('can create annotations if domain object is immutable', async () => {
mockDomainObject.identifier.namespace = 'immutableProvider';
const annotationCreationArguments = {
name: 'Test Annotation',
domainObject: mockDomainObject,
annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
tags: ['sometag'],
contentText: 'fooContext',
targetDomainObjects: [mockDomainObject],
targets: { fooTarget: {} }
};
openmct.annotation.setNamespaceToSaveAnnotations('fooNameSpace');
const annotationObject = await openmct.annotation.create(annotationCreationArguments);
expect(annotationObject).toBeDefined();
expect(annotationObject.type).toEqual('annotation');
});
it('fails if annotation is an unknown type', async () => {
try {
await openmct.annotation.create(
'Garbage Annotation',
mockDomainObject,
'garbageAnnotation',
['sometag'],
'fooContext',
{ fooTarget: {} }
);
} catch (error) {
expect(error).toBeDefined();
}
});
it('fails if annotation if given an immutable namespace to save to', async () => {
try {
const annotationCreationArguments = {
name: 'Test Annotation',
domainObject: mockDomainObject,
annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
tags: ['sometag'],
contentText: 'fooContext',
targetDomainObjects: [mockDomainObject],
targets: { fooTarget: {} }
};
mockDomainObject = {
type: 'notebook',
name: 'fooRabbitNotebook',
location: 'fooNameSpace:someParent',
identifier: {
key: 'some-object',
namespace: 'fooNameSpace'
}
openmct.annotation.setNamespaceToSaveAnnotations('nameespaceThatDoesNotExist');
await openmct.annotation.create(annotationCreationArguments);
} catch (error) {
expect(error).toBeDefined();
}
});
it('fails if annotation if given an undefined namespace to save to', async () => {
try {
const annotationCreationArguments = {
name: 'Test Annotation',
domainObject: mockDomainObject,
annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
tags: ['sometag'],
contentText: 'fooContext',
targetDomainObjects: [mockDomainObject],
targets: { fooTarget: {} }
};
mockAnnotationObject = {
type: 'annotation',
name: 'Some Notebook Annotation',
annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
tags: [availableTags[0].id, availableTags[1].id],
identifier: {
key: 'anAnnotationKey',
namespace: 'fooNameSpace'
},
targets: {
'fooNameSpace:some-object': {
entryId: 'fooBarEntry'
}
}
};
mockObjectProvider = jasmine.createSpyObj("mock provider", [
"create",
"update",
"get"
]);
// eslint-disable-next-line require-await
mockObjectProvider.get = async (identifier) => {
if (identifier.key === mockDomainObject.identifier.key) {
return mockDomainObject;
} else if (identifier.key === mockAnnotationObject.identifier.key) {
return mockAnnotationObject;
} else if (identifier.key === mockFolderObject.identifier.key) {
return mockFolderObject;
} else {
return null;
}
};
mockObjectProvider.create.and.returnValue(Promise.resolve(true));
mockObjectProvider.update.and.returnValue(Promise.resolve(true));
mockImmutableObjectProvider = jasmine.createSpyObj("mock immutable provider", [
"get"
]);
// eslint-disable-next-line require-await
mockImmutableObjectProvider.get = async (identifier) => {
if (identifier.key === mockDomainObject.identifier.key) {
return mockDomainObject;
} else if (identifier.key === mockAnnotationObject.identifier.key) {
return mockAnnotationObject;
} else if (identifier.key === mockFolderObject.identifier.key) {
return mockFolderObject;
} else {
return null;
}
};
openmct.objects.addProvider('immutableProvider', mockImmutableObjectProvider);
openmct.objects.addProvider('fooNameSpace', mockObjectProvider);
openmct.on('start', done);
openmct.startHeadless();
});
afterEach(async () => {
await resetApplicationState(openmct);
});
it("is defined", () => {
expect(openmct.annotation).toBeDefined();
openmct.annotation.setNamespaceToSaveAnnotations('immutableProvider');
await openmct.annotation.create(annotationCreationArguments);
} catch (error) {
expect(error).toBeDefined();
}
});
});
describe("Creation", () => {
it("can create annotations", async () => {
const annotationCreationArguments = {
name: 'Test Annotation',
domainObject: mockDomainObject,
annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
tags: ['sometag'],
contentText: "fooContext",
targetDomainObjects: [mockDomainObject],
targets: {'fooTarget': {}}
};
const annotationObject = await openmct.annotation.create(annotationCreationArguments);
expect(annotationObject).toBeDefined();
expect(annotationObject.type).toEqual('annotation');
});
it("can create annotations if domain object is immutable", async () => {
mockDomainObject.identifier.namespace = 'immutableProvider';
const annotationCreationArguments = {
name: 'Test Annotation',
domainObject: mockDomainObject,
annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
tags: ['sometag'],
contentText: "fooContext",
targetDomainObjects: [mockDomainObject],
targets: {'fooTarget': {}}
};
openmct.annotation.setNamespaceToSaveAnnotations('fooNameSpace');
const annotationObject = await openmct.annotation.create(annotationCreationArguments);
expect(annotationObject).toBeDefined();
expect(annotationObject.type).toEqual('annotation');
});
it("fails if annotation is an unknown type", async () => {
try {
await openmct.annotation.create('Garbage Annotation', mockDomainObject, 'garbageAnnotation', ['sometag'], "fooContext", {'fooTarget': {}});
} catch (error) {
expect(error).toBeDefined();
}
});
it("fails if annotation if given an immutable namespace to save to", async () => {
try {
const annotationCreationArguments = {
name: 'Test Annotation',
domainObject: mockDomainObject,
annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
tags: ['sometag'],
contentText: "fooContext",
targetDomainObjects: [mockDomainObject],
targets: {'fooTarget': {}}
};
openmct.annotation.setNamespaceToSaveAnnotations('nameespaceThatDoesNotExist');
await openmct.annotation.create(annotationCreationArguments);
} catch (error) {
expect(error).toBeDefined();
}
});
it("fails if annotation if given an undefined namespace to save to", async () => {
try {
const annotationCreationArguments = {
name: 'Test Annotation',
domainObject: mockDomainObject,
annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
tags: ['sometag'],
contentText: "fooContext",
targetDomainObjects: [mockDomainObject],
targets: {'fooTarget': {}}
};
openmct.annotation.setNamespaceToSaveAnnotations('immutableProvider');
await openmct.annotation.create(annotationCreationArguments);
} catch (error) {
expect(error).toBeDefined();
}
});
describe('Tagging', () => {
let tagCreationArguments;
beforeEach(() => {
tagCreationArguments = {
name: 'Test Annotation',
domainObject: mockDomainObject,
annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
tags: ['aWonderfulTag'],
contentText: 'fooContext',
targets: { 'fooNameSpace:some-object': { entryId: 'fooBarEntry' } },
targetDomainObjects: [mockDomainObject]
};
});
it('can create a tag', async () => {
const annotationObject = await openmct.annotation.create(tagCreationArguments);
expect(annotationObject).toBeDefined();
expect(annotationObject.type).toEqual('annotation');
expect(annotationObject.tags).toContain('aWonderfulTag');
});
it('can delete a tag', async () => {
const annotationObject = await openmct.annotation.create(tagCreationArguments);
expect(annotationObject).toBeDefined();
openmct.annotation.deleteAnnotations([annotationObject]);
expect(annotationObject._deleted).toBeTrue();
});
it('can remove all tags', async () => {
const annotationObject = await openmct.annotation.create(tagCreationArguments);
expect(annotationObject).toBeDefined();
expect(() => {
openmct.annotation.deleteAnnotations([annotationObject]);
}).not.toThrow();
expect(annotationObject._deleted).toBeTrue();
});
it('can add/delete/add a tag', async () => {
let annotationObject = await openmct.annotation.create(tagCreationArguments);
expect(annotationObject).toBeDefined();
expect(annotationObject.type).toEqual('annotation');
expect(annotationObject.tags).toContain('aWonderfulTag');
openmct.annotation.deleteAnnotations([annotationObject]);
expect(annotationObject._deleted).toBeTrue();
annotationObject = await openmct.annotation.create(tagCreationArguments);
expect(annotationObject).toBeDefined();
expect(annotationObject.type).toEqual('annotation');
expect(annotationObject.tags).toContain('aWonderfulTag');
expect(annotationObject._deleted).toBeFalse();
});
});
describe("Tagging", () => {
let tagCreationArguments;
beforeEach(() => {
tagCreationArguments = {
name: 'Test Annotation',
domainObject: mockDomainObject,
annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
tags: ['aWonderfulTag'],
contentText: 'fooContext',
targets: {'fooNameSpace:some-object': {entryId: 'fooBarEntry'}},
targetDomainObjects: [mockDomainObject]
};
});
it("can create a tag", async () => {
const annotationObject = await openmct.annotation.create(tagCreationArguments);
expect(annotationObject).toBeDefined();
expect(annotationObject.type).toEqual('annotation');
expect(annotationObject.tags).toContain('aWonderfulTag');
});
it("can delete a tag", async () => {
const annotationObject = await openmct.annotation.create(tagCreationArguments);
expect(annotationObject).toBeDefined();
openmct.annotation.deleteAnnotations([annotationObject]);
expect(annotationObject._deleted).toBeTrue();
});
it("can remove all tags", async () => {
const annotationObject = await openmct.annotation.create(tagCreationArguments);
expect(annotationObject).toBeDefined();
expect(() => {
openmct.annotation.deleteAnnotations([annotationObject]);
}).not.toThrow();
expect(annotationObject._deleted).toBeTrue();
});
it("can add/delete/add a tag", async () => {
let annotationObject = await openmct.annotation.create(tagCreationArguments);
expect(annotationObject).toBeDefined();
expect(annotationObject.type).toEqual('annotation');
expect(annotationObject.tags).toContain('aWonderfulTag');
openmct.annotation.deleteAnnotations([annotationObject]);
expect(annotationObject._deleted).toBeTrue();
annotationObject = await openmct.annotation.create(tagCreationArguments);
expect(annotationObject).toBeDefined();
expect(annotationObject.type).toEqual('annotation');
expect(annotationObject.tags).toContain('aWonderfulTag');
expect(annotationObject._deleted).toBeFalse();
});
describe('Search', () => {
let sharedWorkerToRestore;
beforeEach(async () => {
// use local worker
sharedWorkerToRestore = openmct.objects.inMemorySearchProvider.worker;
openmct.objects.inMemorySearchProvider.worker = null;
await openmct.objects.inMemorySearchProvider.index(mockFolderObject);
await openmct.objects.inMemorySearchProvider.index(mockDomainObject);
await openmct.objects.inMemorySearchProvider.index(mockAnnotationObject);
});
describe("Search", () => {
let sharedWorkerToRestore;
beforeEach(async () => {
// use local worker
sharedWorkerToRestore = openmct.objects.inMemorySearchProvider.worker;
openmct.objects.inMemorySearchProvider.worker = null;
await openmct.objects.inMemorySearchProvider.index(mockFolderObject);
await openmct.objects.inMemorySearchProvider.index(mockDomainObject);
await openmct.objects.inMemorySearchProvider.index(mockAnnotationObject);
});
afterEach(() => {
openmct.objects.inMemorySearchProvider.worker = sharedWorkerToRestore;
});
it("can search for tags", async () => {
const results = await openmct.annotation.searchForTags('S');
expect(results).toBeDefined();
expect(results.length).toEqual(1);
});
it("returns no tags for empty search", async () => {
const results = await openmct.annotation.searchForTags('q');
expect(results).toBeDefined();
expect(results.length).toEqual(0);
});
afterEach(() => {
openmct.objects.inMemorySearchProvider.worker = sharedWorkerToRestore;
});
it('can search for tags', async () => {
const results = await openmct.annotation.searchForTags('S');
expect(results).toBeDefined();
expect(results.length).toEqual(1);
});
it('returns no tags for empty search', async () => {
const results = await openmct.annotation.searchForTags('q');
expect(results).toBeDefined();
expect(results.length).toEqual(0);
});
});
});

View File

@@ -21,58 +21,56 @@
*****************************************************************************/
define([
'./actions/ActionsAPI',
'./composition/CompositionAPI',
'./Editor',
'./faultmanagement/FaultManagementAPI',
'./forms/FormsAPI',
'./indicators/IndicatorAPI',
'./menu/MenuAPI',
'./notifications/NotificationAPI',
'./objects/ObjectAPI',
'./priority/PriorityAPI',
'./status/StatusAPI',
'./telemetry/TelemetryAPI',
'./time/TimeAPI',
'./types/TypeRegistry',
'./user/UserAPI',
'./annotation/AnnotationAPI'
],
function (
ActionsAPI,
CompositionAPI,
EditorAPI,
FaultManagementAPI,
FormsAPI,
IndicatorAPI,
MenuAPI,
NotificationAPI,
ObjectAPI,
PriorityAPI,
StatusAPI,
TelemetryAPI,
TimeAPI,
TypeRegistry,
UserAPI,
AnnotationAPI
'./actions/ActionsAPI',
'./composition/CompositionAPI',
'./Editor',
'./faultmanagement/FaultManagementAPI',
'./forms/FormsAPI',
'./indicators/IndicatorAPI',
'./menu/MenuAPI',
'./notifications/NotificationAPI',
'./objects/ObjectAPI',
'./priority/PriorityAPI',
'./status/StatusAPI',
'./telemetry/TelemetryAPI',
'./time/TimeAPI',
'./types/TypeRegistry',
'./user/UserAPI',
'./annotation/AnnotationAPI'
], function (
ActionsAPI,
CompositionAPI,
EditorAPI,
FaultManagementAPI,
FormsAPI,
IndicatorAPI,
MenuAPI,
NotificationAPI,
ObjectAPI,
PriorityAPI,
StatusAPI,
TelemetryAPI,
TimeAPI,
TypeRegistry,
UserAPI,
AnnotationAPI
) {
return {
ActionsAPI: ActionsAPI.default,
CompositionAPI: CompositionAPI,
EditorAPI: EditorAPI,
FaultManagementAPI: FaultManagementAPI,
FormsAPI: FormsAPI,
IndicatorAPI: IndicatorAPI.default,
MenuAPI: MenuAPI.default,
NotificationAPI: NotificationAPI.default,
ObjectAPI: ObjectAPI,
PriorityAPI: PriorityAPI.default,
StatusAPI: StatusAPI.default,
TelemetryAPI: TelemetryAPI,
TimeAPI: TimeAPI.default,
TypeRegistry: TypeRegistry.default,
UserAPI: UserAPI.default,
AnnotationAPI: AnnotationAPI.default
};
return {
ActionsAPI: ActionsAPI.default,
CompositionAPI: CompositionAPI,
EditorAPI: EditorAPI,
FaultManagementAPI: FaultManagementAPI,
FormsAPI: FormsAPI,
IndicatorAPI: IndicatorAPI.default,
MenuAPI: MenuAPI.default,
NotificationAPI: NotificationAPI.default,
ObjectAPI: ObjectAPI,
PriorityAPI: PriorityAPI.default,
StatusAPI: StatusAPI.default,
TelemetryAPI: TelemetryAPI,
TimeAPI: TimeAPI.default,
TypeRegistry: TypeRegistry.default,
UserAPI: UserAPI.default,
AnnotationAPI: AnnotationAPI.default
};
});

View File

@@ -43,102 +43,101 @@ import CompositionCollection from './CompositionCollection';
* @constructor
*/
export default class CompositionAPI {
/**
* @param {OpenMCT} publicAPI
*/
constructor(publicAPI) {
/** @type {CompositionProvider[]} */
this.registry = [];
/** @type {CompositionPolicy[]} */
this.policies = [];
this.addProvider(new DefaultCompositionProvider(publicAPI, this));
/** @type {OpenMCT} */
this.publicAPI = publicAPI;
}
/**
* Add a composition provider.
*
* Plugins can add new composition providers to change the loading
* behavior for certain domain objects.
*
* @method addProvider
* @param {CompositionProvider} provider the provider to add
*/
addProvider(provider) {
this.registry.unshift(provider);
}
/**
* Retrieve the composition (if any) of this domain object.
*
* @method get
* @param {DomainObject} domainObject
* @returns {CompositionCollection}
*/
get(domainObject) {
const provider = this.registry.find(p => {
return p.appliesTo(domainObject);
});
/**
* @param {OpenMCT} publicAPI
*/
constructor(publicAPI) {
/** @type {CompositionProvider[]} */
this.registry = [];
/** @type {CompositionPolicy[]} */
this.policies = [];
this.addProvider(new DefaultCompositionProvider(publicAPI, this));
/** @type {OpenMCT} */
this.publicAPI = publicAPI;
}
/**
* Add a composition provider.
*
* Plugins can add new composition providers to change the loading
* behavior for certain domain objects.
*
* @method addProvider
* @param {CompositionProvider} provider the provider to add
*/
addProvider(provider) {
this.registry.unshift(provider);
}
/**
* Retrieve the composition (if any) of this domain object.
*
* @method get
* @param {DomainObject} domainObject
* @returns {CompositionCollection}
*/
get(domainObject) {
const provider = this.registry.find((p) => {
return p.appliesTo(domainObject);
});
if (!provider) {
return;
}
return new CompositionCollection(domainObject, provider, this.publicAPI);
}
/**
* A composition policy is a function which either allows or disallows
* placing one object in another's composition.
*
* Open MCT's policy model requires consensus, so any one policy may
* reject composition by returning false. As such, policies should
* generally be written to return true in the default case.
*
* @callback CompositionPolicy
* @param {DomainObject} containingObject the object which
* would act as a container
* @param {DomainObject} containedObject the object which
* would be contained
* @returns {boolean} false if this composition should be disallowed
*/
/**
* Add a composition policy. Composition policies may disallow domain
* objects from containing other domain objects.
*
* @method addPolicy
* @param {CompositionPolicy} policy
* the policy to add
*/
addPolicy(policy) {
this.policies.push(policy);
}
/**
* Check whether or not a domain object is allowed to contain another
* domain object.
*
* @private
* @method checkPolicy
* @param {DomainObject} container the object which
* would act as a container
* @param {DomainObject} containee the object which
* would be contained
* @returns {boolean} false if this composition should be disallowed
* @param {CompositionPolicy} policy
* the policy to add
*/
checkPolicy(container, containee) {
return this.policies.every(function (policy) {
return policy(container, containee);
});
if (!provider) {
return;
}
/**
* Check whether or not a domainObject supports composition
*
* @param {DomainObject} domainObject
* @returns {boolean} true if the domainObject supports composition
*/
supportsComposition(domainObject) {
return this.get(domainObject) !== undefined;
}
return new CompositionCollection(domainObject, provider, this.publicAPI);
}
/**
* A composition policy is a function which either allows or disallows
* placing one object in another's composition.
*
* Open MCT's policy model requires consensus, so any one policy may
* reject composition by returning false. As such, policies should
* generally be written to return true in the default case.
*
* @callback CompositionPolicy
* @param {DomainObject} containingObject the object which
* would act as a container
* @param {DomainObject} containedObject the object which
* would be contained
* @returns {boolean} false if this composition should be disallowed
*/
/**
* Add a composition policy. Composition policies may disallow domain
* objects from containing other domain objects.
*
* @method addPolicy
* @param {CompositionPolicy} policy
* the policy to add
*/
addPolicy(policy) {
this.policies.push(policy);
}
/**
* Check whether or not a domain object is allowed to contain another
* domain object.
*
* @private
* @method checkPolicy
* @param {DomainObject} container the object which
* would act as a container
* @param {DomainObject} containee the object which
* would be contained
* @returns {boolean} false if this composition should be disallowed
* @param {CompositionPolicy} policy
* the policy to add
*/
checkPolicy(container, containee) {
return this.policies.every(function (policy) {
return policy(container, containee);
});
}
/**
* Check whether or not a domainObject supports composition
*
* @param {DomainObject} domainObject
* @returns {boolean} true if the domainObject supports composition
*/
supportsComposition(domainObject) {
return this.get(domainObject) !== undefined;
}
}

View File

@@ -2,311 +2,306 @@ import { createOpenMct, resetApplicationState } from '../../utils/testing';
import CompositionCollection from './CompositionCollection';
describe('The Composition API', function () {
let publicAPI;
let compositionAPI;
let publicAPI;
let compositionAPI;
beforeEach(function (done) {
publicAPI = createOpenMct();
compositionAPI = publicAPI.composition;
beforeEach(function (done) {
publicAPI = createOpenMct();
compositionAPI = publicAPI.composition;
const mockObjectProvider = jasmine.createSpyObj("mock provider", [
"create",
"update",
"get"
]);
const mockObjectProvider = jasmine.createSpyObj('mock provider', ['create', 'update', 'get']);
mockObjectProvider.create.and.returnValue(Promise.resolve(true));
mockObjectProvider.update.and.returnValue(Promise.resolve(true));
mockObjectProvider.get.and.callFake((identifier) => {
return Promise.resolve({identifier});
});
publicAPI.objects.addProvider('test', mockObjectProvider);
publicAPI.objects.addProvider('custom', mockObjectProvider);
publicAPI.on('start', done);
publicAPI.startHeadless();
mockObjectProvider.create.and.returnValue(Promise.resolve(true));
mockObjectProvider.update.and.returnValue(Promise.resolve(true));
mockObjectProvider.get.and.callFake((identifier) => {
return Promise.resolve({ identifier });
});
afterEach(() => {
return resetApplicationState(publicAPI);
publicAPI.objects.addProvider('test', mockObjectProvider);
publicAPI.objects.addProvider('custom', mockObjectProvider);
publicAPI.on('start', done);
publicAPI.startHeadless();
});
afterEach(() => {
return resetApplicationState(publicAPI);
});
it('returns falsy if an object does not support composition', function () {
expect(compositionAPI.get({})).toBeFalsy();
});
describe('default composition', function () {
let domainObject;
let composition;
beforeEach(function () {
domainObject = {
name: 'test folder',
identifier: {
namespace: 'test',
key: '1'
},
composition: [
{
namespace: 'test',
key: 'a'
},
{
namespace: 'test',
key: 'b'
},
{
namespace: 'test',
key: 'c'
}
]
};
composition = compositionAPI.get(domainObject);
});
it('returns falsy if an object does not support composition', function () {
expect(compositionAPI.get({})).toBeFalsy();
it('returns composition collection', function () {
expect(composition).toBeDefined();
expect(composition).toEqual(jasmine.any(CompositionCollection));
});
describe('default composition', function () {
let domainObject;
let composition;
beforeEach(function () {
domainObject = {
name: 'test folder',
identifier: {
namespace: 'test',
key: '1'
},
composition: [
{
namespace: 'test',
key: 'a'
},
{
namespace: 'test',
key: 'b'
},
{
namespace: 'test',
key: 'c'
}
]
};
composition = compositionAPI.get(domainObject);
});
it('returns composition collection', function () {
expect(composition).toBeDefined();
expect(composition).toEqual(jasmine.any(CompositionCollection));
});
it('correctly reflects composability', function () {
expect(compositionAPI.supportsComposition(domainObject)).toBe(true);
delete domainObject.composition;
expect(compositionAPI.supportsComposition(domainObject)).toBe(false);
});
it('loads composition from domain object', function () {
const listener = jasmine.createSpy('addListener');
composition.on('add', listener);
return composition.load().then(function () {
expect(listener.calls.count()).toBe(3);
expect(listener).toHaveBeenCalledWith({
identifier: {
namespace: 'test',
key: 'a'
}
});
});
});
describe('supports reordering of composition', function () {
let listener;
beforeEach(function () {
listener = jasmine.createSpy('reorderListener');
spyOn(publicAPI.objects, 'mutate');
publicAPI.objects.mutate.and.callThrough();
composition.on('reorder', listener);
return composition.load();
});
it('', function () {
composition.reorder(1, 0);
let newComposition =
publicAPI.objects.mutate.calls.mostRecent().args[2];
let reorderPlan = listener.calls.mostRecent().args[0][0];
expect(reorderPlan.oldIndex).toBe(1);
expect(reorderPlan.newIndex).toBe(0);
expect(newComposition[0].key).toEqual('b');
expect(newComposition[1].key).toEqual('a');
expect(newComposition[2].key).toEqual('c');
});
it('', function () {
composition.reorder(0, 2);
let newComposition =
publicAPI.objects.mutate.calls.mostRecent().args[2];
let reorderPlan = listener.calls.mostRecent().args[0][0];
expect(reorderPlan.oldIndex).toBe(0);
expect(reorderPlan.newIndex).toBe(2);
expect(newComposition[0].key).toEqual('b');
expect(newComposition[1].key).toEqual('c');
expect(newComposition[2].key).toEqual('a');
});
});
it('supports adding an object to composition', function () {
let mockChildObject = {
identifier: {
key: 'mock-key',
namespace: ''
}
};
return new Promise((resolve) => {
composition.on('add', resolve);
composition.add(mockChildObject);
}).then(() => {
expect(domainObject.composition.length).toBe(4);
expect(domainObject.composition[3]).toEqual(mockChildObject.identifier);
});
});
it('correctly reflects composability', function () {
expect(compositionAPI.supportsComposition(domainObject)).toBe(true);
delete domainObject.composition;
expect(compositionAPI.supportsComposition(domainObject)).toBe(false);
});
describe('static custom composition', function () {
let customProvider;
let domainObject;
let composition;
it('loads composition from domain object', function () {
const listener = jasmine.createSpy('addListener');
composition.on('add', listener);
beforeEach(function () {
// A simple custom provider, returns the same composition for
// all objects of a given type.
customProvider = {
appliesTo: function (object) {
return object.type === 'custom-object-type';
},
load: function (object) {
return Promise.resolve([
{
namespace: 'custom',
key: 'thing'
}
]);
},
add: jasmine.createSpy('add'),
remove: jasmine.createSpy('remove')
};
domainObject = {
identifier: {
namespace: 'test',
key: '1'
},
type: 'custom-object-type'
};
compositionAPI.addProvider(customProvider);
composition = compositionAPI.get(domainObject);
return composition.load().then(function () {
expect(listener.calls.count()).toBe(3);
expect(listener).toHaveBeenCalledWith({
identifier: {
namespace: 'test',
key: 'a'
}
});
});
});
describe('supports reordering of composition', function () {
let listener;
beforeEach(function () {
listener = jasmine.createSpy('reorderListener');
spyOn(publicAPI.objects, 'mutate');
publicAPI.objects.mutate.and.callThrough();
it('supports listening and loading', function () {
const addListener = jasmine.createSpy('addListener');
composition.on('add', addListener);
composition.on('reorder', listener);
return composition.load().then(function (children) {
let listenObject;
const loadedObject = children[0];
return composition.load();
});
it('', function () {
composition.reorder(1, 0);
let newComposition = publicAPI.objects.mutate.calls.mostRecent().args[2];
let reorderPlan = listener.calls.mostRecent().args[0][0];
expect(addListener).toHaveBeenCalled();
expect(reorderPlan.oldIndex).toBe(1);
expect(reorderPlan.newIndex).toBe(0);
expect(newComposition[0].key).toEqual('b');
expect(newComposition[1].key).toEqual('a');
expect(newComposition[2].key).toEqual('c');
});
it('', function () {
composition.reorder(0, 2);
let newComposition = publicAPI.objects.mutate.calls.mostRecent().args[2];
let reorderPlan = listener.calls.mostRecent().args[0][0];
listenObject = addListener.calls.mostRecent().args[0];
expect(listenObject).toEqual(loadedObject);
expect(loadedObject).toEqual({
identifier: {
namespace: 'custom',
key: 'thing'
}
});
});
});
describe('Calling add or remove', function () {
let mockChildObject;
expect(reorderPlan.oldIndex).toBe(0);
expect(reorderPlan.newIndex).toBe(2);
expect(newComposition[0].key).toEqual('b');
expect(newComposition[1].key).toEqual('c');
expect(newComposition[2].key).toEqual('a');
});
});
it('supports adding an object to composition', function () {
let mockChildObject = {
identifier: {
key: 'mock-key',
namespace: ''
}
};
beforeEach(function () {
mockChildObject = {
identifier: {
key: 'mock-key',
namespace: ''
}
};
composition.add(mockChildObject);
});
return new Promise((resolve) => {
composition.on('add', resolve);
composition.add(mockChildObject);
}).then(() => {
expect(domainObject.composition.length).toBe(4);
expect(domainObject.composition[3]).toEqual(mockChildObject.identifier);
});
});
});
it('calls add on the provider', function () {
expect(customProvider.add).toHaveBeenCalledWith(domainObject, mockChildObject.identifier);
});
describe('static custom composition', function () {
let customProvider;
let domainObject;
let composition;
it('calls remove on the provider', function () {
composition.remove(mockChildObject);
expect(customProvider.remove).toHaveBeenCalledWith(domainObject, mockChildObject.identifier);
});
});
beforeEach(function () {
// A simple custom provider, returns the same composition for
// all objects of a given type.
customProvider = {
appliesTo: function (object) {
return object.type === 'custom-object-type';
},
load: function (object) {
return Promise.resolve([
{
namespace: 'custom',
key: 'thing'
}
]);
},
add: jasmine.createSpy('add'),
remove: jasmine.createSpy('remove')
};
domainObject = {
identifier: {
namespace: 'test',
key: '1'
},
type: 'custom-object-type'
};
compositionAPI.addProvider(customProvider);
composition = compositionAPI.get(domainObject);
});
describe('dynamic custom composition', function () {
let customProvider;
let domainObject;
let composition;
it('supports listening and loading', function () {
const addListener = jasmine.createSpy('addListener');
composition.on('add', addListener);
beforeEach(function () {
// A dynamic provider, loads an empty composition and exposes
// listener functions.
customProvider = jasmine.createSpyObj('dynamicProvider', [
'appliesTo',
'load',
'on',
'off'
]);
return composition.load().then(function (children) {
let listenObject;
const loadedObject = children[0];
customProvider.appliesTo.and.returnValue('true');
customProvider.load.and.returnValue(Promise.resolve([]));
expect(addListener).toHaveBeenCalled();
domainObject = {
identifier: {
namespace: 'test',
key: '1'
},
type: 'custom-object-type'
};
compositionAPI.addProvider(customProvider);
composition = compositionAPI.get(domainObject);
listenObject = addListener.calls.mostRecent().args[0];
expect(listenObject).toEqual(loadedObject);
expect(loadedObject).toEqual({
identifier: {
namespace: 'custom',
key: 'thing'
}
});
});
});
describe('Calling add or remove', function () {
let mockChildObject;
it('supports listening and loading', function () {
const addListener = jasmine.createSpy('addListener');
const removeListener = jasmine.createSpy('removeListener');
const addPromise = new Promise(function (resolve) {
addListener.and.callFake(resolve);
});
const removePromise = new Promise(function (resolve) {
removeListener.and.callFake(resolve);
});
beforeEach(function () {
mockChildObject = {
identifier: {
key: 'mock-key',
namespace: ''
}
};
composition.add(mockChildObject);
});
composition.on('add', addListener);
composition.on('remove', removeListener);
it('calls add on the provider', function () {
expect(customProvider.add).toHaveBeenCalledWith(domainObject, mockChildObject.identifier);
});
expect(customProvider.on).toHaveBeenCalledWith(
domainObject,
'add',
jasmine.any(Function),
jasmine.any(CompositionCollection)
);
expect(customProvider.on).toHaveBeenCalledWith(
domainObject,
'remove',
jasmine.any(Function),
jasmine.any(CompositionCollection)
);
const add = customProvider.on.calls.all()[0].args[2];
const remove = customProvider.on.calls.all()[1].args[2];
it('calls remove on the provider', function () {
composition.remove(mockChildObject);
expect(customProvider.remove).toHaveBeenCalledWith(
domainObject,
mockChildObject.identifier
);
});
});
});
return composition.load()
.then(function () {
expect(addListener).not.toHaveBeenCalled();
expect(removeListener).not.toHaveBeenCalled();
add({
namespace: 'custom',
key: 'thing'
});
describe('dynamic custom composition', function () {
let customProvider;
let domainObject;
let composition;
return addPromise;
}).then(function () {
expect(addListener).toHaveBeenCalledWith({
identifier: {
namespace: 'custom',
key: 'thing'
}
});
remove(addListener.calls.mostRecent().args[0]);
beforeEach(function () {
// A dynamic provider, loads an empty composition and exposes
// listener functions.
customProvider = jasmine.createSpyObj('dynamicProvider', ['appliesTo', 'load', 'on', 'off']);
return removePromise;
}).then(function () {
expect(removeListener).toHaveBeenCalledWith({
identifier: {
namespace: 'custom',
key: 'thing'
}
});
});
customProvider.appliesTo.and.returnValue('true');
customProvider.load.and.returnValue(Promise.resolve([]));
domainObject = {
identifier: {
namespace: 'test',
key: '1'
},
type: 'custom-object-type'
};
compositionAPI.addProvider(customProvider);
composition = compositionAPI.get(domainObject);
});
it('supports listening and loading', function () {
const addListener = jasmine.createSpy('addListener');
const removeListener = jasmine.createSpy('removeListener');
const addPromise = new Promise(function (resolve) {
addListener.and.callFake(resolve);
});
const removePromise = new Promise(function (resolve) {
removeListener.and.callFake(resolve);
});
composition.on('add', addListener);
composition.on('remove', removeListener);
expect(customProvider.on).toHaveBeenCalledWith(
domainObject,
'add',
jasmine.any(Function),
jasmine.any(CompositionCollection)
);
expect(customProvider.on).toHaveBeenCalledWith(
domainObject,
'remove',
jasmine.any(Function),
jasmine.any(CompositionCollection)
);
const add = customProvider.on.calls.all()[0].args[2];
const remove = customProvider.on.calls.all()[1].args[2];
return composition
.load()
.then(function () {
expect(addListener).not.toHaveBeenCalled();
expect(removeListener).not.toHaveBeenCalled();
add({
namespace: 'custom',
key: 'thing'
});
return addPromise;
})
.then(function () {
expect(addListener).toHaveBeenCalledWith({
identifier: {
namespace: 'custom',
key: 'thing'
}
});
remove(addListener.calls.mostRecent().args[0]);
return removePromise;
})
.then(function () {
expect(removeListener).toHaveBeenCalledWith({
identifier: {
namespace: 'custom',
key: 'thing'
}
});
});
});
});
});

View File

@@ -55,302 +55,276 @@
* ```
*/
export default class CompositionCollection {
domainObject;
#provider;
#publicAPI;
#listeners;
#mutables;
/**
* @constructor
* @param {DomainObject} domainObject the domain object
* whose composition will be contained
* @param {import('./CompositionProvider').default} provider the provider
* to use to retrieve other domain objects
* @param {OpenMCT} publicAPI the composition API, for
* policy checks
*/
constructor(domainObject, provider, publicAPI) {
this.domainObject = domainObject;
/** @type {import('./CompositionProvider').default} */
this.#provider = provider;
/** @type {OpenMCT} */
this.#publicAPI = publicAPI;
/** @type {ListenerMap} */
this.#listeners = {
add: [],
remove: [],
load: [],
reorder: []
};
this.onProviderAdd = this.#onProviderAdd.bind(this);
this.onProviderRemove = this.#onProviderRemove.bind(this);
this.#mutables = {};
domainObject;
#provider;
#publicAPI;
#listeners;
#mutables;
/**
* @constructor
* @param {DomainObject} domainObject the domain object
* whose composition will be contained
* @param {import('./CompositionProvider').default} provider the provider
* to use to retrieve other domain objects
* @param {OpenMCT} publicAPI the composition API, for
* policy checks
*/
constructor(domainObject, provider, publicAPI) {
this.domainObject = domainObject;
/** @type {import('./CompositionProvider').default} */
this.#provider = provider;
/** @type {OpenMCT} */
this.#publicAPI = publicAPI;
/** @type {ListenerMap} */
this.#listeners = {
add: [],
remove: [],
load: [],
reorder: []
};
this.onProviderAdd = this.#onProviderAdd.bind(this);
this.onProviderRemove = this.#onProviderRemove.bind(this);
this.#mutables = {};
if (this.domainObject.isMutable) {
this.returnMutables = true;
let unobserve = this.domainObject.$on('$_destroy', () => {
Object.values(this.#mutables).forEach(mutable => {
this.#publicAPI.objects.destroyMutable(mutable);
});
unobserve();
});
}
}
/**
* Listen for changes to this composition. Supports 'add', 'remove', and
* 'load' events.
*
* @param {string} event event to listen for, either 'add', 'remove' or 'load'.
* @param {(...args: any[]) => void} callback to trigger when event occurs.
* @param {any} [context] to use when invoking callback, optional.
*/
on(event, callback, context) {
if (!this.#listeners[event]) {
throw new Error('Event not supported by composition: ' + event);
}
if (this.#provider.on && this.#provider.off) {
if (event === 'add') {
this.#provider.on(
this.domainObject,
'add',
this.onProviderAdd,
this
);
}
if (event === 'remove') {
this.#provider.on(
this.domainObject,
'remove',
this.onProviderRemove,
this
);
}
if (event === 'reorder') {
this.#provider.on(
this.domainObject,
'reorder',
this.#onProviderReorder,
this
);
}
}
this.#listeners[event].push({
callback: callback,
context: context
if (this.domainObject.isMutable) {
this.returnMutables = true;
let unobserve = this.domainObject.$on('$_destroy', () => {
Object.values(this.#mutables).forEach((mutable) => {
this.#publicAPI.objects.destroyMutable(mutable);
});
unobserve();
});
}
/**
* Remove a listener. Must be called with same exact parameters as
* `off`.
*
* @param {string} event
* @param {(...args: any[]) => void} callback
* @param {any} [context]
*/
off(event, callback, context) {
if (!this.#listeners[event]) {
throw new Error('Event not supported by composition: ' + event);
}
/**
* Listen for changes to this composition. Supports 'add', 'remove', and
* 'load' events.
*
* @param {string} event event to listen for, either 'add', 'remove' or 'load'.
* @param {(...args: any[]) => void} callback to trigger when event occurs.
* @param {any} [context] to use when invoking callback, optional.
*/
on(event, callback, context) {
if (!this.#listeners[event]) {
throw new Error('Event not supported by composition: ' + event);
}
if (this.#provider.on && this.#provider.off) {
if (event === 'add') {
this.#provider.on(this.domainObject, 'add', this.onProviderAdd, this);
}
if (event === 'remove') {
this.#provider.on(this.domainObject, 'remove', this.onProviderRemove, this);
}
if (event === 'reorder') {
this.#provider.on(this.domainObject, 'reorder', this.#onProviderReorder, this);
}
}
this.#listeners[event].push({
callback: callback,
context: context
});
}
/**
* Remove a listener. Must be called with same exact parameters as
* `off`.
*
* @param {string} event
* @param {(...args: any[]) => void} callback
* @param {any} [context]
*/
off(event, callback, context) {
if (!this.#listeners[event]) {
throw new Error('Event not supported by composition: ' + event);
}
const index = this.#listeners[event].findIndex((l) => {
return l.callback === callback && l.context === context;
});
if (index === -1) {
throw new Error('Tried to remove a listener that does not exist');
}
this.#listeners[event].splice(index, 1);
if (this.#listeners[event].length === 0) {
this._destroy();
// Remove provider listener if this is the last callback to
// be removed.
if (this.#provider.off && this.#provider.on) {
if (event === 'add') {
this.#provider.off(this.domainObject, 'add', this.onProviderAdd, this);
} else if (event === 'remove') {
this.#provider.off(this.domainObject, 'remove', this.onProviderRemove, this);
} else if (event === 'reorder') {
this.#provider.off(this.domainObject, 'reorder', this.#onProviderReorder, this);
}
}
}
}
/**
* Add a domain object to this composition.
*
* A call to [load]{@link module:openmct.CompositionCollection#load}
* must have resolved before using this method.
*
* **TODO:** Remove `skipMutate` parameter.
*
* @param {DomainObject} child the domain object to add
* @param {boolean} skipMutate
* **Intended for internal use ONLY.**
* true if the underlying provider should not be updated.
*/
add(child, skipMutate) {
if (!skipMutate) {
if (!this.#publicAPI.composition.checkPolicy(this.domainObject, child)) {
throw `Object of type ${child.type} cannot be added to object of type ${this.domainObject.type}`;
}
const index = this.#listeners[event].findIndex(l => {
return l.callback === callback && l.context === context;
});
this.#provider.add(this.domainObject, child.identifier);
} else {
if (this.returnMutables && this.#publicAPI.objects.supportsMutation(child.identifier)) {
let keyString = this.#publicAPI.objects.makeKeyString(child.identifier);
if (index === -1) {
throw new Error('Tried to remove a listener that does not exist');
child = this.#publicAPI.objects.toMutable(child);
this.#mutables[keyString] = child;
}
this.#emit('add', child);
}
}
/**
* Load the domain objects in this composition.
*
* @param {AbortSignal} abortSignal
* @returns {Promise.<Array.<DomainObject>>} a promise for
* the domain objects in this composition
* @memberof {module:openmct.CompositionCollection#}
* @name load
*/
async load(abortSignal) {
this.#cleanUpMutables();
const children = await this.#provider.load(this.domainObject);
const childObjects = await Promise.all(
children.map((c) => this.#publicAPI.objects.get(c, abortSignal))
);
childObjects.forEach((c) => this.add(c, true));
this.#emit('load');
return childObjects;
}
/**
* Remove a domain object from this composition.
*
* A call to [load]{@link module:openmct.CompositionCollection#load}
* must have resolved before using this method.
*
* **TODO:** Remove `skipMutate` parameter.
*
* @param {DomainObject} child the domain object to remove
* @param {boolean} skipMutate
* **Intended for internal use ONLY.**
* true if the underlying provider should not be updated.
* @name remove
*/
remove(child, skipMutate) {
if (!skipMutate) {
this.#provider.remove(this.domainObject, child.identifier);
} else {
if (this.returnMutables) {
let keyString = this.#publicAPI.objects.makeKeyString(child);
if (this.#mutables[keyString] !== undefined && this.#mutables[keyString].isMutable) {
this.#publicAPI.objects.destroyMutable(this.#mutables[keyString]);
delete this.#mutables[keyString];
}
}
this.#listeners[event].splice(index, 1);
if (this.#listeners[event].length === 0) {
this._destroy();
// Remove provider listener if this is the last callback to
// be removed.
if (this.#provider.off && this.#provider.on) {
if (event === 'add') {
this.#provider.off(
this.domainObject,
'add',
this.onProviderAdd,
this
);
} else if (event === 'remove') {
this.#provider.off(
this.domainObject,
'remove',
this.onProviderRemove,
this
);
} else if (event === 'reorder') {
this.#provider.off(
this.domainObject,
'reorder',
this.#onProviderReorder,
this
);
}
}
}
this.#emit('remove', child);
}
/**
* Add a domain object to this composition.
*
* A call to [load]{@link module:openmct.CompositionCollection#load}
* must have resolved before using this method.
*
* **TODO:** Remove `skipMutate` parameter.
*
* @param {DomainObject} child the domain object to add
* @param {boolean} skipMutate
* **Intended for internal use ONLY.**
* true if the underlying provider should not be updated.
*/
add(child, skipMutate) {
if (!skipMutate) {
if (!this.#publicAPI.composition.checkPolicy(this.domainObject, child)) {
throw `Object of type ${child.type} cannot be added to object of type ${this.domainObject.type}`;
}
this.#provider.add(this.domainObject, child.identifier);
} else {
if (this.returnMutables && this.#publicAPI.objects.supportsMutation(child.identifier)) {
let keyString = this.#publicAPI.objects.makeKeyString(child.identifier);
child = this.#publicAPI.objects.toMutable(child);
this.#mutables[keyString] = child;
}
this.#emit('add', child);
}
}
/**
* Reorder the domain objects in this composition.
*
* A call to [load]{@link module:openmct.CompositionCollection#load}
* must have resolved before using this method.
*
* @param {number} oldIndex
* @param {number} newIndex
* @name remove
*/
reorder(oldIndex, newIndex, _skipMutate) {
this.#provider.reorder(this.domainObject, oldIndex, newIndex);
}
/**
* Destroy mutationListener
*/
_destroy() {
if (this.mutationListener) {
this.mutationListener();
delete this.mutationListener;
}
/**
* Load the domain objects in this composition.
*
* @param {AbortSignal} abortSignal
* @returns {Promise.<Array.<DomainObject>>} a promise for
* the domain objects in this composition
* @memberof {module:openmct.CompositionCollection#}
* @name load
*/
async load(abortSignal) {
this.#cleanUpMutables();
const children = await this.#provider.load(this.domainObject);
const childObjects = await Promise.all(children.map((c) => this.#publicAPI.objects.get(c, abortSignal)));
childObjects.forEach(c => this.add(c, true));
this.#emit('load');
}
/**
* Handle reorder from provider.
* @private
* @param {object} reorderMap
*/
#onProviderReorder(reorderMap) {
this.#emit('reorder', reorderMap);
}
return childObjects;
}
/**
* Remove a domain object from this composition.
*
* A call to [load]{@link module:openmct.CompositionCollection#load}
* must have resolved before using this method.
*
* **TODO:** Remove `skipMutate` parameter.
*
* @param {DomainObject} child the domain object to remove
* @param {boolean} skipMutate
* **Intended for internal use ONLY.**
* true if the underlying provider should not be updated.
* @name remove
*/
remove(child, skipMutate) {
if (!skipMutate) {
this.#provider.remove(this.domainObject, child.identifier);
} else {
if (this.returnMutables) {
let keyString = this.#publicAPI.objects.makeKeyString(child);
if (this.#mutables[keyString] !== undefined && this.#mutables[keyString].isMutable) {
this.#publicAPI.objects.destroyMutable(this.#mutables[keyString]);
delete this.#mutables[keyString];
}
}
/**
* Handle adds from provider.
* @private
* @param {import('../objects/ObjectAPI').Identifier} childId
* @returns {DomainObject}
*/
#onProviderAdd(childId) {
return this.#publicAPI.objects.get(childId).then(
function (child) {
this.add(child, true);
this.#emit('remove', child);
}
}
/**
* Reorder the domain objects in this composition.
*
* A call to [load]{@link module:openmct.CompositionCollection#load}
* must have resolved before using this method.
*
* @param {number} oldIndex
* @param {number} newIndex
* @name remove
*/
reorder(oldIndex, newIndex, _skipMutate) {
this.#provider.reorder(this.domainObject, oldIndex, newIndex);
}
/**
* Destroy mutationListener
*/
_destroy() {
if (this.mutationListener) {
this.mutationListener();
delete this.mutationListener;
}
}
/**
* Handle reorder from provider.
* @private
* @param {object} reorderMap
*/
#onProviderReorder(reorderMap) {
this.#emit('reorder', reorderMap);
}
return child;
}.bind(this)
);
}
/**
* Handle adds from provider.
* @private
* @param {import('../objects/ObjectAPI').Identifier} childId
* @returns {DomainObject}
*/
#onProviderAdd(childId) {
return this.#publicAPI.objects.get(childId).then(function (child) {
this.add(child, true);
/**
* Handle removal from provider.
* @param {DomainObject} child
*/
#onProviderRemove(child) {
this.remove(child, true);
}
return child;
}.bind(this));
}
/**
* Emit events.
*
* @private
* @param {string} event
* @param {...args.<any>} payload
*/
#emit(event, ...payload) {
this.#listeners[event].forEach(function (l) {
if (l.context) {
l.callback.apply(l.context, payload);
} else {
l.callback(...payload);
}
});
}
/**
* Handle removal from provider.
* @param {DomainObject} child
*/
#onProviderRemove(child) {
this.remove(child, true);
}
/**
* Emit events.
*
* @private
* @param {string} event
* @param {...args.<any>} payload
*/
#emit(event, ...payload) {
this.#listeners[event].forEach(function (l) {
if (l.context) {
l.callback.apply(l.context, payload);
} else {
l.callback(...payload);
}
});
}
/**
* Destroy all mutables.
* @private
*/
#cleanUpMutables() {
Object.values(this.#mutables).forEach(mutable => {
this.#publicAPI.objects.destroyMutable(mutable);
});
}
/**
* Destroy all mutables.
* @private
*/
#cleanUpMutables() {
Object.values(this.#mutables).forEach((mutable) => {
this.#publicAPI.objects.destroyMutable(mutable);
});
}
}

View File

@@ -20,241 +20,236 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import _ from 'lodash';
import objectUtils from "../objects/object-utils";
import objectUtils from '../objects/object-utils';
/**
* @typedef {import('../objects/ObjectAPI').DomainObject} DomainObject
*/
* @typedef {import('../objects/ObjectAPI').DomainObject} DomainObject
*/
/**
* @typedef {import('../objects/ObjectAPI').Identifier} Identifier
*/
* @typedef {import('../objects/ObjectAPI').Identifier} Identifier
*/
/**
* @typedef {import('./CompositionAPI').default} CompositionAPI
*/
* @typedef {import('./CompositionAPI').default} CompositionAPI
*/
/**
* @typedef {import('../../../openmct').OpenMCT} OpenMCT
*/
* @typedef {import('../../../openmct').OpenMCT} OpenMCT
*/
/**
* A CompositionProvider provides the underlying implementation of
* composition-related behavior for certain types of domain object.
*
* By default, a composition provider will not support composition
* modification. You can add support for mutation of composition by
* defining `add` and/or `remove` methods.
*
* If the composition of an object can change over time-- perhaps via
* server updates or mutation via the add/remove methods, then one must
* trigger events as necessary.
*
*/
* A CompositionProvider provides the underlying implementation of
* composition-related behavior for certain types of domain object.
*
* By default, a composition provider will not support composition
* modification. You can add support for mutation of composition by
* defining `add` and/or `remove` methods.
*
* If the composition of an object can change over time-- perhaps via
* server updates or mutation via the add/remove methods, then one must
* trigger events as necessary.
*
*/
export default class CompositionProvider {
#publicAPI;
#listeningTo;
#publicAPI;
#listeningTo;
/**
* @param {OpenMCT} publicAPI
* @param {CompositionAPI} compositionAPI
*/
constructor(publicAPI, compositionAPI) {
this.#publicAPI = publicAPI;
this.#listeningTo = {};
/**
* @param {OpenMCT} publicAPI
* @param {CompositionAPI} compositionAPI
*/
constructor(publicAPI, compositionAPI) {
this.#publicAPI = publicAPI;
this.#listeningTo = {};
compositionAPI.addPolicy(this.#cannotContainItself.bind(this));
compositionAPI.addPolicy(this.#supportsComposition.bind(this));
compositionAPI.addPolicy(this.#cannotContainItself.bind(this));
compositionAPI.addPolicy(this.#supportsComposition.bind(this));
}
get listeningTo() {
return this.#listeningTo;
}
get establishTopicListener() {
return this.#establishTopicListener.bind(this);
}
get publicAPI() {
return this.#publicAPI;
}
/**
* Check if this provider should be used to load composition for a
* particular domain object.
* @method appliesTo
* @param {import('../objects/ObjectAPI').DomainObject} domainObject the domain object
* to check
* @returns {boolean} true if this provider can provide composition for a given domain object
*/
appliesTo(domainObject) {
throw new Error('This method must be implemented by a subclass.');
}
/**
* Load any domain objects contained in the composition of this domain
* object.
* @param {DomainObject} domainObject the domain object
* for which to load composition
* @returns {Promise<Identifier[]>} a promise for
* the Identifiers in this composition
* @method load
*/
load(domainObject) {
throw new Error('This method must be implemented by a subclass.');
}
/**
* Attach listeners for changes to the composition of a given domain object.
* Supports `add` and `remove` events.
*
* @param {DomainObject} domainObject to listen to
* @param {string} event the event to bind to, either `add` or `remove`.
* @param {Function} callback callback to invoke when event is triggered.
* @param {any} [context] to use when invoking callback.
*/
on(domainObject, event, callback, context) {
throw new Error('This method must be implemented by a subclass.');
}
/**
* Remove a listener that was previously added for a given domain object.
* event name, callback, and context must be the same as when the listener
* was originally attached.
*
* @param {DomainObject} domainObject to remove listener for
* @param {string} event event to stop listening to: `add` or `remove`.
* @param {Function} callback callback to remove.
* @param {any} context of callback to remove.
*/
off(domainObject, event, callback, context) {
throw new Error('This method must be implemented by a subclass.');
}
/**
* Remove a domain object from another domain object's composition.
*
* This method is optional; if not present, adding to a domain object's
* composition using this provider will be disallowed.
*
* @param {DomainObject} domainObject the domain object
* which should have its composition modified
* @param {Identifier} childId the domain object to remove
* @method remove
*/
remove(domainObject, childId) {
throw new Error('This method must be implemented by a subclass.');
}
/**
* Add a domain object to another domain object's composition.
*
* This method is optional; if not present, adding to a domain object's
* composition using this provider will be disallowed.
*
* @param {DomainObject} parent the domain object
* which should have its composition modified
* @param {Identifier} childId the domain object to add
* @method add
*/
add(parent, childId) {
throw new Error('This method must be implemented by a subclass.');
}
/**
* @param {DomainObject} parent
* @param {Identifier} childId
* @returns {boolean}
*/
includes(parent, childId) {
throw new Error('This method must be implemented by a subclass.');
}
/**
* @param {DomainObject} domainObject
* @param {number} oldIndex
* @param {number} newIndex
* @returns
*/
reorder(domainObject, oldIndex, newIndex) {
throw new Error('This method must be implemented by a subclass.');
}
/**
* Listens on general mutation topic, using injector to fetch to avoid
* circular dependencies.
* @private
*/
#establishTopicListener() {
if (this.topicListener) {
return;
}
get listeningTo() {
return this.#listeningTo;
this.#publicAPI.objects.eventEmitter.on('mutation', this.#onMutation.bind(this));
this.topicListener = () => {
this.#publicAPI.objects.eventEmitter.off('mutation', this.#onMutation.bind(this));
};
}
/**
* @private
* @param {DomainObject} parent
* @param {DomainObject} child
* @returns {boolean}
*/
#cannotContainItself(parent, child) {
return !(
parent.identifier.namespace === child.identifier.namespace &&
parent.identifier.key === child.identifier.key
);
}
/**
* @private
* @param {DomainObject} parent
* @returns {boolean}
*/
#supportsComposition(parent, _child) {
return this.#publicAPI.composition.supportsComposition(parent);
}
/**
* Handles mutation events. If there are active listeners for the mutated
* object, detects changes to composition and triggers necessary events.
*
* @private
* @param {DomainObject} oldDomainObject
*/
#onMutation(newDomainObject, oldDomainObject) {
const id = objectUtils.makeKeyString(oldDomainObject.identifier);
const listeners = this.#listeningTo[id];
if (!listeners) {
return;
}
get establishTopicListener() {
return this.#establishTopicListener.bind(this);
}
const oldComposition = oldDomainObject.composition.map(objectUtils.makeKeyString);
const newComposition = newDomainObject.composition.map(objectUtils.makeKeyString);
get publicAPI() {
return this.#publicAPI;
}
const added = _.difference(newComposition, oldComposition).map(objectUtils.parseKeyString);
const removed = _.difference(oldComposition, newComposition).map(objectUtils.parseKeyString);
/**
* Check if this provider should be used to load composition for a
* particular domain object.
* @method appliesTo
* @param {import('../objects/ObjectAPI').DomainObject} domainObject the domain object
* to check
* @returns {boolean} true if this provider can provide composition for a given domain object
*/
appliesTo(domainObject) {
throw new Error("This method must be implemented by a subclass.");
}
/**
* Load any domain objects contained in the composition of this domain
* object.
* @param {DomainObject} domainObject the domain object
* for which to load composition
* @returns {Promise<Identifier[]>} a promise for
* the Identifiers in this composition
* @method load
*/
load(domainObject) {
throw new Error("This method must be implemented by a subclass.");
}
/**
* Attach listeners for changes to the composition of a given domain object.
* Supports `add` and `remove` events.
*
* @param {DomainObject} domainObject to listen to
* @param {string} event the event to bind to, either `add` or `remove`.
* @param {Function} callback callback to invoke when event is triggered.
* @param {any} [context] to use when invoking callback.
*/
on(domainObject,
event,
callback,
context) {
throw new Error("This method must be implemented by a subclass.");
}
/**
* Remove a listener that was previously added for a given domain object.
* event name, callback, and context must be the same as when the listener
* was originally attached.
*
* @param {DomainObject} domainObject to remove listener for
* @param {string} event event to stop listening to: `add` or `remove`.
* @param {Function} callback callback to remove.
* @param {any} context of callback to remove.
*/
off(domainObject,
event,
callback,
context) {
throw new Error("This method must be implemented by a subclass.");
}
/**
* Remove a domain object from another domain object's composition.
*
* This method is optional; if not present, adding to a domain object's
* composition using this provider will be disallowed.
*
* @param {DomainObject} domainObject the domain object
* which should have its composition modified
* @param {Identifier} childId the domain object to remove
* @method remove
*/
remove(domainObject, childId) {
throw new Error("This method must be implemented by a subclass.");
}
/**
* Add a domain object to another domain object's composition.
*
* This method is optional; if not present, adding to a domain object's
* composition using this provider will be disallowed.
*
* @param {DomainObject} parent the domain object
* which should have its composition modified
* @param {Identifier} childId the domain object to add
* @method add
*/
add(parent, childId) {
throw new Error("This method must be implemented by a subclass.");
}
/**
* @param {DomainObject} parent
* @param {Identifier} childId
* @returns {boolean}
*/
includes(parent, childId) {
throw new Error("This method must be implemented by a subclass.");
}
/**
* @param {DomainObject} domainObject
* @param {number} oldIndex
* @param {number} newIndex
* @returns
*/
reorder(domainObject, oldIndex, newIndex) {
throw new Error("This method must be implemented by a subclass.");
}
/**
* Listens on general mutation topic, using injector to fetch to avoid
* circular dependencies.
* @private
*/
#establishTopicListener() {
if (this.topicListener) {
return;
function notify(value) {
return function (listener) {
if (listener.context) {
listener.callback.call(listener.context, value);
} else {
listener.callback(value);
}
this.#publicAPI.objects.eventEmitter.on('mutation', this.#onMutation.bind(this));
this.topicListener = () => {
this.#publicAPI.objects.eventEmitter.off('mutation', this.#onMutation.bind(this));
};
};
}
/**
* @private
* @param {DomainObject} parent
* @param {DomainObject} child
* @returns {boolean}
*/
#cannotContainItself(parent, child) {
return !(parent.identifier.namespace === child.identifier.namespace
&& parent.identifier.key === child.identifier.key);
}
added.forEach(function (addedChild) {
listeners.add.forEach(notify(addedChild));
});
/**
* @private
* @param {DomainObject} parent
* @returns {boolean}
*/
#supportsComposition(parent, _child) {
return this.#publicAPI.composition.supportsComposition(parent);
}
/**
* Handles mutation events. If there are active listeners for the mutated
* object, detects changes to composition and triggers necessary events.
*
* @private
* @param {DomainObject} oldDomainObject
*/
#onMutation(newDomainObject, oldDomainObject) {
const id = objectUtils.makeKeyString(oldDomainObject.identifier);
const listeners = this.#listeningTo[id];
if (!listeners) {
return;
}
const oldComposition = oldDomainObject.composition.map(objectUtils.makeKeyString);
const newComposition = newDomainObject.composition.map(objectUtils.makeKeyString);
const added = _.difference(newComposition, oldComposition).map(objectUtils.parseKeyString);
const removed = _.difference(oldComposition, newComposition).map(objectUtils.parseKeyString);
function notify(value) {
return function (listener) {
if (listener.context) {
listener.callback.call(listener.context, value);
} else {
listener.callback(value);
}
};
}
added.forEach(function (addedChild) {
listeners.add.forEach(notify(addedChild));
});
removed.forEach(function (removedChild) {
listeners.remove.forEach(notify(removedChild));
});
}
removed.forEach(function (removedChild) {
listeners.remove.forEach(notify(removedChild));
});
}
}

View File

@@ -19,7 +19,7 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import objectUtils from "../objects/object-utils";
import objectUtils from '../objects/object-utils';
import CompositionProvider from './CompositionProvider';
/**
@@ -52,195 +52,195 @@ import CompositionProvider from './CompositionProvider';
* @extends CompositionProvider
*/
export default class DefaultCompositionProvider extends CompositionProvider {
/**
* Check if this provider should be used to load composition for a
* particular domain object.
* @override
* @param {DomainObject} domainObject the domain object
* to check
* @returns {boolean} true if this provider can provide composition for a given domain object
*/
appliesTo(domainObject) {
return Boolean(domainObject.composition);
/**
* Check if this provider should be used to load composition for a
* particular domain object.
* @override
* @param {DomainObject} domainObject the domain object
* to check
* @returns {boolean} true if this provider can provide composition for a given domain object
*/
appliesTo(domainObject) {
return Boolean(domainObject.composition);
}
/**
* Load any domain objects contained in the composition of this domain
* object.
* @override
* @param {DomainObject} domainObject the domain object
* for which to load composition
* @returns {Promise<Identifier[]>} a promise for
* the Identifiers in this composition
*/
load(domainObject) {
return Promise.all(domainObject.composition);
}
/**
* Attach listeners for changes to the composition of a given domain object.
* Supports `add` and `remove` events.
*
* @override
* @param {DomainObject} domainObject to listen to
* @param {string} event the event to bind to, either `add` or `remove`.
* @param {Function} callback callback to invoke when event is triggered.
* @param {any} [context] to use when invoking callback.
*/
on(domainObject, event, callback, context) {
this.establishTopicListener();
/** @type {string} */
const keyString = objectUtils.makeKeyString(domainObject.identifier);
let objectListeners = this.listeningTo[keyString];
if (!objectListeners) {
objectListeners = this.listeningTo[keyString] = {
add: [],
remove: [],
reorder: []
};
}
/**
* Load any domain objects contained in the composition of this domain
* object.
* @override
* @param {DomainObject} domainObject the domain object
* for which to load composition
* @returns {Promise<Identifier[]>} a promise for
* the Identifiers in this composition
*/
load(domainObject) {
return Promise.all(domainObject.composition);
objectListeners[event].push({
callback: callback,
context: context
});
}
/**
* Remove a listener that was previously added for a given domain object.
* event name, callback, and context must be the same as when the listener
* was originally attached.
*
* @override
* @param {DomainObject} domainObject to remove listener for
* @param {string} event event to stop listening to: `add` or `remove`.
* @param {Function} callback callback to remove.
* @param {any} context of callback to remove.
*/
off(domainObject, event, callback, context) {
/** @type {string} */
const keyString = objectUtils.makeKeyString(domainObject.identifier);
const objectListeners = this.listeningTo[keyString];
const index = objectListeners[event].findIndex((l) => {
return l.callback === callback && l.context === context;
});
objectListeners[event].splice(index, 1);
if (
!objectListeners.add.length &&
!objectListeners.remove.length &&
!objectListeners.reorder.length
) {
delete this.listeningTo[keyString];
}
/**
* Attach listeners for changes to the composition of a given domain object.
* Supports `add` and `remove` events.
*
* @override
* @param {DomainObject} domainObject to listen to
* @param {string} event the event to bind to, either `add` or `remove`.
* @param {Function} callback callback to invoke when event is triggered.
* @param {any} [context] to use when invoking callback.
*/
on(domainObject,
event,
callback,
context) {
this.establishTopicListener();
}
/**
* Remove a domain object from another domain object's composition.
*
* This method is optional; if not present, adding to a domain object's
* composition using this provider will be disallowed.
*
* @override
* @param {DomainObject} domainObject the domain object
* which should have its composition modified
* @param {Identifier} childId the domain object to remove
* @method remove
*/
remove(domainObject, childId) {
let composition = domainObject.composition.filter(function (child) {
return !(childId.namespace === child.namespace && childId.key === child.key);
});
/** @type {string} */
const keyString = objectUtils.makeKeyString(domainObject.identifier);
let objectListeners = this.listeningTo[keyString];
this.publicAPI.objects.mutate(domainObject, 'composition', composition);
}
/**
* Add a domain object to another domain object's composition.
*
* This method is optional; if not present, adding to a domain object's
* composition using this provider will be disallowed.
*
* @override
* @param {DomainObject} parent the domain object
* which should have its composition modified
* @param {Identifier} childId the domain object to add
* @method add
*/
add(parent, childId) {
if (!this.includes(parent, childId)) {
const composition = structuredClone(parent.composition);
composition.push(childId);
this.publicAPI.objects.mutate(parent, 'composition', composition);
}
}
if (!objectListeners) {
objectListeners = this.listeningTo[keyString] = {
add: [],
remove: [],
reorder: []
};
}
/**
* @override
* @param {DomainObject} parent
* @param {Identifier} childId
* @returns {boolean}
*/
includes(parent, childId) {
return parent.composition.some((composee) =>
this.publicAPI.objects.areIdsEqual(composee, childId)
);
}
objectListeners[event].push({
callback: callback,
context: context
/**
* @override
* @param {DomainObject} domainObject
* @param {number} oldIndex
* @param {number} newIndex
* @returns
*/
reorder(domainObject, oldIndex, newIndex) {
let newComposition = domainObject.composition.slice();
let removeId = oldIndex > newIndex ? oldIndex + 1 : oldIndex;
let insertPosition = oldIndex < newIndex ? newIndex + 1 : newIndex;
//Insert object in new position
newComposition.splice(insertPosition, 0, domainObject.composition[oldIndex]);
newComposition.splice(removeId, 1);
let reorderPlan = [
{
oldIndex,
newIndex
}
];
if (oldIndex > newIndex) {
for (let i = newIndex; i < oldIndex; i++) {
reorderPlan.push({
oldIndex: i,
newIndex: i + 1
});
}
/**
* Remove a listener that was previously added for a given domain object.
* event name, callback, and context must be the same as when the listener
* was originally attached.
*
* @override
* @param {DomainObject} domainObject to remove listener for
* @param {string} event event to stop listening to: `add` or `remove`.
* @param {Function} callback callback to remove.
* @param {any} context of callback to remove.
*/
off(domainObject,
event,
callback,
context) {
/** @type {string} */
const keyString = objectUtils.makeKeyString(domainObject.identifier);
const objectListeners = this.listeningTo[keyString];
const index = objectListeners[event].findIndex(l => {
return l.callback === callback && l.context === context;
}
} else {
for (let i = oldIndex + 1; i <= newIndex; i++) {
reorderPlan.push({
oldIndex: i,
newIndex: i - 1
});
objectListeners[event].splice(index, 1);
if (!objectListeners.add.length && !objectListeners.remove.length && !objectListeners.reorder.length) {
delete this.listeningTo[keyString];
}
}
/**
* Remove a domain object from another domain object's composition.
*
* This method is optional; if not present, adding to a domain object's
* composition using this provider will be disallowed.
*
* @override
* @param {DomainObject} domainObject the domain object
* which should have its composition modified
* @param {Identifier} childId the domain object to remove
* @method remove
*/
remove(domainObject, childId) {
let composition = domainObject.composition.filter(function (child) {
return !(childId.namespace === child.namespace
&& childId.key === child.key);
});
this.publicAPI.objects.mutate(domainObject, 'composition', composition);
}
/**
* Add a domain object to another domain object's composition.
*
* This method is optional; if not present, adding to a domain object's
* composition using this provider will be disallowed.
*
* @override
* @param {DomainObject} parent the domain object
* which should have its composition modified
* @param {Identifier} childId the domain object to add
* @method add
*/
add(parent, childId) {
if (!this.includes(parent, childId)) {
const composition = structuredClone(parent.composition);
composition.push(childId);
this.publicAPI.objects.mutate(parent, 'composition', composition);
}
}
}
/**
* @override
* @param {DomainObject} parent
* @param {Identifier} childId
* @returns {boolean}
*/
includes(parent, childId) {
return parent.composition.some(composee => this.publicAPI.objects.areIdsEqual(composee, childId));
this.publicAPI.objects.mutate(domainObject, 'composition', newComposition);
/** @type {string} */
let id = objectUtils.makeKeyString(domainObject.identifier);
const listeners = this.listeningTo[id];
if (!listeners) {
return;
}
/**
* @override
* @param {DomainObject} domainObject
* @param {number} oldIndex
* @param {number} newIndex
* @returns
*/
reorder(domainObject, oldIndex, newIndex) {
let newComposition = domainObject.composition.slice();
let removeId = oldIndex > newIndex ? oldIndex + 1 : oldIndex;
let insertPosition = oldIndex < newIndex ? newIndex + 1 : newIndex;
//Insert object in new position
newComposition.splice(insertPosition, 0, domainObject.composition[oldIndex]);
newComposition.splice(removeId, 1);
listeners.reorder.forEach(notify);
let reorderPlan = [{
oldIndex,
newIndex
}];
if (oldIndex > newIndex) {
for (let i = newIndex; i < oldIndex; i++) {
reorderPlan.push({
oldIndex: i,
newIndex: i + 1
});
}
} else {
for (let i = oldIndex + 1; i <= newIndex; i++) {
reorderPlan.push({
oldIndex: i,
newIndex: i - 1
});
}
}
this.publicAPI.objects.mutate(domainObject, 'composition', newComposition);
/** @type {string} */
let id = objectUtils.makeKeyString(domainObject.identifier);
const listeners = this.listeningTo[id];
if (!listeners) {
return;
}
listeners.reorder.forEach(notify);
function notify(listener) {
if (listener.context) {
listener.callback.call(listener.context, reorderPlan);
} else {
listener.callback(reorderPlan);
}
}
function notify(listener) {
if (listener.context) {
listener.callback.call(listener.context, reorderPlan);
} else {
listener.callback(reorderPlan);
}
}
}
}

View File

@@ -21,68 +21,70 @@
*****************************************************************************/
export default class FaultManagementAPI {
/**
* @param {import("openmct").OpenMCT} openmct
*/
constructor(openmct) {
this.openmct = openmct;
/**
* @param {import("openmct").OpenMCT} openmct
*/
constructor(openmct) {
this.openmct = openmct;
}
/**
* @param {*} provider
*/
addProvider(provider) {
this.provider = provider;
}
/**
* @returns {boolean}
*/
supportsActions() {
return (
this.provider?.acknowledgeFault !== undefined && this.provider?.shelveFault !== undefined
);
}
/**
* @param {import("../objects/ObjectAPI").DomainObject} domainObject
* @returns {Promise.<FaultAPIResponse[]>}
*/
request(domainObject) {
if (!this.provider?.supportsRequest(domainObject)) {
return Promise.reject();
}
/**
* @param {*} provider
*/
addProvider(provider) {
this.provider = provider;
return this.provider.request(domainObject);
}
/**
* @param {import("../objects/ObjectAPI").DomainObject} domainObject
* @param {Function} callback
* @returns {Function} unsubscribe
*/
subscribe(domainObject, callback) {
if (!this.provider?.supportsSubscribe(domainObject)) {
return Promise.reject();
}
/**
* @returns {boolean}
*/
supportsActions() {
return this.provider?.acknowledgeFault !== undefined && this.provider?.shelveFault !== undefined;
}
return this.provider.subscribe(domainObject, callback);
}
/**
* @param {import("../objects/ObjectAPI").DomainObject} domainObject
* @returns {Promise.<FaultAPIResponse[]>}
*/
request(domainObject) {
if (!this.provider?.supportsRequest(domainObject)) {
return Promise.reject();
}
/**
* @param {Fault} fault
* @param {*} ackData
*/
acknowledgeFault(fault, ackData) {
return this.provider.acknowledgeFault(fault, ackData);
}
return this.provider.request(domainObject);
}
/**
* @param {import("../objects/ObjectAPI").DomainObject} domainObject
* @param {Function} callback
* @returns {Function} unsubscribe
*/
subscribe(domainObject, callback) {
if (!this.provider?.supportsSubscribe(domainObject)) {
return Promise.reject();
}
return this.provider.subscribe(domainObject, callback);
}
/**
* @param {Fault} fault
* @param {*} ackData
*/
acknowledgeFault(fault, ackData) {
return this.provider.acknowledgeFault(fault, ackData);
}
/**
* @param {Fault} fault
* @param {*} shelveData
* @returns {Promise.<T>}
*/
shelveFault(fault, shelveData) {
return this.provider.shelveFault(fault, shelveData);
}
/**
* @param {Fault} fault
* @param {*} shelveData
* @returns {Promise.<T>}
*/
shelveFault(fault, shelveData) {
return this.provider.shelveFault(fault, shelveData);
}
}
/**

View File

@@ -20,125 +20,123 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import {
createOpenMct,
resetApplicationState
} from '../../utils/testing';
import { createOpenMct, resetApplicationState } from '../../utils/testing';
const faultName = 'super duper fault';
const aFault = {
type: '',
fault: {
acknowledged: true,
currentValueInfo: {
value: 0,
rangeCondition: '',
monitoringResult: ''
},
id: '',
name: faultName,
namespace: '',
seqNum: 0,
severity: '',
shelved: true,
shortDescription: '',
triggerTime: '',
triggerValueInfo: {
value: 0,
rangeCondition: '',
monitoringResult: ''
}
type: '',
fault: {
acknowledged: true,
currentValueInfo: {
value: 0,
rangeCondition: '',
monitoringResult: ''
},
id: '',
name: faultName,
namespace: '',
seqNum: 0,
severity: '',
shelved: true,
shortDescription: '',
triggerTime: '',
triggerValueInfo: {
value: 0,
rangeCondition: '',
monitoringResult: ''
}
}
};
const faultDomainObject = {
name: 'it is not your fault',
type: 'faultManagement',
identifier: {
key: 'nobodies',
namespace: 'fault'
}
name: 'it is not your fault',
type: 'faultManagement',
identifier: {
key: 'nobodies',
namespace: 'fault'
}
};
const aComment = 'THIS is my fault.';
const faultManagementProvider = {
request() {
return Promise.resolve([aFault]);
},
subscribe(domainObject, callback) {
return () => {};
},
supportsRequest(domainObject) {
return domainObject.type === 'faultManagement';
},
supportsSubscribe(domainObject) {
return domainObject.type === 'faultManagement';
},
acknowledgeFault(fault, { comment = '' }) {
return Promise.resolve({
success: true
});
},
shelveFault(fault, shelveData) {
return Promise.resolve({
success: true
});
}
request() {
return Promise.resolve([aFault]);
},
subscribe(domainObject, callback) {
return () => {};
},
supportsRequest(domainObject) {
return domainObject.type === 'faultManagement';
},
supportsSubscribe(domainObject) {
return domainObject.type === 'faultManagement';
},
acknowledgeFault(fault, { comment = '' }) {
return Promise.resolve({
success: true
});
},
shelveFault(fault, shelveData) {
return Promise.resolve({
success: true
});
}
};
describe('The Fault Management API', () => {
let openmct;
let openmct;
beforeEach(() => {
openmct = createOpenMct();
openmct.install(openmct.plugins.FaultManagement());
// openmct.install(openmct.plugins.example.ExampleFaultSource());
openmct.faults.addProvider(faultManagementProvider);
});
beforeEach(() => {
openmct = createOpenMct();
openmct.install(openmct.plugins.FaultManagement());
// openmct.install(openmct.plugins.example.ExampleFaultSource());
openmct.faults.addProvider(faultManagementProvider);
});
afterEach(() => {
return resetApplicationState(openmct);
});
afterEach(() => {
return resetApplicationState(openmct);
});
it('allows you to request a fault', async () => {
spyOn(faultManagementProvider, 'supportsRequest').and.callThrough();
it('allows you to request a fault', async () => {
spyOn(faultManagementProvider, 'supportsRequest').and.callThrough();
let faultResponse = await openmct.faults.request(faultDomainObject);
let faultResponse = await openmct.faults.request(faultDomainObject);
expect(faultManagementProvider.supportsRequest).toHaveBeenCalledWith(faultDomainObject);
expect(faultResponse[0].fault.name).toEqual(faultName);
});
expect(faultManagementProvider.supportsRequest).toHaveBeenCalledWith(faultDomainObject);
expect(faultResponse[0].fault.name).toEqual(faultName);
});
it('allows you to subscribe to a fault', () => {
spyOn(faultManagementProvider, 'subscribe').and.callThrough();
spyOn(faultManagementProvider, 'supportsSubscribe').and.callThrough();
it('allows you to subscribe to a fault', () => {
spyOn(faultManagementProvider, 'subscribe').and.callThrough();
spyOn(faultManagementProvider, 'supportsSubscribe').and.callThrough();
let unsubscribe = openmct.faults.subscribe(faultDomainObject, () => {});
let unsubscribe = openmct.faults.subscribe(faultDomainObject, () => {});
expect(unsubscribe).toEqual(jasmine.any(Function));
expect(faultManagementProvider.supportsSubscribe).toHaveBeenCalledWith(faultDomainObject);
expect(faultManagementProvider.subscribe).toHaveBeenCalledOnceWith(faultDomainObject, jasmine.any(Function));
expect(unsubscribe).toEqual(jasmine.any(Function));
expect(faultManagementProvider.supportsSubscribe).toHaveBeenCalledWith(faultDomainObject);
expect(faultManagementProvider.subscribe).toHaveBeenCalledOnceWith(
faultDomainObject,
jasmine.any(Function)
);
});
});
it('will tell you if the fault management provider supports actions', () => {
expect(openmct.faults.supportsActions()).toBeTrue();
});
it('will tell you if the fault management provider supports actions', () => {
expect(openmct.faults.supportsActions()).toBeTrue();
});
it('will allow you to acknowledge a fault', async () => {
spyOn(faultManagementProvider, 'acknowledgeFault').and.callThrough();
it('will allow you to acknowledge a fault', async () => {
spyOn(faultManagementProvider, 'acknowledgeFault').and.callThrough();
let ackResponse = await openmct.faults.acknowledgeFault(aFault, aComment);
let ackResponse = await openmct.faults.acknowledgeFault(aFault, aComment);
expect(faultManagementProvider.acknowledgeFault).toHaveBeenCalledWith(aFault, aComment);
expect(ackResponse.success).toBeTrue();
});
expect(faultManagementProvider.acknowledgeFault).toHaveBeenCalledWith(aFault, aComment);
expect(ackResponse.success).toBeTrue();
});
it('will allow you to shelve a fault', async () => {
spyOn(faultManagementProvider, 'shelveFault').and.callThrough();
it('will allow you to shelve a fault', async () => {
spyOn(faultManagementProvider, 'shelveFault').and.callThrough();
let shelveResponse = await openmct.faults.shelveFault(aFault, aComment);
expect(faultManagementProvider.shelveFault).toHaveBeenCalledWith(aFault, aComment);
expect(shelveResponse.success).toBeTrue();
});
let shelveResponse = await openmct.faults.shelveFault(aFault, aComment);
expect(faultManagementProvider.shelveFault).toHaveBeenCalledWith(aFault, aComment);
expect(shelveResponse.success).toBeTrue();
});
});

View File

@@ -13,88 +13,88 @@ import ToggleSwitchField from './components/controls/ToggleSwitchField.vue';
import Vue from 'vue';
export const DEFAULT_CONTROLS_MAP = {
'autocomplete': AutoCompleteField,
'checkbox': CheckBoxField,
'composite': ClockDisplayFormatField,
'datetime': Datetime,
'file-input': FileInput,
'locator': Locator,
'numberfield': NumberField,
'select': SelectField,
'textarea': TextAreaField,
'textfield': TextField,
'toggleSwitch': ToggleSwitchField
autocomplete: AutoCompleteField,
checkbox: CheckBoxField,
composite: ClockDisplayFormatField,
datetime: Datetime,
'file-input': FileInput,
locator: Locator,
numberfield: NumberField,
select: SelectField,
textarea: TextAreaField,
textfield: TextField,
toggleSwitch: ToggleSwitchField
};
export default class FormControl {
constructor(openmct) {
this.openmct = openmct;
this.controls = {};
constructor(openmct) {
this.openmct = openmct;
this.controls = {};
this._addDefaultFormControls();
this._addDefaultFormControls();
}
addControl(controlName, controlViewProvider) {
const control = this.controls[controlName];
if (control) {
console.warn(`Error: provided form control '${controlName}', already exists`);
return;
}
addControl(controlName, controlViewProvider) {
const control = this.controls[controlName];
if (control) {
console.warn(`Error: provided form control '${controlName}', already exists`);
this.controls[controlName] = controlViewProvider;
}
return;
}
this.controls[controlName] = controlViewProvider;
getControl(controlName) {
const control = this.controls[controlName];
if (!control) {
console.error(`Error: form control '${controlName}', does not exist`);
}
getControl(controlName) {
const control = this.controls[controlName];
if (!control) {
console.error(`Error: form control '${controlName}', does not exist`);
}
return control;
}
return control;
}
/**
* @private
*/
_addDefaultFormControls() {
Object.keys(DEFAULT_CONTROLS_MAP).forEach((control) => {
const controlViewProvider = this._getControlViewProvider(control);
this.addControl(control, controlViewProvider);
});
}
/**
* @private
*/
_addDefaultFormControls() {
Object.keys(DEFAULT_CONTROLS_MAP).forEach(control => {
const controlViewProvider = this._getControlViewProvider(control);
this.addControl(control, controlViewProvider);
/**
* @private
*/
_getControlViewProvider(control) {
const self = this;
let rowComponent;
return {
show(element, model, onChange) {
rowComponent = new Vue({
el: element,
components: {
FormControlComponent: DEFAULT_CONTROLS_MAP[control]
},
provide: {
openmct: self.openmct
},
data() {
return {
model,
onChange
};
},
template: `<FormControlComponent :model="model" @onChange="onChange"></FormControlComponent>`
});
}
/**
* @private
*/
_getControlViewProvider(control) {
const self = this;
let rowComponent;
return {
show(element, model, onChange) {
rowComponent = new Vue({
el: element,
components: {
FormControlComponent: DEFAULT_CONTROLS_MAP[control]
},
provide: {
openmct: self.openmct
},
data() {
return {
model,
onChange
};
},
template: `<FormControlComponent :model="model" @onChange="onChange"></FormControlComponent>`
});
return rowComponent;
},
destroy() {
rowComponent.$destroy();
}
};
}
return rowComponent;
},
destroy() {
rowComponent.$destroy();
}
};
}
}

View File

@@ -27,187 +27,183 @@ import Vue from 'vue';
import _ from 'lodash';
export default class FormsAPI {
constructor(openmct) {
this.openmct = openmct;
this.formController = new FormController(openmct);
constructor(openmct) {
this.openmct = openmct;
this.formController = new FormController(openmct);
}
/**
* Control View Provider definition for a form control
* @typedef ControlViewProvider
* @property {function} show a function renders view in place of given element
* This function accepts element, model and onChange function
* element - html element (place holder) to render a row view
* model - row data for rendering name, value etc for given row type
* onChange - an onChange event callback funtion to keep track of any change in value
* @property {function} destroy a callback function when a vue component gets destroyed
*/
/**
* Create a new form control definition with a formControlViewProvider
* this formControlViewProvider is used inside form overlay to show/render a form row
*
* @public
* @param {String} controlName a form structure, array of section
* @param {ControlViewProvider} controlViewProvider
*/
addNewFormControl(controlName, controlViewProvider) {
this.formController.addControl(controlName, controlViewProvider);
}
/**
* Get a ControlViewProvider for a given/stored form controlName
*
* @public
* @param {String} controlName a form structure, array of section
* @return {ControlViewProvider}
*/
getFormControl(controlName) {
return this.formController.getControl(controlName);
}
/**
* Section definition for formStructure
* @typedef Section
* @property {object} name Name of the section to display on Form
* @property {string} cssClass class name for styling section
* @property {array<Row>} rows collection of rows inside a section
*/
/**
* Row definition for Section
* @typedef Row
* @property {string} control represents type of row to render
* eg:autocomplete,composite,datetime,file-input,locator,numberfield,select,textarea,textfield
* @property {string} cssClass class name for styling this row
* @property {module:openmct.DomainObject} domainObject object to be used by row
* @property {string} key id for this row
* @property {string} name Name of the row to display on Form
* @property {module:openmct.DomainObject} parent parent object to be used by row
* @property {boolean} required is this row mandatory
* @property {function} validate a function to validate this row on any changes
*/
/**
* Show form inside an Overlay dialog with given form structure
* @public
* @param {Array<Section>} formStructure a form structure, array of section
* @param {Object} options
* @property {function} onChange a callback function when any changes detected
*/
showForm(formStructure, { onChange } = {}) {
let overlay;
const self = this;
const overlayEl = document.createElement('div');
overlayEl.classList.add('u-contents');
overlay = self.openmct.overlays.overlay({
element: overlayEl,
size: 'dialog'
});
let formSave;
let formCancel;
const promise = new Promise((resolve, reject) => {
formSave = resolve;
formCancel = reject;
});
this.showCustomForm(formStructure, {
element: overlayEl,
onChange
})
.then((response) => {
overlay.dismiss();
formSave(response);
})
.catch((response) => {
overlay.dismiss();
formCancel(response);
});
return promise;
}
/**
* Show form as a child of the element provided with given form structure
*
* @public
* @param {Array<Section>} formStructure a form structure, array of section
* @param {Object} options
* @property {HTMLElement} element Parent Element to render a Form
* @property {function} onChange a callback function when any changes detected
*/
showCustomForm(formStructure, { element, onChange } = {}) {
if (element === undefined) {
throw Error('Required element parameter not provided');
}
/**
* Control View Provider definition for a form control
* @typedef ControlViewProvider
* @property {function} show a function renders view in place of given element
* This function accepts element, model and onChange function
* element - html element (place holder) to render a row view
* model - row data for rendering name, value etc for given row type
* onChange - an onChange event callback funtion to keep track of any change in value
* @property {function} destroy a callback function when a vue component gets destroyed
*/
const self = this;
/**
* Create a new form control definition with a formControlViewProvider
* this formControlViewProvider is used inside form overlay to show/render a form row
*
* @public
* @param {String} controlName a form structure, array of section
* @param {ControlViewProvider} controlViewProvider
*/
addNewFormControl(controlName, controlViewProvider) {
this.formController.addControl(controlName, controlViewProvider);
}
const changes = {};
let formSave;
let formCancel;
/**
* Get a ControlViewProvider for a given/stored form controlName
*
* @public
* @param {String} controlName a form structure, array of section
* @return {ControlViewProvider}
*/
getFormControl(controlName) {
return this.formController.getControl(controlName);
}
const promise = new Promise((resolve, reject) => {
formSave = onFormAction(resolve);
formCancel = onFormAction(reject);
});
/**
* Section definition for formStructure
* @typedef Section
* @property {object} name Name of the section to display on Form
* @property {string} cssClass class name for styling section
* @property {array<Row>} rows collection of rows inside a section
*/
const vm = new Vue({
components: { FormProperties },
provide: {
openmct: self.openmct
},
data() {
return {
formStructure,
onChange: onFormPropertyChange,
onCancel: formCancel,
onSave: formSave
};
},
template:
'<FormProperties :model="formStructure" @onChange="onChange" @onCancel="onCancel" @onSave="onSave"></FormProperties>'
}).$mount();
/**
* Row definition for Section
* @typedef Row
* @property {string} control represents type of row to render
* eg:autocomplete,composite,datetime,file-input,locator,numberfield,select,textarea,textfield
* @property {string} cssClass class name for styling this row
* @property {module:openmct.DomainObject} domainObject object to be used by row
* @property {string} key id for this row
* @property {string} name Name of the row to display on Form
* @property {module:openmct.DomainObject} parent parent object to be used by row
* @property {boolean} required is this row mandatory
* @property {function} validate a function to validate this row on any changes
*/
const formElement = vm.$el;
element.append(formElement);
/**
* Show form inside an Overlay dialog with given form structure
* @public
* @param {Array<Section>} formStructure a form structure, array of section
* @param {Object} options
* @property {function} onChange a callback function when any changes detected
*/
showForm(formStructure, {
onChange
} = {}) {
let overlay;
function onFormPropertyChange(data) {
if (onChange) {
onChange(data);
}
const self = this;
if (data.model) {
const property = data.model.property;
let key = data.model.key;
const overlayEl = document.createElement('div');
overlayEl.classList.add('u-contents');
overlay = self.openmct.overlays.overlay({
element: overlayEl,
size: 'dialog'
});
let formSave;
let formCancel;
const promise = new Promise((resolve, reject) => {
formSave = resolve;
formCancel = reject;
});
this.showCustomForm(formStructure, {
element: overlayEl,
onChange
})
.then((response) => {
overlay.dismiss();
formSave(response);
})
.catch((response) => {
overlay.dismiss();
formCancel(response);
});
return promise;
}
/**
* Show form as a child of the element provided with given form structure
*
* @public
* @param {Array<Section>} formStructure a form structure, array of section
* @param {Object} options
* @property {HTMLElement} element Parent Element to render a Form
* @property {function} onChange a callback function when any changes detected
*/
showCustomForm(formStructure, {
element,
onChange
} = {}) {
if (element === undefined) {
throw Error('Required element parameter not provided');
if (property && property.length) {
key = property.join('.');
}
const self = this;
const changes = {};
let formSave;
let formCancel;
const promise = new Promise((resolve, reject) => {
formSave = onFormAction(resolve);
formCancel = onFormAction(reject);
});
const vm = new Vue({
components: { FormProperties },
provide: {
openmct: self.openmct
},
data() {
return {
formStructure,
onChange: onFormPropertyChange,
onCancel: formCancel,
onSave: formSave
};
},
template: '<FormProperties :model="formStructure" @onChange="onChange" @onCancel="onCancel" @onSave="onSave"></FormProperties>'
}).$mount();
const formElement = vm.$el;
element.append(formElement);
function onFormPropertyChange(data) {
if (onChange) {
onChange(data);
}
if (data.model) {
const property = data.model.property;
let key = data.model.key;
if (property && property.length) {
key = property.join('.');
}
_.set(changes, key, data.value);
}
}
function onFormAction(callback) {
return () => {
formElement.remove();
vm.$destroy();
if (callback) {
callback(changes);
}
};
}
return promise;
_.set(changes, key, data.value);
}
}
function onFormAction(callback) {
return () => {
formElement.remove();
vm.$destroy();
if (callback) {
callback(changes);
}
};
}
return promise;
}
}

View File

@@ -22,136 +22,136 @@
import { createOpenMct, resetApplicationState } from '../../utils/testing';
describe('The Forms API', () => {
let openmct;
let element;
let openmct;
let element;
beforeEach((done) => {
element = document.createElement('div');
element.style.display = 'block';
element.style.width = '1920px';
element.style.height = '1080px';
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 = createOpenMct();
openmct.on('start', done);
openmct.startHeadless(element);
openmct.startHeadless(element);
});
afterEach(() => {
return resetApplicationState(openmct);
});
it('openmct supports form API', () => {
expect(openmct.forms).not.toBe(null);
});
describe('check default form controls exists', () => {
it('autocomplete', () => {
const control = openmct.forms.getFormControl('autocomplete');
expect(control).not.toBe(null);
});
afterEach(() => {
return resetApplicationState(openmct);
it('clock', () => {
const control = openmct.forms.getFormControl('composite');
expect(control).not.toBe(null);
});
it('openmct supports form API', () => {
expect(openmct.forms).not.toBe(null);
it('datetime', () => {
const control = openmct.forms.getFormControl('datetime');
expect(control).not.toBe(null);
});
describe('check default form controls exists', () => {
it('autocomplete', () => {
const control = openmct.forms.getFormControl('autocomplete');
expect(control).not.toBe(null);
});
it('clock', () => {
const control = openmct.forms.getFormControl('composite');
expect(control).not.toBe(null);
});
it('datetime', () => {
const control = openmct.forms.getFormControl('datetime');
expect(control).not.toBe(null);
});
it('file-input', () => {
const control = openmct.forms.getFormControl('file-input');
expect(control).not.toBe(null);
});
it('locator', () => {
const control = openmct.forms.getFormControl('locator');
expect(control).not.toBe(null);
});
it('numberfield', () => {
const control = openmct.forms.getFormControl('numberfield');
expect(control).not.toBe(null);
});
it('select', () => {
const control = openmct.forms.getFormControl('select');
expect(control).not.toBe(null);
});
it('textarea', () => {
const control = openmct.forms.getFormControl('textarea');
expect(control).not.toBe(null);
});
it('textfield', () => {
const control = openmct.forms.getFormControl('textfield');
expect(control).not.toBe(null);
});
it('file-input', () => {
const control = openmct.forms.getFormControl('file-input');
expect(control).not.toBe(null);
});
it('supports user defined form controls', () => {
const newFormControl = {
show: () => {
console.log('show new control');
},
destroy: () => {
console.log('destroy');
}
};
openmct.forms.addNewFormControl('newFormControl', newFormControl);
const control = openmct.forms.getFormControl('newFormControl');
expect(control).not.toBe(null);
expect(control.show).not.toBe(null);
expect(control.destroy).not.toBe(null);
it('locator', () => {
const control = openmct.forms.getFormControl('locator');
expect(control).not.toBe(null);
});
describe('show form on UI', () => {
let formStructure;
beforeEach(() => {
formStructure = {
title: 'Test Show Form',
sections: [
{
rows: [
{
key: 'name',
control: 'textfield',
name: 'Title',
pattern: '\\S+',
required: false,
cssClass: 'l-input-lg',
value: 'Test Name'
}
]
}
]
};
});
it('when container element is provided', (done) => {
openmct.forms.showCustomForm(formStructure, { element }).catch(() => {
done();
});
const titleElement = element.querySelector('.c-overlay__dialog-title');
expect(titleElement.textContent).toBe(formStructure.title);
element.querySelector('.js-cancel-button').click();
});
it('when container element is not provided', (done) => {
openmct.forms.showForm(formStructure).catch(() => {
done();
});
const titleElement = document.querySelector('.c-overlay__dialog-title');
const title = titleElement.textContent;
expect(title).toBe(formStructure.title);
document.querySelector('.js-cancel-button').click();
});
it('numberfield', () => {
const control = openmct.forms.getFormControl('numberfield');
expect(control).not.toBe(null);
});
it('select', () => {
const control = openmct.forms.getFormControl('select');
expect(control).not.toBe(null);
});
it('textarea', () => {
const control = openmct.forms.getFormControl('textarea');
expect(control).not.toBe(null);
});
it('textfield', () => {
const control = openmct.forms.getFormControl('textfield');
expect(control).not.toBe(null);
});
});
it('supports user defined form controls', () => {
const newFormControl = {
show: () => {
console.log('show new control');
},
destroy: () => {
console.log('destroy');
}
};
openmct.forms.addNewFormControl('newFormControl', newFormControl);
const control = openmct.forms.getFormControl('newFormControl');
expect(control).not.toBe(null);
expect(control.show).not.toBe(null);
expect(control.destroy).not.toBe(null);
});
describe('show form on UI', () => {
let formStructure;
beforeEach(() => {
formStructure = {
title: 'Test Show Form',
sections: [
{
rows: [
{
key: 'name',
control: 'textfield',
name: 'Title',
pattern: '\\S+',
required: false,
cssClass: 'l-input-lg',
value: 'Test Name'
}
]
}
]
};
});
it('when container element is provided', (done) => {
openmct.forms.showCustomForm(formStructure, { element }).catch(() => {
done();
});
const titleElement = element.querySelector('.c-overlay__dialog-title');
expect(titleElement.textContent).toBe(formStructure.title);
element.querySelector('.js-cancel-button').click();
});
it('when container element is not provided', (done) => {
openmct.forms.showForm(formStructure).catch(() => {
done();
});
const titleElement = document.querySelector('.c-overlay__dialog-title');
const title = titleElement.textContent;
expect(title).toBe(formStructure.title);
document.querySelector('.js-cancel-button').click();
});
});
});

View File

@@ -21,155 +21,136 @@
-->
<template>
<div class="c-form js-form">
<div class="c-form js-form">
<div class="c-overlay__top-bar c-form__top-bar">
<div class="c-overlay__dialog-title js-form-title">{{ model.title }}</div>
<div
v-if="hasRequiredFields"
class="c-overlay__dialog-hint hint"
>All fields marked <span class="req icon-asterisk"></span> are required.</div>
<div class="c-overlay__dialog-title js-form-title">{{ model.title }}</div>
<div v-if="hasRequiredFields" class="c-overlay__dialog-hint hint">
All fields marked <span class="req icon-asterisk"></span> are required.
</div>
</div>
<form
name="mctForm"
class="c-form__contents"
autocomplete="off"
@submit.prevent
>
<div
v-for="section in formSections"
:key="section.id"
class="c-form__section"
:class="section.cssClass"
>
<h2
v-if="section.name"
class="c-form__section-header"
>
{{ section.name }}
</h2>
<FormRow
v-for="(row, index) in section.rows"
:key="row.id"
:css-class="row.cssClass"
:first="index < 1"
:row="row"
@onChange="onChange"
/>
</div>
<form name="mctForm" class="c-form__contents" autocomplete="off" @submit.prevent>
<div
v-for="section in formSections"
:key="section.id"
class="c-form__section"
:class="section.cssClass"
>
<h2 v-if="section.name" class="c-form__section-header">
{{ section.name }}
</h2>
<FormRow
v-for="(row, index) in section.rows"
:key="row.id"
:css-class="row.cssClass"
:first="index < 1"
:row="row"
@onChange="onChange"
/>
</div>
</form>
<div class="mct-form__controls c-overlay__button-bar c-form__bottom-bar">
<button
tabindex="0"
:disabled="isInvalid"
class="c-button c-button--major"
aria-label="Save"
@click="onSave"
>
{{ submitLabel }}
</button>
<button
v-if="!shouldHideCancelButton"
tabindex="0"
class="c-button js-cancel-button"
aria-label="Cancel"
@click="onCancel"
>
{{ cancelLabel }}
</button>
<button
tabindex="0"
:disabled="isInvalid"
class="c-button c-button--major"
aria-label="Save"
@click="onSave"
>
{{ submitLabel }}
</button>
<button
v-if="!shouldHideCancelButton"
tabindex="0"
class="c-button js-cancel-button"
aria-label="Cancel"
@click="onCancel"
>
{{ cancelLabel }}
</button>
</div>
</div>
</div>
</template>
<script>
import FormRow from "@/api/forms/components/FormRow.vue";
import FormRow from '@/api/forms/components/FormRow.vue';
import { v4 as uuid } from 'uuid';
export default {
components: {
FormRow
components: {
FormRow
},
inject: ['openmct'],
props: {
model: {
type: Object,
required: true
},
inject: ['openmct'],
props: {
model: {
type: Object,
required: true
},
value: {
type: Object,
default() {
return {};
}
}
},
data() {
return {
invalidProperties: {},
formSections: []
};
},
computed: {
hasRequiredFields() {
return this.model.sections.some(section =>
section.rows.some(row => row.required));
},
isInvalid() {
return Object.entries(this.invalidProperties)
.some(([key, value]) => {
return value;
});
},
submitLabel() {
if (
this.model.buttons
&& this.model.buttons.submit
&& this.model.buttons.submit.label
) {
return this.model.buttons.submit.label;
}
return 'OK';
},
cancelLabel() {
if (
this.model.buttons
&& this.model.buttons.cancel
&& this.model.buttons.cancel.label
) {
return this.model.buttons.submit.label;
}
return 'Cancel';
},
shouldHideCancelButton() {
return this.model.buttons?.cancel?.hide === true;
}
},
mounted() {
this.formSections = this.model.sections.map(section => {
section.id = uuid();
section.rows = section.rows.map(row => {
row.id = uuid();
return row;
});
return section;
});
},
methods: {
onChange(data) {
this.$set(this.invalidProperties, data.model.key, data.invalid);
this.$emit('onChange', data);
},
onCancel() {
this.$emit('onCancel');
},
onSave() {
this.$emit('onSave');
}
value: {
type: Object,
default() {
return {};
}
}
},
data() {
return {
invalidProperties: {},
formSections: []
};
},
computed: {
hasRequiredFields() {
return this.model.sections.some((section) => section.rows.some((row) => row.required));
},
isInvalid() {
return Object.entries(this.invalidProperties).some(([key, value]) => {
return value;
});
},
submitLabel() {
if (this.model.buttons && this.model.buttons.submit && this.model.buttons.submit.label) {
return this.model.buttons.submit.label;
}
return 'OK';
},
cancelLabel() {
if (this.model.buttons && this.model.buttons.cancel && this.model.buttons.cancel.label) {
return this.model.buttons.submit.label;
}
return 'Cancel';
},
shouldHideCancelButton() {
return this.model.buttons?.cancel?.hide === true;
}
},
mounted() {
this.formSections = this.model.sections.map((section) => {
section.id = uuid();
section.rows = section.rows.map((row) => {
row.id = uuid();
return row;
});
return section;
});
},
methods: {
onChange(data) {
this.$set(this.invalidProperties, data.model.key, data.invalid);
this.$emit('onChange', data);
},
onCancel() {
this.$emit('onCancel');
},
onSave() {
this.$emit('onSave');
}
}
};
</script>

View File

@@ -21,133 +21,113 @@
-->
<template>
<div
class="form-row c-form__row"
:class="[
{ 'first': first },
cssClass
]"
@onChange="onChange"
>
<div
class="c-form-row__label"
:title="row.description"
>
{{ row.name }}
<div class="form-row c-form__row" :class="[{ first: first }, cssClass]" @onChange="onChange">
<div class="c-form-row__label" :title="row.description">
{{ row.name }}
</div>
<div
class="c-form-row__state-indicator"
:class="reqClass"
>
<div class="c-form-row__state-indicator" :class="reqClass"></div>
<div v-if="row.control" class="c-form-row__controls">
<div ref="rowElement"></div>
</div>
<div
v-if="row.control"
class="c-form-row__controls"
>
<div ref="rowElement"></div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'FormRow',
components: {
name: 'FormRow',
components: {},
inject: ['openmct'],
props: {
cssClass: {
type: String,
default: '',
required: true
},
inject: ['openmct'],
props: {
cssClass: {
type: String,
default: '',
required: true
},
first: {
type: Boolean,
default: false,
required: true
},
row: {
type: Object,
required: true
}
first: {
type: Boolean,
default: false,
required: true
},
data() {
return {
formControl: this.openmct.forms.getFormControl(this.row.control),
valid: undefined,
visited: false
};
},
computed: {
reqClass() {
let reqClass = 'req';
if (!this.row.required) {
return;
}
if (this.visited && this.valid !== undefined) {
if (this.valid === true) {
reqClass = 'valid';
} else {
reqClass = 'invalid';
}
}
return reqClass;
}
},
mounted() {
if (this.row.required) {
const data = {
model: this.row,
value: this.row.value
};
this.onChange(data, false);
}
this.formControl.show(this.$refs.rowElement, this.row, this.onChange);
},
destroyed() {
const destroy = this.formControl.destroy;
if (destroy) {
destroy();
}
},
methods: {
onChange(data, visited = true) {
this.visited = visited;
this.valid = this.validateRow(data);
data.invalid = !this.valid;
this.$emit('onChange', data);
},
validateRow(data) {
let valid = true;
if (this.row.required) {
valid = data.value !== undefined
&& data.value !== null
&& data.value !== '';
}
if (this.row.required && !valid) {
return false;
}
const pattern = data.model.pattern;
if (valid && pattern) {
const regex = new RegExp(pattern);
valid = regex.test(data.value);
}
const validate = data.model.validate;
if (valid && validate) {
valid = validate(data);
}
return Boolean(valid);
}
row: {
type: Object,
required: true
}
},
data() {
return {
formControl: this.openmct.forms.getFormControl(this.row.control),
valid: undefined,
visited: false
};
},
computed: {
reqClass() {
let reqClass = 'req';
if (!this.row.required) {
return;
}
if (this.visited && this.valid !== undefined) {
if (this.valid === true) {
reqClass = 'valid';
} else {
reqClass = 'invalid';
}
}
return reqClass;
}
},
mounted() {
if (this.row.required) {
const data = {
model: this.row,
value: this.row.value
};
this.onChange(data, false);
}
this.formControl.show(this.$refs.rowElement, this.row, this.onChange);
},
destroyed() {
const destroy = this.formControl.destroy;
if (destroy) {
destroy();
}
},
methods: {
onChange(data, visited = true) {
this.visited = visited;
this.valid = this.validateRow(data);
data.invalid = !this.valid;
this.$emit('onChange', data);
},
validateRow(data) {
let valid = true;
if (this.row.required) {
valid = data.value !== undefined && data.value !== null && data.value !== '';
}
if (this.row.required && !valid) {
return false;
}
const pattern = data.model.pattern;
if (valid && pattern) {
const regex = new RegExp(pattern);
valid = regex.test(data.value);
}
const validate = data.model.validate;
if (valid && validate) {
valid = validate(data);
}
return Boolean(valid);
}
}
};
</script>

View File

@@ -20,252 +20,243 @@
at runtime from the About dialog for additional information.
-->
<template>
<div
ref="autoCompleteForm"
class="form-control c-input--autocomplete js-autocomplete"
>
<div ref="autoCompleteForm" class="form-control c-input--autocomplete js-autocomplete">
<div class="c-input--autocomplete__wrapper">
<input
ref="autoCompleteInput"
v-model="field"
class="c-input--autocomplete__input js-autocomplete__input"
type="text"
:placeholder="placeHolderText"
@click="inputClicked()"
@keydown="keyDown($event)"
/>
<div
class="icon-arrow-down c-icon-button c-input--autocomplete__afford-arrow js-autocomplete__afford-arrow"
@click="arrowClicked()"
></div>
</div>
<div
class="c-input--autocomplete__wrapper"
v-if="!hideOptions && filteredOptions.length > 0"
class="c-menu c-input--autocomplete__options js-autocomplete-options"
aria-label="Autocomplete Options"
@blur="hideOptions = true"
>
<input
ref="autoCompleteInput"
v-model="field"
class="c-input--autocomplete__input js-autocomplete__input"
type="text"
:placeholder="placeHolderText"
@click="inputClicked()"
@keydown="keyDown($event)"
<ul>
<li
v-for="opt in filteredOptions"
:key="opt.optionId"
:class="[{ optionPreSelected: optionIndex === opt.optionId }, itemCssClass]"
:style="itemStyle(opt)"
@click="fillInputWithString(opt.name)"
@mouseover="optionMouseover(opt.optionId)"
>
<div
class="icon-arrow-down c-icon-button c-input--autocomplete__afford-arrow js-autocomplete__afford-arrow"
@click="arrowClicked()"
></div>
{{ opt.name }}
</li>
</ul>
</div>
<div
v-if="!hideOptions && filteredOptions.length > 0"
class="c-menu c-input--autocomplete__options js-autocomplete-options"
aria-label="Autocomplete Options"
@blur="hideOptions = true"
>
<ul>
<li
v-for="opt in filteredOptions"
:key="opt.optionId"
:class="[
{'optionPreSelected': optionIndex === opt.optionId},
itemCssClass
]"
:style="itemStyle(opt)"
@click="fillInputWithString(opt.name)"
@mouseover="optionMouseover(opt.optionId)"
>
{{ opt.name }}
</li>
</ul>
</div>
</div>
</div>
</template>
<script>
const key = {
down: 40,
up: 38,
enter: 13
down: 40,
up: 38,
enter: 13
};
export default {
props: {
model: {
type: Object,
required: true,
default() {
return {};
}
},
placeHolderText: {
type: String,
default() {
return "";
}
},
itemCssClass: {
type: String,
required: false,
default() {
return "";
}
}
props: {
model: {
type: Object,
required: true,
default() {
return {};
}
},
data() {
return {
hideOptions: true,
showFilteredOptions: false,
optionIndex: 0,
field: this.model.value
};
placeHolderText: {
type: String,
default() {
return '';
}
},
computed: {
filteredOptions() {
const fullOptions = this.options || [];
if (this.showFilteredOptions) {
const optionsFiltered = fullOptions
.filter(option => {
if (option.name && this.field) {
return option.name.toLowerCase().indexOf(this.field.toLowerCase()) >= 0;
}
return false;
}).map((option, index) => {
return {
optionId: index,
name: option.name,
color: option.color
};
});
return optionsFiltered;
}
const optionsFiltered = fullOptions.map((option, index) => {
return {
optionId: index,
name: option.name,
color: option.color
};
});
return optionsFiltered;
}
},
watch: {
field(newValue, oldValue) {
if (newValue !== oldValue) {
const data = {
model: this.model,
value: newValue
};
this.$emit('onChange', data);
}
},
hideOptions(newValue) {
if (!newValue) {
// adding a event listener when the hideOptions is false (dropdown is visible)
// handleoutsideclick can collapse the dropdown when clicked outside autocomplete
document.body.addEventListener('click', this.handleOutsideClick);
} else {
//removing event listener when hideOptions become true (dropdown is collapsed)
document.body.removeEventListener('click', this.handleOutsideClick);
}
}
},
mounted() {
this.autocompleteInputAndArrow = this.$refs.autoCompleteForm;
this.autocompleteInputElement = this.$refs.autoCompleteInput;
if (this.model.options && this.model.options.length && !this.model.options[0].name) {
// If options is only an array of string.
this.options = this.model.options.map((option) => {
return {
name: option
};
});
} else {
this.options = this.model.options;
}
},
destroyed() {
document.body.removeEventListener('click', this.handleOutsideClick);
},
methods: {
decrementOptionIndex() {
if (this.optionIndex === 0) {
this.optionIndex = this.filteredOptions.length;
}
this.optionIndex--;
this.scrollIntoView();
},
incrementOptionIndex() {
if (this.optionIndex === this.filteredOptions.length - 1) {
this.optionIndex = -1;
}
this.optionIndex++;
this.scrollIntoView();
},
fillInputWithString(string) {
this.hideOptions = true;
this.field = string;
},
showOptions() {
this.hideOptions = false;
this.optionIndex = 0;
},
keyDown($event) {
this.showFilteredOptions = true;
if (this.filteredOptions) {
let keyCode = $event.keyCode;
switch (keyCode) {
case key.down:
this.incrementOptionIndex();
break;
case key.up:
$event.preventDefault(); // Prevents cursor jumping back and forth
this.decrementOptionIndex();
break;
case key.enter:
if (this.filteredOptions[this.optionIndex]) {
this.fillInputWithString(this.filteredOptions[this.optionIndex].name);
}
}
}
},
inputClicked() {
this.autocompleteInputElement.select();
this.showOptions();
},
arrowClicked() {
// if the user clicked the arrow, we want
// to show them all the options
this.showFilteredOptions = false;
this.autocompleteInputElement.select();
if (!this.hideOptions && this.filteredOptions.length > 0) {
this.hideOptions = true;
} else {
this.showOptions();
}
},
handleOutsideClick(event) {
// if click event is detected outside autocomplete (both input & arrow) while the
// dropdown is visible, this will collapse the dropdown.
const clickedInsideAutocomplete = this.autocompleteInputAndArrow.contains(event.target);
if (!clickedInsideAutocomplete && !this.hideOptions) {
this.hideOptions = true;
}
},
optionMouseover(optionId) {
this.optionIndex = optionId;
},
scrollIntoView() {
setTimeout(() => {
const element = this.$el.querySelector('.optionPreSelected');
if (element) {
element.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'nearest'
});
}
});
},
itemStyle(option) {
if (option.color) {
return { '--optionIconColor': option.color };
}
}
itemCssClass: {
type: String,
required: false,
default() {
return '';
}
}
},
data() {
return {
hideOptions: true,
showFilteredOptions: false,
optionIndex: 0,
field: this.model.value
};
},
computed: {
filteredOptions() {
const fullOptions = this.options || [];
if (this.showFilteredOptions) {
const optionsFiltered = fullOptions
.filter((option) => {
if (option.name && this.field) {
return option.name.toLowerCase().indexOf(this.field.toLowerCase()) >= 0;
}
return false;
})
.map((option, index) => {
return {
optionId: index,
name: option.name,
color: option.color
};
});
return optionsFiltered;
}
const optionsFiltered = fullOptions.map((option, index) => {
return {
optionId: index,
name: option.name,
color: option.color
};
});
return optionsFiltered;
}
},
watch: {
field(newValue, oldValue) {
if (newValue !== oldValue) {
const data = {
model: this.model,
value: newValue
};
this.$emit('onChange', data);
}
},
hideOptions(newValue) {
if (!newValue) {
// adding a event listener when the hideOptions is false (dropdown is visible)
// handleoutsideclick can collapse the dropdown when clicked outside autocomplete
document.body.addEventListener('click', this.handleOutsideClick);
} else {
//removing event listener when hideOptions become true (dropdown is collapsed)
document.body.removeEventListener('click', this.handleOutsideClick);
}
}
},
mounted() {
this.autocompleteInputAndArrow = this.$refs.autoCompleteForm;
this.autocompleteInputElement = this.$refs.autoCompleteInput;
if (this.model.options && this.model.options.length && !this.model.options[0].name) {
// If options is only an array of string.
this.options = this.model.options.map((option) => {
return {
name: option
};
});
} else {
this.options = this.model.options;
}
},
destroyed() {
document.body.removeEventListener('click', this.handleOutsideClick);
},
methods: {
decrementOptionIndex() {
if (this.optionIndex === 0) {
this.optionIndex = this.filteredOptions.length;
}
this.optionIndex--;
this.scrollIntoView();
},
incrementOptionIndex() {
if (this.optionIndex === this.filteredOptions.length - 1) {
this.optionIndex = -1;
}
this.optionIndex++;
this.scrollIntoView();
},
fillInputWithString(string) {
this.hideOptions = true;
this.field = string;
},
showOptions() {
this.hideOptions = false;
this.optionIndex = 0;
},
keyDown($event) {
this.showFilteredOptions = true;
if (this.filteredOptions) {
let keyCode = $event.keyCode;
switch (keyCode) {
case key.down:
this.incrementOptionIndex();
break;
case key.up:
$event.preventDefault(); // Prevents cursor jumping back and forth
this.decrementOptionIndex();
break;
case key.enter:
if (this.filteredOptions[this.optionIndex]) {
this.fillInputWithString(this.filteredOptions[this.optionIndex].name);
}
}
}
},
inputClicked() {
this.autocompleteInputElement.select();
this.showOptions();
},
arrowClicked() {
// if the user clicked the arrow, we want
// to show them all the options
this.showFilteredOptions = false;
this.autocompleteInputElement.select();
if (!this.hideOptions && this.filteredOptions.length > 0) {
this.hideOptions = true;
} else {
this.showOptions();
}
},
handleOutsideClick(event) {
// if click event is detected outside autocomplete (both input & arrow) while the
// dropdown is visible, this will collapse the dropdown.
const clickedInsideAutocomplete = this.autocompleteInputAndArrow.contains(event.target);
if (!clickedInsideAutocomplete && !this.hideOptions) {
this.hideOptions = true;
}
},
optionMouseover(optionId) {
this.optionIndex = optionId;
},
scrollIntoView() {
setTimeout(() => {
const element = this.$el.querySelector('.optionPreSelected');
if (element) {
element.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'nearest'
});
}
});
},
itemStyle(option) {
if (option.color) {
return { '--optionIconColor': option.color };
}
}
}
};
</script>

View File

@@ -21,35 +21,28 @@
-->
<template>
<span class="form-control shell">
<span
class="field control"
:class="model.cssClass"
>
<input
type="checkbox"
:checked="isChecked"
@input="toggleCheckBox"
>
<span class="form-control shell">
<span class="field control" :class="model.cssClass">
<input type="checkbox" :checked="isChecked" @input="toggleCheckBox" />
</span>
</span>
</span>
</template>
<script>
import toggleMixin from '../../toggle-check-box-mixin';
export default {
mixins: [toggleMixin],
props: {
model: {
type: Object,
required: true
}
},
data() {
return {
isChecked: this.model.value
};
mixins: [toggleMixin],
props: {
model: {
type: Object,
required: true
}
},
data() {
return {
isChecked: this.model.value
};
}
};
</script>

View File

@@ -21,47 +21,42 @@
-->
<template>
<div class="c-form-control--clock-display-format-fields">
<SelectField
v-for="item in items"
:key="item.key"
:model="item"
@onChange="onChange"
/>
</div>
<div class="c-form-control--clock-display-format-fields">
<SelectField v-for="item in items" :key="item.key" :model="item" @onChange="onChange" />
</div>
</template>
<script>
import SelectField from '@/api/forms/components/controls/SelectField.vue';
export default {
components: {
SelectField
},
props: {
model: {
type: Object,
required: true
}
},
data() {
return {
items: []
};
},
mounted() {
const values = this.model.value || [];
this.items = this.model.items.map((item, index) => {
item.value = values[index];
item.key = `${this.model.key}.${index}`;
return item;
});
},
methods: {
onChange(data) {
this.$emit('onChange', data);
}
components: {
SelectField
},
props: {
model: {
type: Object,
required: true
}
},
data() {
return {
items: []
};
},
mounted() {
const values = this.model.value || [];
this.items = this.model.items.map((item, index) => {
item.value = values[index];
item.key = `${this.model.key}.${index}`;
return item;
});
},
methods: {
onChange(data) {
this.$emit('onChange', data);
}
}
};
</script>

View File

@@ -21,38 +21,38 @@
-->
<template>
<span>
<span>
<CompositeItem
v-for="(item, index) in model.items"
:key="item.name"
:first="index < 1"
:value="JSON.stringify(model.value[index])"
:item="item"
@onChange="onChange"
v-for="(item, index) in model.items"
:key="item.name"
:first="index < 1"
:value="JSON.stringify(model.value[index])"
:item="item"
@onChange="onChange"
/>
</span>
</span>
</template>
<script>
import CompositeItem from "@/api/forms/components/controls/CompositeItem.vue";
import CompositeItem from '@/api/forms/components/controls/CompositeItem.vue';
export default {
components: {
CompositeItem
},
props: {
model: {
type: Object,
required: true
}
},
mounted() {
this.model.items.forEach((item, index) => item.key = `${this.model.key}.${index}`);
},
methods: {
onChange(data) {
this.$emit('onChange', data);
}
components: {
CompositeItem
},
props: {
model: {
type: Object,
required: true
}
},
mounted() {
this.model.items.forEach((item, index) => (item.key = `${this.model.key}.${index}`));
},
methods: {
onChange(data) {
this.$emit('onChange', data);
}
}
};
</script>

View File

@@ -21,56 +21,50 @@
-->
<template>
<div :class="compositeCssClass">
<FormRow
:css-class="item.cssClass"
:first="first"
:row="row"
@onChange="onChange"
/>
<div :class="compositeCssClass">
<FormRow :css-class="item.cssClass" :first="first" :row="row" @onChange="onChange" />
<span class="composite-control-label">
{{ item.name }}
{{ item.name }}
</span>
</div>
</div>
</template>
<script>
export default {
components: {
FormRow: () => import('@/api/forms/components/FormRow.vue')
components: {
FormRow: () => import('@/api/forms/components/FormRow.vue')
},
props: {
item: {
type: Object,
required: true
},
props: {
item: {
type: Object,
required: true
},
first: {
type: Boolean,
required: true
},
value: {
type: String,
default() {
return '';
}
}
first: {
type: Boolean,
required: true
},
computed: {
compositeCssClass() {
return `l-composite-control l-${this.item.control}`;
},
row() {
const row = this.item;
row.value = JSON.parse(this.value);
return row;
}
},
methods: {
onChange(data) {
this.$emit('onChange', data);
}
value: {
type: String,
default() {
return '';
}
}
},
computed: {
compositeCssClass() {
return `l-composite-control l-${this.item.control}`;
},
row() {
const row = this.item;
row.value = JSON.parse(this.value);
return row;
}
},
methods: {
onChange(data) {
this.$emit('onChange', data);
}
}
};
</script>

View File

@@ -21,150 +21,144 @@
-->
<template>
<div class="c-form-control--datetime">
<div class="c-form-control--datetime">
<div class="hint date">Date</div>
<div class="hint time sm">Hour</div>
<div class="hint time sm">Min</div>
<div class="hint time sm">Sec</div>
<div class="hint timezone">Timezone</div>
<form
ref="dateTimeForm"
prevent
class="u-contents"
>
<input
v-model="date"
class="field control date"
:pattern="/\d{4}-\d{2}-\d{2}/"
:placeholder="format"
type="date"
name="date"
@change="onChange"
>
<input
v-model="hour"
class="field control hour c-input--sm"
:pattern="/\d+/"
type="number"
name="hour"
maxlength="10"
min="0"
max="23"
@change="onChange"
>
<input
v-model="min"
class="field control min c-input--sm"
:pattern="/\d+/"
type="number"
name="min"
maxlength="2"
min="0"
max="59"
@change="onChange"
>
<input
v-model="sec"
class="field control sec c-input--sm"
:pattern="/\d+/"
type="number"
name="sec"
maxlength="2"
min="0"
max="59"
@change="onChange"
>
<div class="field control hint timezone">
UTC
</div>
<form ref="dateTimeForm" prevent class="u-contents">
<input
v-model="date"
class="field control date"
:pattern="/\d{4}-\d{2}-\d{2}/"
:placeholder="format"
type="date"
name="date"
@change="onChange"
/>
<input
v-model="hour"
class="field control hour c-input--sm"
:pattern="/\d+/"
type="number"
name="hour"
maxlength="10"
min="0"
max="23"
@change="onChange"
/>
<input
v-model="min"
class="field control min c-input--sm"
:pattern="/\d+/"
type="number"
name="min"
maxlength="2"
min="0"
max="59"
@change="onChange"
/>
<input
v-model="sec"
class="field control sec c-input--sm"
:pattern="/\d+/"
type="number"
name="sec"
maxlength="2"
min="0"
max="59"
@change="onChange"
/>
<div class="field control hint timezone">UTC</div>
</form>
</div>
</div>
</template>
<script>
const DATE_FORMAT = 'YYYY-MM-DD';
export default {
props: {
model: {
type: Object,
required: true
}
},
data() {
return {
format: DATE_FORMAT,
date: '',
hour: 0,
min: 0,
sec: 0
};
},
mounted() {
this.formatDatetime();
},
methods: {
convertToDatetime(timestamp) {
const dateValue = new Date(timestamp);
const date = dateValue.toISOString().slice(0, 10);
const hour = dateValue.getUTCHours() || 0;
const min = dateValue.getUTCMinutes() || 0;
const sec = dateValue.getUTCSeconds() || 0;
return {
date,
hour,
min,
sec
};
},
convertToTimestamp() {
const date = new Date(this.date);
date.setUTCHours(this.hour || 0);
date.setUTCMinutes(this.min || 0);
date.setUTCSeconds(this.sec || 0);
return date.getTime();
},
formatDatetime(timestamp = this.model.value) {
if (!timestamp) {
this.resetValues();
return;
}
const datetime = this.convertToDatetime(timestamp);
this.setDatetime(datetime.date, datetime.hour, datetime.min, datetime.sec);
},
onChange() {
const timestamp = this.convertToTimestamp();
const model = this.model;
model.validate = () => this.validate(timestamp);
const data = {
model,
value: timestamp
};
this.$emit('onChange', data);
},
resetValues() {
this.setDatetime();
},
setDatetime(date = '', hour = 0, min = 0, sec = 0) {
this.date = date.toString();
this.hour = hour;
this.min = min;
this.sec = sec;
},
validate(timestamp) {
const valid = timestamp > 0 && this.$refs.dateTimeForm.checkValidity();
if (!valid) {
this.$refs.dateTimeForm.reportValidity();
}
return valid;
}
props: {
model: {
type: Object,
required: true
}
},
data() {
return {
format: DATE_FORMAT,
date: '',
hour: 0,
min: 0,
sec: 0
};
},
mounted() {
this.formatDatetime();
},
methods: {
convertToDatetime(timestamp) {
const dateValue = new Date(timestamp);
const date = dateValue.toISOString().slice(0, 10);
const hour = dateValue.getUTCHours() || 0;
const min = dateValue.getUTCMinutes() || 0;
const sec = dateValue.getUTCSeconds() || 0;
return {
date,
hour,
min,
sec
};
},
convertToTimestamp() {
const date = new Date(this.date);
date.setUTCHours(this.hour || 0);
date.setUTCMinutes(this.min || 0);
date.setUTCSeconds(this.sec || 0);
return date.getTime();
},
formatDatetime(timestamp = this.model.value) {
if (!timestamp) {
this.resetValues();
return;
}
const datetime = this.convertToDatetime(timestamp);
this.setDatetime(datetime.date, datetime.hour, datetime.min, datetime.sec);
},
onChange() {
const timestamp = this.convertToTimestamp();
const model = this.model;
model.validate = () => this.validate(timestamp);
const data = {
model,
value: timestamp
};
this.$emit('onChange', data);
},
resetValues() {
this.setDatetime();
},
setDatetime(date = '', hour = 0, min = 0, sec = 0) {
this.date = date.toString();
this.hour = hour;
this.min = min;
this.sec = sec;
},
validate(timestamp) {
const valid = timestamp > 0 && this.$refs.dateTimeForm.checkValidity();
if (!valid) {
this.$refs.dateTimeForm.reportValidity();
}
return valid;
}
}
};
</script>

View File

@@ -21,129 +21,122 @@
-->
<template>
<span class="form-control shell">
<span
class="field control"
:class="model.cssClass"
>
<input
id="fileElem"
ref="fileInput"
type="file"
:accept="acceptableFileTypes"
style="display:none"
>
<button
id="fileSelect"
class="c-button"
@click="selectFile"
>
{{ name }}
</button>
<button
v-if="removable"
class="c-button icon-trash"
title="Remove file"
@click="removeFile"
></button>
<span class="form-control shell">
<span class="field control" :class="model.cssClass">
<input
id="fileElem"
ref="fileInput"
type="file"
:accept="acceptableFileTypes"
style="display: none"
/>
<button id="fileSelect" class="c-button" @click="selectFile">
{{ name }}
</button>
<button
v-if="removable"
class="c-button icon-trash"
title="Remove file"
@click="removeFile"
></button>
</span>
</span>
</span>
</template>
<script>
export default {
inject: ['openmct'],
props: {
model: {
type: Object,
required: true
}
},
data() {
return {
fileInfo: undefined
};
},
computed: {
name() {
const fileInfo = this.fileInfo || this.model.value;
return fileInfo && fileInfo.name || this.model.text;
},
removable() {
return (this.fileInfo || this.model.value) && this.model.removable;
},
acceptableFileTypes() {
if (this.model.type) {
return this.model.type;
}
return 'application/json';
}
},
mounted() {
this.$refs.fileInput.addEventListener("change", this.handleFiles, false);
},
methods: {
handleFiles() {
const fileList = this.$refs.fileInput.files;
const file = fileList[0];
if (this.acceptableFileTypes === 'application/json') {
this.readFile(file);
} else {
this.handleRawFile(file);
}
},
readFile(file) {
const self = this;
const fileReader = new FileReader();
const fileInfo = {};
fileInfo.name = file.name;
fileReader.onload = function (event) {
fileInfo.body = event.target.result;
self.fileInfo = fileInfo;
const data = {
model: self.model,
value: fileInfo
};
self.$emit('onChange', data);
};
fileReader.onerror = function (error) {
console.error('fileReader error', error);
};
fileReader.readAsText(file);
},
handleRawFile(file) {
const fileInfo = {
name: file.name,
body: file
};
this.fileInfo = Object.assign({}, fileInfo);
const data = {
model: this.model,
value: fileInfo
};
this.$emit('onChange', data);
},
selectFile() {
this.$refs.fileInput.click();
},
removeFile() {
this.model.value = undefined;
this.fileInfo = undefined;
const data = {
model: this.model,
value: undefined
};
this.$emit('onChange', data);
}
inject: ['openmct'],
props: {
model: {
type: Object,
required: true
}
},
data() {
return {
fileInfo: undefined
};
},
computed: {
name() {
const fileInfo = this.fileInfo || this.model.value;
return (fileInfo && fileInfo.name) || this.model.text;
},
removable() {
return (this.fileInfo || this.model.value) && this.model.removable;
},
acceptableFileTypes() {
if (this.model.type) {
return this.model.type;
}
return 'application/json';
}
},
mounted() {
this.$refs.fileInput.addEventListener('change', this.handleFiles, false);
},
methods: {
handleFiles() {
const fileList = this.$refs.fileInput.files;
const file = fileList[0];
if (this.acceptableFileTypes === 'application/json') {
this.readFile(file);
} else {
this.handleRawFile(file);
}
},
readFile(file) {
const self = this;
const fileReader = new FileReader();
const fileInfo = {};
fileInfo.name = file.name;
fileReader.onload = function (event) {
fileInfo.body = event.target.result;
self.fileInfo = fileInfo;
const data = {
model: self.model,
value: fileInfo
};
self.$emit('onChange', data);
};
fileReader.onerror = function (error) {
console.error('fileReader error', error);
};
fileReader.readAsText(file);
},
handleRawFile(file) {
const fileInfo = {
name: file.name,
body: file
};
this.fileInfo = Object.assign({}, fileInfo);
const data = {
model: this.model,
value: fileInfo
};
this.$emit('onChange', data);
},
selectFile() {
this.$refs.fileInput.click();
},
removeFile() {
this.model.value = undefined;
this.fileInfo = undefined;
const data = {
model: this.model,
value: undefined
};
this.$emit('onChange', data);
}
}
};
</script>

View File

@@ -21,36 +21,36 @@
-->
<template>
<mct-tree
<mct-tree
:is-selector-tree="true"
:initial-selection="model.parent"
@tree-item-selection="handleItemSelection"
/>
/>
</template>
<script>
import MctTree from '@/ui/layout/mct-tree.vue';
export default {
components: {
MctTree
},
inject: ['openmct'],
props: {
model: {
type: Object,
required: true
}
},
methods: {
handleItemSelection(item) {
const data = {
model: this.model,
value: item.objectPath
};
this.$emit('onChange', data);
}
components: {
MctTree
},
inject: ['openmct'],
props: {
model: {
type: Object,
required: true
}
},
methods: {
handleItemSelection(item) {
const data = {
model: this.model,
value: item.objectPath
};
this.$emit('onChange', data);
}
}
};
</script>

View File

@@ -21,51 +21,48 @@
-->
<template>
<span class="form-control shell">
<span
class="field control"
:class="model.cssClass"
>
<input
v-model="field"
:aria-label="model.name"
type="number"
:min="model.min"
:max="model.max"
:step="model.step"
@input="updateText()"
>
<span class="form-control shell">
<span class="field control" :class="model.cssClass">
<input
v-model="field"
:aria-label="model.name"
type="number"
:min="model.min"
:max="model.max"
:step="model.step"
@input="updateText()"
/>
</span>
</span>
</span>
</template>
<script>
import { throttle } from 'lodash';
export default {
props: {
model: {
type: Object,
required: true
}
},
data() {
return {
field: this.model.value
};
},
mounted() {
this.updateText = throttle(this.updateText.bind(this), 200);
},
methods: {
updateText() {
const data = {
model: this.model,
value: this.field
};
this.$emit('onChange', data);
}
props: {
model: {
type: Object,
required: true
}
},
data() {
return {
field: this.model.value
};
},
mounted() {
this.updateText = throttle(this.updateText.bind(this), 200);
},
methods: {
updateText() {
const data = {
model: this.model,
value: this.field
};
this.$emit('onChange', data);
}
}
};
</script>

View File

@@ -21,47 +21,43 @@
-->
<template>
<div class="form-control select-field">
<div class="form-control select-field">
<select
v-model="selected"
required="model.required"
name="mctControl"
:aria-label="model.ariaLabel || model.name"
@change="onChange($event)"
v-model="selected"
required="model.required"
name="mctControl"
:aria-label="model.ariaLabel || model.name"
@change="onChange($event)"
>
<option
v-for="option in model.options"
:key="option.name"
:value="option.value"
>
{{ option.name }}
</option>
<option v-for="option in model.options" :key="option.name" :value="option.value">
{{ option.name }}
</option>
</select>
</div>
</div>
</template>
<script>
export default {
props: {
model: {
type: Object,
required: true
}
},
data() {
return {
selected: this.model.value
};
},
methods: {
onChange() {
const data = {
model: this.model,
value: this.selected
};
this.$emit('onChange', data);
}
props: {
model: {
type: Object,
required: true
}
},
data() {
return {
selected: this.model.value
};
},
methods: {
onChange() {
const data = {
model: this.model,
value: this.selected
};
this.$emit('onChange', data);
}
}
};
</script>

View File

@@ -21,50 +21,47 @@
-->
<template>
<span class="form-control shell">
<span
class="field control"
:class="model.cssClass"
>
<textarea
:id="`${model.key}-textarea`"
v-model="field"
type="text"
:size="model.size"
@input="updateText()"
>
</textarea>
<span class="form-control shell">
<span class="field control" :class="model.cssClass">
<textarea
:id="`${model.key}-textarea`"
v-model="field"
type="text"
:size="model.size"
@input="updateText()"
>
</textarea>
</span>
</span>
</span>
</template>
<script>
import { throttle } from 'lodash';
export default {
props: {
model: {
type: Object,
required: true
}
},
data() {
return {
field: this.model.value
};
},
mounted() {
this.updateText = throttle(this.updateText.bind(this), 500);
},
methods: {
updateText() {
const data = {
model: this.model,
value: this.field
};
this.$emit('onChange', data);
}
props: {
model: {
type: Object,
required: true
}
},
data() {
return {
field: this.model.value
};
},
mounted() {
this.updateText = throttle(this.updateText.bind(this), 500);
},
methods: {
updateText() {
const data = {
model: this.model,
value: this.field
};
this.$emit('onChange', data);
}
}
};
</script>

View File

@@ -21,48 +21,40 @@
-->
<template>
<span class="form-control shell">
<span
class="field control"
:class="model.cssClass"
>
<input
v-model="field"
type="text"
:size="model.size"
@input="updateText()"
>
<span class="form-control shell">
<span class="field control" :class="model.cssClass">
<input v-model="field" type="text" :size="model.size" @input="updateText()" />
</span>
</span>
</span>
</template>
<script>
import { throttle } from 'lodash';
export default {
props: {
model: {
type: Object,
required: true
}
},
data() {
return {
field: this.model.value
};
},
mounted() {
this.updateText = throttle(this.updateText.bind(this), 500);
},
methods: {
updateText() {
const data = {
model: this.model,
value: this.field
};
this.$emit('onChange', data);
}
props: {
model: {
type: Object,
required: true
}
},
data() {
return {
field: this.model.value
};
},
mounted() {
this.updateText = throttle(this.updateText.bind(this), 500);
},
methods: {
updateText() {
const data = {
model: this.model,
value: this.field
};
this.$emit('onChange', data);
}
}
};
</script>

View File

@@ -21,19 +21,16 @@
-->
<template>
<span class="form-control shell">
<span
class="field control"
:class="model.cssClass"
>
<ToggleSwitch
id="switchId"
:checked="isChecked"
:name="model.name"
@change="toggleCheckBox"
/>
<span class="form-control shell">
<span class="field control" :class="model.cssClass">
<ToggleSwitch
id="switchId"
:checked="isChecked"
:name="model.name"
@change="toggleCheckBox"
/>
</span>
</span>
</span>
</template>
<script>
@@ -43,21 +40,21 @@ import ToggleSwitch from '@/ui/components/ToggleSwitch.vue';
import { v4 as uuid } from 'uuid';
export default {
components: {
ToggleSwitch
},
mixins: [toggleMixin],
props: {
model: {
type: Object,
required: true
}
},
data() {
return {
switchId: `toggleSwitch-${uuid}`,
isChecked: this.model.value
};
components: {
ToggleSwitch
},
mixins: [toggleMixin],
props: {
model: {
type: Object,
required: true
}
},
data() {
return {
switchId: `toggleSwitch-${uuid}`,
isChecked: this.model.value
};
}
};
</script>

View File

@@ -1,19 +1,19 @@
export default {
data() {
return {
isChecked: false
};
},
methods: {
toggleCheckBox(event) {
this.isChecked = !this.isChecked;
data() {
return {
isChecked: false
};
},
methods: {
toggleCheckBox(event) {
this.isChecked = !this.isChecked;
const data = {
model: this.model,
value: this.isChecked
};
const data = {
model: this.model,
value: this.isChecked
};
this.$emit('onChange', data);
}
this.$emit('onChange', data);
}
}
};

View File

@@ -20,58 +20,57 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import EventEmitter from "EventEmitter";
import SimpleIndicator from "./SimpleIndicator";
import EventEmitter from 'EventEmitter';
import SimpleIndicator from './SimpleIndicator';
class IndicatorAPI extends EventEmitter {
constructor(openmct) {
super();
constructor(openmct) {
super();
this.openmct = openmct;
this.indicatorObjects = [];
this.openmct = openmct;
this.indicatorObjects = [];
}
getIndicatorObjectsByPriority() {
const sortedIndicators = this.indicatorObjects.sort((a, b) => b.priority - a.priority);
return sortedIndicators;
}
simpleIndicator() {
return new SimpleIndicator(this.openmct);
}
/**
* Accepts an indicator object, which is a simple object
* with a two attributes: 'element' which has an HTMLElement
* as its value, and 'priority' with an integer that specifies its order in the layout.
* The lower the priority, the further to the right the element is placed.
* If undefined, the priority will be assigned -1.
*
* We provide .simpleIndicator() as a convenience function
* which will create a default Open MCT indicator that can
* be passed to .add(indicator). This indicator also exposes
* functions for changing its appearance to support customization
* and dynamic behavior.
*
* Eg.
* const myIndicator = openmct.indicators.simpleIndicator();
* openmct.indicators.add(myIndicator);
*
* myIndicator.text("Hello World!");
* myIndicator.iconClass("icon-info");
*
*/
add(indicator) {
if (!indicator.priority) {
indicator.priority = this.openmct.priority.DEFAULT;
}
getIndicatorObjectsByPriority() {
const sortedIndicators = this.indicatorObjects.sort((a, b) => b.priority - a.priority);
return sortedIndicators;
}
simpleIndicator() {
return new SimpleIndicator(this.openmct);
}
/**
* Accepts an indicator object, which is a simple object
* with a two attributes: 'element' which has an HTMLElement
* as its value, and 'priority' with an integer that specifies its order in the layout.
* The lower the priority, the further to the right the element is placed.
* If undefined, the priority will be assigned -1.
*
* We provide .simpleIndicator() as a convenience function
* which will create a default Open MCT indicator that can
* be passed to .add(indicator). This indicator also exposes
* functions for changing its appearance to support customization
* and dynamic behavior.
*
* Eg.
* const myIndicator = openmct.indicators.simpleIndicator();
* openmct.indicators.add(myIndicator);
*
* myIndicator.text("Hello World!");
* myIndicator.iconClass("icon-info");
*
*/
add(indicator) {
if (!indicator.priority) {
indicator.priority = this.openmct.priority.DEFAULT;
}
this.indicatorObjects.push(indicator);
this.emit('addIndicator', indicator);
}
this.indicatorObjects.push(indicator);
this.emit('addIndicator', indicator);
}
}
export default IndicatorAPI;

View File

@@ -22,61 +22,77 @@
import { createOpenMct, resetApplicationState } from '../../utils/testing';
import SimpleIndicator from './SimpleIndicator';
describe("The Indicator API", () => {
let openmct;
describe('The Indicator API', () => {
let openmct;
beforeEach(() => {
openmct = createOpenMct();
});
beforeEach(() => {
openmct = createOpenMct();
});
afterEach(() => {
return resetApplicationState(openmct);
});
afterEach(() => {
return resetApplicationState(openmct);
});
function generateIndicator(className, label, priority) {
const element = document.createElement('div');
element.classList.add(className);
const textNode = document.createTextNode(label);
element.appendChild(textNode);
const testIndicator = {
element,
priority
};
function generateIndicator(className, label, priority) {
const element = document.createElement('div');
element.classList.add(className);
const textNode = document.createTextNode(label);
element.appendChild(textNode);
const testIndicator = {
element,
priority
};
return testIndicator;
}
return testIndicator;
}
it("can register an indicator", () => {
const testIndicator = generateIndicator('test-indicator', 'This is a test indicator', 2);
openmct.indicators.add(testIndicator);
expect(openmct.indicators.indicatorObjects).toBeDefined();
// notifier indicator is installed by default
expect(openmct.indicators.indicatorObjects.length).toBe(2);
});
it('can register an indicator', () => {
const testIndicator = generateIndicator('test-indicator', 'This is a test indicator', 2);
openmct.indicators.add(testIndicator);
expect(openmct.indicators.indicatorObjects).toBeDefined();
// notifier indicator is installed by default
expect(openmct.indicators.indicatorObjects.length).toBe(2);
});
it("can order indicators based on priority", () => {
const testIndicator1 = generateIndicator('test-indicator-1', 'This is a test indicator', openmct.priority.LOW);
openmct.indicators.add(testIndicator1);
it('can order indicators based on priority', () => {
const testIndicator1 = generateIndicator(
'test-indicator-1',
'This is a test indicator',
openmct.priority.LOW
);
openmct.indicators.add(testIndicator1);
const testIndicator2 = generateIndicator('test-indicator-2', 'This is another test indicator', openmct.priority.DEFAULT);
openmct.indicators.add(testIndicator2);
const testIndicator2 = generateIndicator(
'test-indicator-2',
'This is another test indicator',
openmct.priority.DEFAULT
);
openmct.indicators.add(testIndicator2);
const testIndicator3 = generateIndicator('test-indicator-3', 'This is yet another test indicator', openmct.priority.LOW);
openmct.indicators.add(testIndicator3);
const testIndicator3 = generateIndicator(
'test-indicator-3',
'This is yet another test indicator',
openmct.priority.LOW
);
openmct.indicators.add(testIndicator3);
const testIndicator4 = generateIndicator('test-indicator-4', 'This is yet another test indicator', openmct.priority.HIGH);
openmct.indicators.add(testIndicator4);
const testIndicator4 = generateIndicator(
'test-indicator-4',
'This is yet another test indicator',
openmct.priority.HIGH
);
openmct.indicators.add(testIndicator4);
expect(openmct.indicators.indicatorObjects.length).toBe(5);
const indicatorObjectsByPriority = openmct.indicators.getIndicatorObjectsByPriority();
expect(indicatorObjectsByPriority.length).toBe(5);
expect(indicatorObjectsByPriority[2].priority).toBe(openmct.priority.DEFAULT);
});
expect(openmct.indicators.indicatorObjects.length).toBe(5);
const indicatorObjectsByPriority = openmct.indicators.getIndicatorObjectsByPriority();
expect(indicatorObjectsByPriority.length).toBe(5);
expect(indicatorObjectsByPriority[2].priority).toBe(openmct.priority.DEFAULT);
});
it("the simple indicator can be added", () => {
const simpleIndicator = new SimpleIndicator(openmct);
openmct.indicators.add(simpleIndicator);
it('the simple indicator can be added', () => {
const simpleIndicator = new SimpleIndicator(openmct);
openmct.indicators.add(simpleIndicator);
expect(openmct.indicators.indicatorObjects.length).toBe(2);
});
expect(openmct.indicators.indicatorObjects.length).toBe(2);
});
});

View File

@@ -27,94 +27,94 @@ import { convertTemplateToHTML } from '@/utils/template/templateHelpers';
const DEFAULT_ICON_CLASS = 'icon-info';
class SimpleIndicator extends EventEmitter {
constructor(openmct) {
super();
constructor(openmct) {
super();
this.openmct = openmct;
this.element = convertTemplateToHTML(indicatorTemplate)[0];
this.priority = openmct.priority.DEFAULT;
this.openmct = openmct;
this.element = convertTemplateToHTML(indicatorTemplate)[0];
this.priority = openmct.priority.DEFAULT;
this.textElement = this.element.querySelector('.js-indicator-text');
this.textElement = this.element.querySelector('.js-indicator-text');
//Set defaults
this.text('New Indicator');
this.description('');
this.iconClass(DEFAULT_ICON_CLASS);
//Set defaults
this.text('New Indicator');
this.description('');
this.iconClass(DEFAULT_ICON_CLASS);
this.click = this.click.bind(this);
this.click = this.click.bind(this);
this.element.addEventListener('click', this.click);
openmct.once('destroy', () => {
this.removeAllListeners();
this.element.removeEventListener('click', this.click);
});
this.element.addEventListener('click', this.click);
openmct.once('destroy', () => {
this.removeAllListeners();
this.element.removeEventListener('click', this.click);
});
}
text(text) {
if (text !== undefined && text !== this.textValue) {
this.textValue = text;
this.textElement.innerText = text;
if (!text) {
this.element.classList.add('hidden');
} else {
this.element.classList.remove('hidden');
}
}
text(text) {
if (text !== undefined && text !== this.textValue) {
this.textValue = text;
this.textElement.innerText = text;
return this.textValue;
}
if (!text) {
this.element.classList.add('hidden');
} else {
this.element.classList.remove('hidden');
}
}
return this.textValue;
description(description) {
if (description !== undefined && description !== this.descriptionValue) {
this.descriptionValue = description;
this.element.title = description;
}
description(description) {
if (description !== undefined && description !== this.descriptionValue) {
this.descriptionValue = description;
this.element.title = description;
}
return this.descriptionValue;
}
return this.descriptionValue;
iconClass(iconClass) {
if (iconClass !== undefined && iconClass !== this.iconClassValue) {
// element.classList is precious and throws errors if you try and add
// or remove empty strings
if (this.iconClassValue) {
this.element.classList.remove(this.iconClassValue);
}
if (iconClass) {
this.element.classList.add(iconClass);
}
this.iconClassValue = iconClass;
}
iconClass(iconClass) {
if (iconClass !== undefined && iconClass !== this.iconClassValue) {
// element.classList is precious and throws errors if you try and add
// or remove empty strings
if (this.iconClassValue) {
this.element.classList.remove(this.iconClassValue);
}
return this.iconClassValue;
}
if (iconClass) {
this.element.classList.add(iconClass);
}
statusClass(statusClass) {
if (arguments.length === 1 && statusClass !== this.statusClassValue) {
if (this.statusClassValue) {
this.element.classList.remove(this.statusClassValue);
}
this.iconClassValue = iconClass;
}
if (statusClass !== undefined) {
this.element.classList.add(statusClass);
}
return this.iconClassValue;
this.statusClassValue = statusClass;
}
statusClass(statusClass) {
if (arguments.length === 1 && statusClass !== this.statusClassValue) {
if (this.statusClassValue) {
this.element.classList.remove(this.statusClassValue);
}
return this.statusClassValue;
}
if (statusClass !== undefined) {
this.element.classList.add(statusClass);
}
click(event) {
this.emit('click', event);
}
this.statusClassValue = statusClass;
}
return this.statusClassValue;
}
click(event) {
this.emit('click', event);
}
getElement() {
return this.element;
}
getElement() {
return this.element;
}
}
export default SimpleIndicator;

View File

@@ -1,3 +1,3 @@
<div class="c-indicator c-indicator--clickable c-indicator--simple" title="">
<span class="label js-indicator-text c-indicator__label"></span>
<span class="label js-indicator-text c-indicator__label"></span>
</div>

View File

@@ -48,83 +48,86 @@ import Menu, { MENU_PLACEMENT } from './menu.js';
*/
class MenuAPI {
constructor(openmct) {
this.openmct = openmct;
constructor(openmct) {
this.openmct = openmct;
this.menuPlacement = MENU_PLACEMENT;
this.showMenu = this.showMenu.bind(this);
this.showSuperMenu = this.showSuperMenu.bind(this);
this.menuPlacement = MENU_PLACEMENT;
this.showMenu = this.showMenu.bind(this);
this.showSuperMenu = this.showSuperMenu.bind(this);
this._clearMenuComponent = this._clearMenuComponent.bind(this);
this._showObjectMenu = this._showObjectMenu.bind(this);
}
this._clearMenuComponent = this._clearMenuComponent.bind(this);
this._showObjectMenu = this._showObjectMenu.bind(this);
}
/**
* Show popup menu
* @param {number} x x-coordinates for popup
* @param {number} y x-coordinates for popup
* @param {Array.<Action>|Array.<Array.<Action>>} actions collection of actions{@link Action} or collection of groups of actions {@link Action}
* @param {MenuOptions} [menuOptions] [Optional] The {@link MenuOptions} options for Menu
*/
showMenu(x, y, items, menuOptions) {
this._createMenuComponent(x, y, items, menuOptions);
/**
* Show popup menu
* @param {number} x x-coordinates for popup
* @param {number} y x-coordinates for popup
* @param {Array.<Action>|Array.<Array.<Action>>} actions collection of actions{@link Action} or collection of groups of actions {@link Action}
* @param {MenuOptions} [menuOptions] [Optional] The {@link MenuOptions} options for Menu
*/
showMenu(x, y, items, menuOptions) {
this._createMenuComponent(x, y, items, menuOptions);
this.menuComponent.showMenu();
}
this.menuComponent.showMenu();
}
actionsToMenuItems(actions, objectPath, view) {
return actions.map(action => {
const isActionGroup = Array.isArray(action);
if (isActionGroup) {
action = this.actionsToMenuItems(action, objectPath, view);
} else {
action.onItemClicked = () => {
action.invoke(objectPath, view);
};
}
return action;
});
}
/**
* Show popup menu with description of item on hover
* @param {number} x x-coordinates for popup
* @param {number} y x-coordinates for popup
* @param {Array.<Action>|Array.<Array.<Action>>} actions collection of actions {@link Action} or collection of groups of actions {@link Action}
* @param {MenuOptions} [menuOptions] [Optional] The {@link MenuOptions} options for Menu
*/
showSuperMenu(x, y, actions, menuOptions) {
this._createMenuComponent(x, y, actions, menuOptions);
this.menuComponent.showSuperMenu();
}
_clearMenuComponent() {
this.menuComponent = undefined;
delete this.menuComponent;
}
_createMenuComponent(x, y, actions, menuOptions = {}) {
if (this.menuComponent) {
this.menuComponent.dismiss();
}
let options = {
x,
y,
actions,
...menuOptions
actionsToMenuItems(actions, objectPath, view) {
return actions.map((action) => {
const isActionGroup = Array.isArray(action);
if (isActionGroup) {
action = this.actionsToMenuItems(action, objectPath, view);
} else {
action.onItemClicked = () => {
action.invoke(objectPath, view);
};
}
this.menuComponent = new Menu(options);
this.menuComponent.once('destroy', this._clearMenuComponent);
return action;
});
}
/**
* Show popup menu with description of item on hover
* @param {number} x x-coordinates for popup
* @param {number} y x-coordinates for popup
* @param {Array.<Action>|Array.<Array.<Action>>} actions collection of actions {@link Action} or collection of groups of actions {@link Action}
* @param {MenuOptions} [menuOptions] [Optional] The {@link MenuOptions} options for Menu
*/
showSuperMenu(x, y, actions, menuOptions) {
this._createMenuComponent(x, y, actions, menuOptions);
this.menuComponent.showSuperMenu();
}
_clearMenuComponent() {
this.menuComponent = undefined;
delete this.menuComponent;
}
_createMenuComponent(x, y, actions, menuOptions = {}) {
if (this.menuComponent) {
this.menuComponent.dismiss();
}
_showObjectMenu(objectPath, x, y, actionsToBeIncluded) {
let applicableActions = this.openmct.actions._groupedAndSortedObjectActions(objectPath, actionsToBeIncluded);
let options = {
x,
y,
actions,
...menuOptions
};
this.showMenu(x, y, applicableActions);
}
this.menuComponent = new Menu(options);
this.menuComponent.once('destroy', this._clearMenuComponent);
}
_showObjectMenu(objectPath, x, y, actionsToBeIncluded) {
let applicableActions = this.openmct.actions._groupedAndSortedObjectActions(
objectPath,
actionsToBeIncluded
);
this.showMenu(x, y, applicableActions);
}
}
export default MenuAPI;

View File

@@ -24,208 +24,208 @@ import MenuAPI from './MenuAPI';
import Menu from './menu';
import { createOpenMct, createMouseEvent, resetApplicationState } from '../../utils/testing';
describe ('The Menu API', () => {
let openmct;
let appHolder;
let menuAPI;
let actionsArray;
let result;
let menuElement;
describe('The Menu API', () => {
let openmct;
let appHolder;
let menuAPI;
let actionsArray;
let result;
let menuElement;
const x = 8;
const y = 16;
const x = 8;
const y = 16;
const menuOptions = {
onDestroy: () => {
console.log('default onDestroy');
const menuOptions = {
onDestroy: () => {
console.log('default onDestroy');
}
};
beforeEach((done) => {
appHolder = document.createElement('div');
appHolder.style.display = 'block';
appHolder.style.width = '1920px';
appHolder.style.height = '1080px';
openmct = createOpenMct();
openmct.on('start', done);
openmct.startHeadless();
menuAPI = new MenuAPI(openmct);
actionsArray = [
{
key: 'test-css-class-1',
name: 'Test Action 1',
cssClass: 'icon-clock',
description: 'This is a test action 1',
onItemClicked: () => {
result = 'Test Action 1 Invoked';
}
};
},
{
key: 'test-css-class-2',
name: 'Test Action 2',
cssClass: 'icon-clock',
description: 'This is a test action 2',
onItemClicked: () => {
result = 'Test Action 2 Invoked';
}
}
];
});
beforeEach((done) => {
appHolder = document.createElement('div');
appHolder.style.display = 'block';
appHolder.style.width = '1920px';
appHolder.style.height = '1080px';
afterEach(() => {
return resetApplicationState(openmct);
});
openmct = createOpenMct();
openmct.on('start', done);
openmct.startHeadless();
menuAPI = new MenuAPI(openmct);
actionsArray = [
{
key: 'test-css-class-1',
name: 'Test Action 1',
cssClass: 'icon-clock',
description: 'This is a test action 1',
onItemClicked: () => {
result = 'Test Action 1 Invoked';
}
},
{
key: 'test-css-class-2',
name: 'Test Action 2',
cssClass: 'icon-clock',
description: 'This is a test action 2',
onItemClicked: () => {
result = 'Test Action 2 Invoked';
}
}
];
describe('showMenu method', () => {
beforeAll(() => {
spyOn(menuOptions, 'onDestroy').and.callThrough();
});
afterEach(() => {
return resetApplicationState(openmct);
it('creates an instance of Menu when invoked', (done) => {
menuOptions.onDestroy = done;
menuAPI.showMenu(x, y, actionsArray, menuOptions);
expect(menuAPI.menuComponent).toBeInstanceOf(Menu);
document.body.click();
});
describe('showMenu method', () => {
beforeAll(() => {
spyOn(menuOptions, 'onDestroy').and.callThrough();
});
describe('creates a menu component', () => {
it('with all the actions passed in', (done) => {
menuOptions.onDestroy = done;
it('creates an instance of Menu when invoked', (done) => {
menuOptions.onDestroy = done;
menuAPI.showMenu(x, y, actionsArray, menuOptions);
menuElement = document.querySelector('.c-menu');
expect(menuElement).toBeDefined();
menuAPI.showMenu(x, y, actionsArray, menuOptions);
const listItems = menuElement.children[0].children;
expect(menuAPI.menuComponent).toBeInstanceOf(Menu);
document.body.click();
});
expect(listItems.length).toEqual(actionsArray.length);
document.body.click();
});
describe('creates a menu component', () => {
it('with all the actions passed in', (done) => {
menuOptions.onDestroy = done;
it('with click-able menu items, that will invoke the correct callBack', (done) => {
menuOptions.onDestroy = done;
menuAPI.showMenu(x, y, actionsArray, menuOptions);
menuElement = document.querySelector('.c-menu');
expect(menuElement).toBeDefined();
menuAPI.showMenu(x, y, actionsArray, menuOptions);
const listItems = menuElement.children[0].children;
menuElement = document.querySelector('.c-menu');
const listItem1 = menuElement.children[0].children[0];
expect(listItems.length).toEqual(actionsArray.length);
document.body.click();
});
listItem1.click();
it('with click-able menu items, that will invoke the correct callBack', (done) => {
menuOptions.onDestroy = done;
expect(result).toEqual('Test Action 1 Invoked');
});
menuAPI.showMenu(x, y, actionsArray, menuOptions);
it('dismisses the menu when action is clicked on', (done) => {
menuOptions.onDestroy = done;
menuElement = document.querySelector('.c-menu');
const listItem1 = menuElement.children[0].children[0];
menuAPI.showMenu(x, y, actionsArray, menuOptions);
listItem1.click();
menuElement = document.querySelector('.c-menu');
const listItem1 = menuElement.children[0].children[0];
listItem1.click();
expect(result).toEqual('Test Action 1 Invoked');
});
menuElement = document.querySelector('.c-menu');
it('dismisses the menu when action is clicked on', (done) => {
menuOptions.onDestroy = done;
expect(menuElement).toBeNull();
});
menuAPI.showMenu(x, y, actionsArray, menuOptions);
it('invokes the destroy method when menu is dismissed', (done) => {
menuOptions.onDestroy = done;
menuElement = document.querySelector('.c-menu');
const listItem1 = menuElement.children[0].children[0];
listItem1.click();
menuAPI.showMenu(x, y, actionsArray, menuOptions);
menuElement = document.querySelector('.c-menu');
const vueComponent = menuAPI.menuComponent.component;
spyOn(vueComponent, '$destroy');
expect(menuElement).toBeNull();
});
document.body.click();
it('invokes the destroy method when menu is dismissed', (done) => {
menuOptions.onDestroy = done;
expect(vueComponent.$destroy).toHaveBeenCalled();
});
menuAPI.showMenu(x, y, actionsArray, menuOptions);
it('invokes the onDestroy callback if passed in', (done) => {
let count = 0;
menuOptions.onDestroy = () => {
count++;
expect(count).toEqual(1);
done();
};
const vueComponent = menuAPI.menuComponent.component;
spyOn(vueComponent, '$destroy');
menuAPI.showMenu(x, y, actionsArray, menuOptions);
document.body.click();
document.body.click();
});
});
});
expect(vueComponent.$destroy).toHaveBeenCalled();
});
describe('superMenu method', () => {
it('creates a superMenu', (done) => {
menuOptions.onDestroy = done;
it('invokes the onDestroy callback if passed in', (done) => {
let count = 0;
menuOptions.onDestroy = () => {
count++;
expect(count).toEqual(1);
done();
};
menuAPI.showSuperMenu(x, y, actionsArray, menuOptions);
menuElement = document.querySelector('.c-super-menu__menu');
menuAPI.showMenu(x, y, actionsArray, menuOptions);
document.body.click();
});
});
expect(menuElement).not.toBeNull();
document.body.click();
});
describe('superMenu method', () => {
it('creates a superMenu', (done) => {
menuOptions.onDestroy = done;
it('Mouse over a superMenu shows correct description', (done) => {
menuOptions.onDestroy = done;
menuAPI.showSuperMenu(x, y, actionsArray, menuOptions);
menuElement = document.querySelector('.c-super-menu__menu');
menuAPI.showSuperMenu(x, y, actionsArray, menuOptions);
menuElement = document.querySelector('.c-super-menu__menu');
expect(menuElement).not.toBeNull();
document.body.click();
});
const superMenuItem = menuElement.querySelector('li');
const mouseOverEvent = createMouseEvent('mouseover');
it('Mouse over a superMenu shows correct description', (done) => {
menuOptions.onDestroy = done;
superMenuItem.dispatchEvent(mouseOverEvent);
const itemDescription = document.querySelector('.l-item-description__description');
menuAPI.showSuperMenu(x, y, actionsArray, menuOptions);
menuElement = document.querySelector('.c-super-menu__menu');
menuAPI.menuComponent.component.$nextTick(() => {
expect(menuElement).not.toBeNull();
expect(itemDescription.innerText).toEqual(actionsArray[0].description);
const superMenuItem = menuElement.querySelector('li');
const mouseOverEvent = createMouseEvent('mouseover');
document.body.click();
});
});
});
superMenuItem.dispatchEvent(mouseOverEvent);
const itemDescription = document.querySelector('.l-item-description__description');
describe('Menu Placements', () => {
it('default menu position BOTTOM_RIGHT', (done) => {
menuOptions.onDestroy = done;
menuAPI.menuComponent.component.$nextTick(() => {
expect(menuElement).not.toBeNull();
expect(itemDescription.innerText).toEqual(actionsArray[0].description);
menuAPI.showMenu(x, y, actionsArray, menuOptions);
menuElement = document.querySelector('.c-menu');
document.body.click();
});
});
const boundingClientRect = menuElement.getBoundingClientRect();
const left = boundingClientRect.left;
const top = boundingClientRect.top;
expect(left).toEqual(x);
expect(top).toEqual(y);
document.body.click();
});
describe('Menu Placements', () => {
it('default menu position BOTTOM_RIGHT', (done) => {
menuOptions.onDestroy = done;
it('menu position BOTTOM_RIGHT', (done) => {
menuOptions.onDestroy = done;
menuOptions.placement = openmct.menus.menuPlacement.BOTTOM_RIGHT;
menuAPI.showMenu(x, y, actionsArray, menuOptions);
menuElement = document.querySelector('.c-menu');
menuAPI.showMenu(x, y, actionsArray, menuOptions);
menuElement = document.querySelector('.c-menu');
const boundingClientRect = menuElement.getBoundingClientRect();
const left = boundingClientRect.left;
const top = boundingClientRect.top;
const boundingClientRect = menuElement.getBoundingClientRect();
const left = boundingClientRect.left;
const top = boundingClientRect.top;
expect(left).toEqual(x);
expect(top).toEqual(y);
expect(left).toEqual(x);
expect(top).toEqual(y);
document.body.click();
});
it('menu position BOTTOM_RIGHT', (done) => {
menuOptions.onDestroy = done;
menuOptions.placement = openmct.menus.menuPlacement.BOTTOM_RIGHT;
menuAPI.showMenu(x, y, actionsArray, menuOptions);
menuElement = document.querySelector('.c-menu');
const boundingClientRect = menuElement.getBoundingClientRect();
const left = boundingClientRect.left;
const top = boundingClientRect.top;
expect(left).toEqual(x);
expect(top).toEqual(y);
document.body.click();
});
document.body.click();
});
});
});

View File

@@ -20,72 +20,51 @@
at runtime from the About dialog for additional information.
-->
<template>
<div
class="c-menu"
:class="options.menuClass"
>
<ul
v-if="options.actions.length && options.actions[0].length"
role="menu"
>
<template
v-for="(actionGroups, index) in options.actions"
>
<div
:key="index"
role="group"
>
<li
v-for="action in actionGroups"
:key="action.name"
role="menuitem"
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
:title="action.description"
:data-testid="action.testId || false"
@click="action.onItemClicked"
>
{{ action.name }}
</li>
<div
v-if="index !== options.actions.length - 1"
:key="index"
role="separator"
class="c-menu__section-separator"
>
</div>
<li
v-if="actionGroups.length === 0"
:key="index"
>
No actions defined.
</li>
</div></template>
</ul>
<ul
v-else
role="menu"
>
<li
v-for="action in options.actions"
<div class="c-menu" :class="options.menuClass">
<ul v-if="options.actions.length && options.actions[0].length" role="menu">
<template v-for="(actionGroups, index) in options.actions">
<div :key="index" role="group">
<li
v-for="action in actionGroups"
:key="action.name"
role="menuitem"
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
:title="action.description"
:data-testid="action.testId || false"
@click="action.onItemClicked"
>
>
{{ action.name }}
</li>
<li v-if="options.actions.length === 0">
No actions defined.
</li>
</li>
<div
v-if="index !== options.actions.length - 1"
:key="index"
role="separator"
class="c-menu__section-separator"
></div>
<li v-if="actionGroups.length === 0" :key="index">No actions defined.</li>
</div></template
>
</ul>
</div>
<ul v-else role="menu">
<li
v-for="action in options.actions"
:key="action.name"
role="menuitem"
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
:title="action.description"
:data-testid="action.testId || false"
@click="action.onItemClicked"
>
{{ action.name }}
</li>
<li v-if="options.actions.length === 0">No actions defined.</li>
</ul>
</div>
</template>
<script>
export default {
inject: ['options']
inject: ['options']
};
</script>

View File

@@ -20,104 +20,85 @@
at runtime from the About dialog for additional information.
-->
<template>
<div
class="c-menu"
:class="[options.menuClass, 'c-super-menu']"
>
<div class="c-menu" :class="[options.menuClass, 'c-super-menu']">
<ul
v-if="options.actions.length && options.actions[0].length"
role="menu"
class="c-super-menu__menu"
v-if="options.actions.length && options.actions[0].length"
role="menu"
class="c-super-menu__menu"
>
<template
v-for="(actionGroups, index) in options.actions"
>
<div
:key="index"
role="group"
>
<li
v-for="action in actionGroups"
:key="action.name"
role="menuitem"
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
:title="action.description"
:data-testid="action.testId || false"
@click="action.onItemClicked"
@mouseover="toggleItemDescription(action)"
@mouseleave="toggleItemDescription()"
>
{{ action.name }}
</li>
<div
v-if="index !== options.actions.length - 1"
:key="index"
role="separator"
class="c-menu__section-separator"
>
</div>
<li
v-if="actionGroups.length === 0"
:key="index"
>
No actions defined.
</li>
</div></template>
</ul>
<ul
v-else
class="c-super-menu__menu"
role="menu"
>
<li
v-for="action in options.actions"
<template v-for="(actionGroups, index) in options.actions">
<div :key="index" role="group">
<li
v-for="action in actionGroups"
:key="action.name"
role="menuitem"
:class="action.cssClass"
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
:title="action.description"
:data-testid="action.testId || false"
@click="action.onItemClicked"
@mouseover="toggleItemDescription(action)"
@mouseleave="toggleItemDescription()"
>
>
{{ action.name }}
</li>
<li v-if="options.actions.length === 0">
No actions defined.
</li>
</li>
<div
v-if="index !== options.actions.length - 1"
:key="index"
role="separator"
class="c-menu__section-separator"
></div>
<li v-if="actionGroups.length === 0" :key="index">No actions defined.</li>
</div></template
>
</ul>
<ul v-else class="c-super-menu__menu" role="menu">
<li
v-for="action in options.actions"
:key="action.name"
role="menuitem"
:class="action.cssClass"
:title="action.description"
:data-testid="action.testId || false"
@click="action.onItemClicked"
@mouseover="toggleItemDescription(action)"
@mouseleave="toggleItemDescription()"
>
{{ action.name }}
</li>
<li v-if="options.actions.length === 0">No actions defined.</li>
</ul>
<div class="c-super-menu__item-description">
<div :class="['l-item-description__icon', 'bg-' + hoveredItem.cssClass]"></div>
<div class="l-item-description__name">
{{ hoveredItem.name }}
</div>
<div class="l-item-description__description">
{{ hoveredItem.description }}
</div>
<div :class="['l-item-description__icon', 'bg-' + hoveredItem.cssClass]"></div>
<div class="l-item-description__name">
{{ hoveredItem.name }}
</div>
<div class="l-item-description__description">
{{ hoveredItem.description }}
</div>
</div>
</div>
</div>
</template>
<script>
export default {
inject: ['options'],
data: function () {
return {
hoveredItem: {}
};
},
methods: {
toggleItemDescription(action = {}) {
const hoveredItem = {
name: action.name,
description: action.description,
cssClass: action.cssClass
};
inject: ['options'],
data: function () {
return {
hoveredItem: {}
};
},
methods: {
toggleItemDescription(action = {}) {
const hoveredItem = {
name: action.name,
description: action.description,
cssClass: action.cssClass
};
this.hoveredItem = Object.assign({}, this.hoveredItem, hoveredItem);
}
this.hoveredItem = Object.assign({}, this.hoveredItem, hoveredItem);
}
}
};
</script>

View File

@@ -25,165 +25,165 @@ import SuperMenuComponent from './components/SuperMenu.vue';
import Vue from 'vue';
export const MENU_PLACEMENT = {
TOP: 'top',
TOP_LEFT: 'top-left',
TOP_RIGHT: 'top-right',
BOTTOM: 'bottom',
BOTTOM_LEFT: 'bottom-left',
BOTTOM_RIGHT: 'bottom-right',
LEFT: 'left',
RIGHT: 'right'
TOP: 'top',
TOP_LEFT: 'top-left',
TOP_RIGHT: 'top-right',
BOTTOM: 'bottom',
BOTTOM_LEFT: 'bottom-left',
BOTTOM_RIGHT: 'bottom-right',
LEFT: 'left',
RIGHT: 'right'
};
class Menu extends EventEmitter {
constructor(options) {
super();
constructor(options) {
super();
this.options = options;
if (options.onDestroy) {
this.once('destroy', options.onDestroy);
}
this.dismiss = this.dismiss.bind(this);
this.show = this.show.bind(this);
this.showMenu = this.showMenu.bind(this);
this.showSuperMenu = this.showSuperMenu.bind(this);
this.options = options;
if (options.onDestroy) {
this.once('destroy', options.onDestroy);
}
dismiss() {
this.emit('destroy');
document.body.removeChild(this.component.$el);
document.removeEventListener('click', this.dismiss);
this.component.$destroy();
this.dismiss = this.dismiss.bind(this);
this.show = this.show.bind(this);
this.showMenu = this.showMenu.bind(this);
this.showSuperMenu = this.showSuperMenu.bind(this);
}
dismiss() {
this.emit('destroy');
document.body.removeChild(this.component.$el);
document.removeEventListener('click', this.dismiss);
this.component.$destroy();
}
show() {
this.component.$mount();
document.body.appendChild(this.component.$el);
let position = this._calculatePopupPosition(this.component.$el);
this.component.$el.style.left = `${position.x}px`;
this.component.$el.style.top = `${position.y}px`;
document.addEventListener('click', this.dismiss);
}
showMenu() {
this.component = new Vue({
components: {
MenuComponent
},
provide: {
options: this.options
},
template: '<menu-component />'
});
this.show();
}
showSuperMenu() {
this.component = new Vue({
components: {
SuperMenuComponent
},
provide: {
options: this.options
},
template: '<super-menu-component />'
});
this.show();
}
/**
* @private
*/
_calculatePopupPosition(menuElement) {
let menuDimensions = menuElement.getBoundingClientRect();
if (!this.options.placement) {
this.options.placement = MENU_PLACEMENT.BOTTOM_RIGHT;
}
show() {
this.component.$mount();
document.body.appendChild(this.component.$el);
const menuPosition = this._getMenuPositionBasedOnPlacement(menuDimensions);
let position = this._calculatePopupPosition(this.component.$el);
return this._preventMenuOverflow(menuPosition, menuDimensions);
}
this.component.$el.style.left = `${position.x}px`;
this.component.$el.style.top = `${position.y}px`;
/**
* @private
*/
_getMenuPositionBasedOnPlacement(menuDimensions) {
let eventPosX = this.options.x;
let eventPosY = this.options.y;
document.addEventListener('click', this.dismiss);
// Adjust popup menu based on placement
switch (this.options.placement) {
case MENU_PLACEMENT.TOP:
eventPosX = this.options.x - Math.floor(menuDimensions.width / 2);
eventPosY = this.options.y - menuDimensions.height;
break;
case MENU_PLACEMENT.BOTTOM:
eventPosX = this.options.x - Math.floor(menuDimensions.width / 2);
break;
case MENU_PLACEMENT.LEFT:
eventPosX = this.options.x - menuDimensions.width;
eventPosY = this.options.y - Math.floor(menuDimensions.height / 2);
break;
case MENU_PLACEMENT.RIGHT:
eventPosY = this.options.y - Math.floor(menuDimensions.height / 2);
break;
case MENU_PLACEMENT.TOP_LEFT:
eventPosX = this.options.x - menuDimensions.width;
eventPosY = this.options.y - menuDimensions.height;
break;
case MENU_PLACEMENT.TOP_RIGHT:
eventPosY = this.options.y - menuDimensions.height;
break;
case MENU_PLACEMENT.BOTTOM_LEFT:
eventPosX = this.options.x - menuDimensions.width;
break;
case MENU_PLACEMENT.BOTTOM_RIGHT:
break;
}
showMenu() {
this.component = new Vue({
components: {
MenuComponent
},
provide: {
options: this.options
},
template: '<menu-component />'
});
return {
x: eventPosX,
y: eventPosY
};
}
this.show();
/**
* @private
*/
_preventMenuOverflow(menuPosition, menuDimensions) {
let { x: eventPosX, y: eventPosY } = menuPosition;
let overflowX = eventPosX + menuDimensions.width - document.body.clientWidth;
let overflowY = eventPosY + menuDimensions.height - document.body.clientHeight;
if (overflowX > 0) {
eventPosX = eventPosX - overflowX;
}
showSuperMenu() {
this.component = new Vue({
components: {
SuperMenuComponent
},
provide: {
options: this.options
},
template: '<super-menu-component />'
});
this.show();
if (overflowY > 0) {
eventPosY = eventPosY - overflowY;
}
/**
* @private
*/
_calculatePopupPosition(menuElement) {
let menuDimensions = menuElement.getBoundingClientRect();
if (!this.options.placement) {
this.options.placement = MENU_PLACEMENT.BOTTOM_RIGHT;
}
const menuPosition = this._getMenuPositionBasedOnPlacement(menuDimensions);
return this._preventMenuOverflow(menuPosition, menuDimensions);
if (eventPosX < 0) {
eventPosX = 0;
}
/**
* @private
*/
_getMenuPositionBasedOnPlacement(menuDimensions) {
let eventPosX = this.options.x;
let eventPosY = this.options.y;
// Adjust popup menu based on placement
switch (this.options.placement) {
case MENU_PLACEMENT.TOP:
eventPosX = this.options.x - Math.floor(menuDimensions.width / 2);
eventPosY = this.options.y - menuDimensions.height;
break;
case MENU_PLACEMENT.BOTTOM:
eventPosX = this.options.x - Math.floor(menuDimensions.width / 2);
break;
case MENU_PLACEMENT.LEFT:
eventPosX = this.options.x - menuDimensions.width;
eventPosY = this.options.y - Math.floor(menuDimensions.height / 2);
break;
case MENU_PLACEMENT.RIGHT:
eventPosY = this.options.y - Math.floor(menuDimensions.height / 2);
break;
case MENU_PLACEMENT.TOP_LEFT:
eventPosX = this.options.x - menuDimensions.width;
eventPosY = this.options.y - menuDimensions.height;
break;
case MENU_PLACEMENT.TOP_RIGHT:
eventPosY = this.options.y - menuDimensions.height;
break;
case MENU_PLACEMENT.BOTTOM_LEFT:
eventPosX = this.options.x - menuDimensions.width;
break;
case MENU_PLACEMENT.BOTTOM_RIGHT:
break;
}
return {
x: eventPosX,
y: eventPosY
};
if (eventPosY < 0) {
eventPosY = 0;
}
/**
* @private
*/
_preventMenuOverflow(menuPosition, menuDimensions) {
let { x: eventPosX, y: eventPosY } = menuPosition;
let overflowX = (eventPosX + menuDimensions.width) - document.body.clientWidth;
let overflowY = (eventPosY + menuDimensions.height) - document.body.clientHeight;
if (overflowX > 0) {
eventPosX = eventPosX - overflowX;
}
if (overflowY > 0) {
eventPosY = eventPosY - overflowY;
}
if (eventPosX < 0) {
eventPosX = 0;
}
if (eventPosY < 0) {
eventPosY = 0;
}
return {
x: eventPosX,
y: eventPosY
};
}
return {
x: eventPosX,
y: eventPosY
};
}
}
export default Menu;

View File

@@ -84,126 +84,126 @@ const MINIMIZE_ANIMATION_TIMEOUT = 300;
/**
* The notification service is responsible for informing the user of
* events via the use of banner notifications.
*/
*/
export default class NotificationAPI extends EventEmitter {
constructor() {
super();
/** @type {Notification[]} */
this.notifications = [];
/** @type {{severity: "info" | "alert" | "error"}} */
this.highest = { severity: "info" };
/**
* A context in which to hold the active notification and a
* handle to its timeout.
* @type {Notification | undefined}
*/
this.activeNotification = undefined;
}
constructor() {
super();
/** @type {Notification[]} */
this.notifications = [];
/** @type {{severity: "info" | "alert" | "error"}} */
this.highest = { severity: 'info' };
/**
* Info notifications are low priority informational messages for the user. They will be auto-destroy after a brief
* period of time.
* @param {string} message The message to display to the user
* @param {NotificationOptions} [options] The notification options
* @returns {Notification}
* A context in which to hold the active notification and a
* handle to its timeout.
* @type {Notification | undefined}
*/
info(message, options = {}) {
/** @type {NotificationModel} */
const notificationModel = {
message: message,
autoDismiss: true,
severity: "info",
options
};
this.activeNotification = undefined;
}
return this._notify(notificationModel);
/**
* Info notifications are low priority informational messages for the user. They will be auto-destroy after a brief
* period of time.
* @param {string} message The message to display to the user
* @param {NotificationOptions} [options] The notification options
* @returns {Notification}
*/
info(message, options = {}) {
/** @type {NotificationModel} */
const notificationModel = {
message: message,
autoDismiss: true,
severity: 'info',
options
};
return this._notify(notificationModel);
}
/**
* Present an alert to the user.
* @param {string} message The message to display to the user.
* @param {NotificationOptions} [options] object with following properties
* autoDismissTimeout: {number} in milliseconds to automatically dismisses notification
* link: {Object} Add a link to notifications for navigation
* onClick: callback function
* cssClass: css class name to add style on link
* text: text to display for link
* @returns {Notification}
*/
alert(message, options = {}) {
const notificationModel = {
message: message,
severity: 'alert',
options
};
return this._notify(notificationModel);
}
/**
* Present an error message to the user
* @param {string} message
* @param {Object} [options] object with following properties
* autoDismissTimeout: {number} in milliseconds to automatically dismisses notification
* link: {Object} Add a link to notifications for navigation
* onClick: callback function
* cssClass: css class name to add style on link
* text: text to display for link
* @returns {Notification}
*/
error(message, options = {}) {
let notificationModel = {
message: message,
severity: 'error',
options
};
return this._notify(notificationModel);
}
/**
* Create a new progress notification. These notifications will contain a progress bar.
* @param {string} message
* @param {number | 'unknown'} progressPerc A value between 0 and 100, or the string 'unknown'.
* @param {string} [progressText] Text description of progress (eg. "10 of 20 objects copied").
*/
progress(message, progressPerc, progressText) {
let notificationModel = {
message: message,
progressPerc: progressPerc,
progressText: progressText,
severity: 'info',
options: {}
};
return this._notify(notificationModel);
}
dismissAllNotifications() {
this.notifications = [];
this.emit('dismiss-all');
}
/**
* Minimize a notification. The notification will still be available
* from the notification list. Typically notifications with a
* severity of 'info' should not be minimized, but rather
* dismissed.
*
* @private
* @param {Notification | undefined} notification
*/
_minimize(notification) {
if (!notification) {
return;
}
/**
* Present an alert to the user.
* @param {string} message The message to display to the user.
* @param {NotificationOptions} [options] object with following properties
* autoDismissTimeout: {number} in milliseconds to automatically dismisses notification
* link: {Object} Add a link to notifications for navigation
* onClick: callback function
* cssClass: css class name to add style on link
* text: text to display for link
* @returns {Notification}
*/
alert(message, options = {}) {
const notificationModel = {
message: message,
severity: "alert",
options
};
//Check this is a known notification
let index = this.notifications.indexOf(notification);
return this._notify(notificationModel);
}
/**
* Present an error message to the user
* @param {string} message
* @param {Object} [options] object with following properties
* autoDismissTimeout: {number} in milliseconds to automatically dismisses notification
* link: {Object} Add a link to notifications for navigation
* onClick: callback function
* cssClass: css class name to add style on link
* text: text to display for link
* @returns {Notification}
*/
error(message, options = {}) {
let notificationModel = {
message: message,
severity: "error",
options
};
return this._notify(notificationModel);
}
/**
* Create a new progress notification. These notifications will contain a progress bar.
* @param {string} message
* @param {number | 'unknown'} progressPerc A value between 0 and 100, or the string 'unknown'.
* @param {string} [progressText] Text description of progress (eg. "10 of 20 objects copied").
*/
progress(message, progressPerc, progressText) {
let notificationModel = {
message: message,
progressPerc: progressPerc,
progressText: progressText,
severity: "info",
options: {}
};
return this._notify(notificationModel);
}
dismissAllNotifications() {
this.notifications = [];
this.emit('dismiss-all');
}
/**
* Minimize a notification. The notification will still be available
* from the notification list. Typically notifications with a
* severity of 'info' should not be minimized, but rather
* dismissed.
*
* @private
* @param {Notification | undefined} notification
*/
_minimize(notification) {
if (!notification) {
return;
}
//Check this is a known notification
let index = this.notifications.indexOf(notification);
if (this.activeTimeout) {
/*
if (this.activeTimeout) {
/*
Method can be called manually (clicking dismiss) or
automatically from an auto-timeout. this.activeTimeout
acts as a semaphore to prevent race conditions. Cancel any
@@ -211,127 +211,127 @@ export default class NotificationAPI extends EventEmitter {
has shortcut an active auto-dismiss), and clear the
semaphore.
*/
clearTimeout(this.activeTimeout);
delete this.activeTimeout;
}
if (index >= 0) {
notification.model.minimized = true;
notification.emit('minimized');
//Add a brief timeout before showing the next notification
// in order to allow the minimize animation to run through.
setTimeout(() => {
notification.emit('destroy');
this._setActiveNotification(this._selectNextNotification());
}, MINIMIZE_ANIMATION_TIMEOUT);
}
clearTimeout(this.activeTimeout);
delete this.activeTimeout;
}
/**
* Completely removes a notification. This will dismiss it from the
* message banner and remove it from the list of notifications.
* Typically only notifications with a severity of info should be
* dismissed. If you're not sure whether to dismiss or minimize a
* notification, use {@link Notification#dismissOrMinimize}.
* dismiss
*
* @private
* @param {Notification | undefined} notification
*/
_dismiss(notification) {
if (!notification) {
return;
}
//Check this is a known notification
let index = this.notifications.indexOf(notification);
if (this.activeTimeout) {
/* Method can be called manually (clicking dismiss) or
* automatically from an auto-timeout. this.activeTimeout
* acts as a semaphore to prevent race conditions. Cancel any
* timeout in progress (for the case where a manual dismiss
* has shortcut an active auto-dismiss), and clear the
* semaphore.
*/
clearTimeout(this.activeTimeout);
delete this.activeTimeout;
}
if (index >= 0) {
this.notifications.splice(index, 1);
}
this._setActiveNotification(this._selectNextNotification());
this._setHighestSeverity();
if (index >= 0) {
notification.model.minimized = true;
notification.emit('minimized');
//Add a brief timeout before showing the next notification
// in order to allow the minimize animation to run through.
setTimeout(() => {
notification.emit('destroy');
this._setActiveNotification(this._selectNextNotification());
}, MINIMIZE_ANIMATION_TIMEOUT);
}
}
/**
* Completely removes a notification. This will dismiss it from the
* message banner and remove it from the list of notifications.
* Typically only notifications with a severity of info should be
* dismissed. If you're not sure whether to dismiss or minimize a
* notification, use {@link Notification#dismissOrMinimize}.
* dismiss
*
* @private
* @param {Notification | undefined} notification
*/
_dismiss(notification) {
if (!notification) {
return;
}
/**
* Depending on the severity of the notification will selectively
* dismiss or minimize where appropriate.
*
* @private
* @param {Notification | undefined} notification
*/
_dismissOrMinimize(notification) {
let model = notification?.model;
if (model?.severity === "info") {
this._dismiss(notification);
} else {
this._minimize(notification);
}
//Check this is a known notification
let index = this.notifications.indexOf(notification);
if (this.activeTimeout) {
/* Method can be called manually (clicking dismiss) or
* automatically from an auto-timeout. this.activeTimeout
* acts as a semaphore to prevent race conditions. Cancel any
* timeout in progress (for the case where a manual dismiss
* has shortcut an active auto-dismiss), and clear the
* semaphore.
*/
clearTimeout(this.activeTimeout);
delete this.activeTimeout;
}
/**
* @private
*/
_setHighestSeverity() {
let severity = {
info: 1,
alert: 2,
error: 3
};
this.highest.severity = this.notifications.reduce((previous, notification) => {
if (severity[notification.model.severity] > severity[previous]) {
return notification.model.severity;
} else {
return previous;
}
}, "info");
if (index >= 0) {
this.notifications.splice(index, 1);
}
/**
* Notifies the user of an event. If there is a banner notification
* already active, then it will be dismissed or minimized automatically,
* and the provided notification displayed in its place.
*
* @param {NotificationModel} notificationModel The notification to
* display
* @returns {Notification} the provided notification decorated with
* functions to {@link Notification#dismiss} or {@link Notification#minimize}
*/
_notify(notificationModel) {
let notification;
let activeNotification = this.activeNotification;
this._setActiveNotification(this._selectNextNotification());
this._setHighestSeverity();
notification.emit('destroy');
}
notificationModel.severity = notificationModel.severity || "info";
notificationModel.timestamp = moment.utc().format('YYYY-MM-DD hh:mm:ss.ms');
/**
* Depending on the severity of the notification will selectively
* dismiss or minimize where appropriate.
*
* @private
* @param {Notification | undefined} notification
*/
_dismissOrMinimize(notification) {
let model = notification?.model;
if (model?.severity === 'info') {
this._dismiss(notification);
} else {
this._minimize(notification);
}
}
notification = this._createNotification(notificationModel);
/**
* @private
*/
_setHighestSeverity() {
let severity = {
info: 1,
alert: 2,
error: 3
};
this.notifications.push(notification);
this._setHighestSeverity();
this.highest.severity = this.notifications.reduce((previous, notification) => {
if (severity[notification.model.severity] > severity[previous]) {
return notification.model.severity;
} else {
return previous;
}
}, 'info');
}
/*
/**
* Notifies the user of an event. If there is a banner notification
* already active, then it will be dismissed or minimized automatically,
* and the provided notification displayed in its place.
*
* @param {NotificationModel} notificationModel The notification to
* display
* @returns {Notification} the provided notification decorated with
* functions to {@link Notification#dismiss} or {@link Notification#minimize}
*/
_notify(notificationModel) {
let notification;
let activeNotification = this.activeNotification;
notificationModel.severity = notificationModel.severity || 'info';
notificationModel.timestamp = moment.utc().format('YYYY-MM-DD hh:mm:ss.ms');
notification = this._createNotification(notificationModel);
this.notifications.push(notification);
this._setHighestSeverity();
/*
Check if there is already an active (ie. visible) notification
*/
if (!this.activeNotification && !notification?.model?.options?.minimized) {
this._setActiveNotification(notification);
} else if (!this.activeTimeout) {
/*
if (!this.activeNotification && !notification?.model?.options?.minimized) {
this._setActiveNotification(notification);
} else if (!this.activeTimeout) {
/*
If there is already an active notification, time it out. If it's
already got a timeout in progress (either because it has had
timeout forced because of a queue of messages, or it had an
@@ -341,87 +341,86 @@ export default class NotificationAPI extends EventEmitter {
This notification has been added to queue and will be
serviced as soon as possible.
*/
this.activeTimeout = setTimeout(() => {
this._dismissOrMinimize(activeNotification);
}, DEFAULT_AUTO_DISMISS_TIMEOUT);
}
return notification;
this.activeTimeout = setTimeout(() => {
this._dismissOrMinimize(activeNotification);
}, DEFAULT_AUTO_DISMISS_TIMEOUT);
}
/**
* @private
* @param {NotificationModel} notificationModel
* @returns {Notification}
*/
_createNotification(notificationModel) {
/** @type {Notification} */
let notification = new EventEmitter();
notification.model = notificationModel;
notification.dismiss = () => {
this._dismiss(notification);
};
return notification;
}
if (Object.prototype.hasOwnProperty.call(notificationModel, 'progressPerc')) {
notification.progress = (progressPerc, progressText) => {
notification.model.progressPerc = progressPerc;
notification.model.progressText = progressText;
notification.emit('progress', progressPerc, progressText);
};
}
/**
* @private
* @param {NotificationModel} notificationModel
* @returns {Notification}
*/
_createNotification(notificationModel) {
/** @type {Notification} */
let notification = new EventEmitter();
notification.model = notificationModel;
notification.dismiss = () => {
this._dismiss(notification);
};
return notification;
if (Object.prototype.hasOwnProperty.call(notificationModel, 'progressPerc')) {
notification.progress = (progressPerc, progressText) => {
notification.model.progressPerc = progressPerc;
notification.model.progressText = progressText;
notification.emit('progress', progressPerc, progressText);
};
}
/**
* @private
* @param {Notification | undefined} notification
*/
_setActiveNotification(notification) {
this.activeNotification = notification;
return notification;
}
if (!notification) {
delete this.activeTimeout;
/**
* @private
* @param {Notification | undefined} notification
*/
_setActiveNotification(notification) {
this.activeNotification = notification;
return;
}
if (!notification) {
delete this.activeTimeout;
this.emit('notification', notification);
if (notification.model.autoDismiss || this._selectNextNotification()) {
const autoDismissTimeout = notification.model.options.autoDismissTimeout
|| DEFAULT_AUTO_DISMISS_TIMEOUT;
this.activeTimeout = setTimeout(() => {
this._dismissOrMinimize(notification);
}, autoDismissTimeout);
} else {
delete this.activeTimeout;
}
return;
}
/**
* Used internally by the NotificationService
*
* @private
*/
_selectNextNotification() {
let notification;
let i = 0;
this.emit('notification', notification);
/*
if (notification.model.autoDismiss || this._selectNextNotification()) {
const autoDismissTimeout =
notification.model.options.autoDismissTimeout || DEFAULT_AUTO_DISMISS_TIMEOUT;
this.activeTimeout = setTimeout(() => {
this._dismissOrMinimize(notification);
}, autoDismissTimeout);
} else {
delete this.activeTimeout;
}
}
/**
* Used internally by the NotificationService
*
* @private
*/
_selectNextNotification() {
let notification;
let i = 0;
/*
Loop through the notifications queue and find the first one that
has not already been minimized (manually or otherwise).
*/
for (; i < this.notifications.length; i++) {
notification = this.notifications[i];
for (; i < this.notifications.length; i++) {
notification = this.notifications[i];
const isNotificationMinimized = notification.model.minimized
|| notification?.model?.options?.minimized;
const isNotificationMinimized =
notification.model.minimized || notification?.model?.options?.minimized;
if (!isNotificationMinimized
&& notification !== this.activeNotification) {
return notification;
}
}
if (!isNotificationMinimized && notification !== this.activeNotification) {
return notification;
}
}
}
}

View File

@@ -23,150 +23,150 @@
import NotificationAPI from './NotificationAPI';
describe('The Notifiation API', () => {
let notificationAPIInstance;
let defaultTimeout = 4000;
let notificationAPIInstance;
let defaultTimeout = 4000;
beforeAll(() => {
notificationAPIInstance = new NotificationAPI();
});
describe('the info method', () => {
let message = 'Example Notification Message';
let severity = 'info';
let notificationModel;
beforeAll(() => {
notificationAPIInstance = new NotificationAPI();
notificationModel = notificationAPIInstance.info(message).model;
});
describe('the info method', () => {
let message = 'Example Notification Message';
let severity = 'info';
let notificationModel;
beforeAll(() => {
notificationModel = notificationAPIInstance.info(message).model;
});
afterAll(() => {
notificationAPIInstance.dismissAllNotifications();
});
it('shows a string message with info severity', () => {
expect(notificationModel.message).toEqual(message);
expect(notificationModel.severity).toEqual(severity);
});
it('auto dismisses the notification after a brief timeout', (done) => {
window.setTimeout(() => {
expect(notificationAPIInstance.notifications.length).toEqual(0);
done();
}, defaultTimeout);
});
afterAll(() => {
notificationAPIInstance.dismissAllNotifications();
});
describe('the alert method', () => {
let message = 'Example alert message';
let severity = 'alert';
let notificationModel;
beforeAll(() => {
notificationModel = notificationAPIInstance.alert(message).model;
});
afterAll(() => {
notificationAPIInstance.dismissAllNotifications();
});
it('shows a string message, with alert severity', () => {
expect(notificationModel.message).toEqual(message);
expect(notificationModel.severity).toEqual(severity);
});
it('does not auto dismiss the notification', (done) => {
window.setTimeout(() => {
expect(notificationAPIInstance.notifications.length).toEqual(1);
done();
}, defaultTimeout);
});
it('shows a string message with info severity', () => {
expect(notificationModel.message).toEqual(message);
expect(notificationModel.severity).toEqual(severity);
});
describe('the error method', () => {
let message = 'Example error message';
let severity = 'error';
let notificationModel;
it('auto dismisses the notification after a brief timeout', (done) => {
window.setTimeout(() => {
expect(notificationAPIInstance.notifications.length).toEqual(0);
done();
}, defaultTimeout);
});
});
beforeAll(() => {
notificationModel = notificationAPIInstance.error(message).model;
});
describe('the alert method', () => {
let message = 'Example alert message';
let severity = 'alert';
let notificationModel;
afterAll(() => {
notificationAPIInstance.dismissAllNotifications();
});
it('shows a string message, with severity error', () => {
expect(notificationModel.message).toEqual(message);
expect(notificationModel.severity).toEqual(severity);
});
it('does not auto dismiss the notification', (done) => {
window.setTimeout(() => {
expect(notificationAPIInstance.notifications.length).toEqual(1);
done();
}, defaultTimeout);
});
beforeAll(() => {
notificationModel = notificationAPIInstance.alert(message).model;
});
describe('the error method notificiation', () => {
let message = 'Minimized error message';
afterAll(() => {
notificationAPIInstance.dismissAllNotifications();
});
it('is not shown if configured to show minimized', (done) => {
notificationAPIInstance.activeNotification = undefined;
notificationAPIInstance.error(message, { minimized: true });
window.setTimeout(() => {
expect(notificationAPIInstance.notifications.length).toEqual(1);
expect(notificationAPIInstance.activeNotification).toEqual(undefined);
done();
}, defaultTimeout);
});
afterAll(() => {
notificationAPIInstance.dismissAllNotifications();
});
describe('the progress method', () => {
let title = 'This is a progress notification';
let message1 = 'Example progress message 1';
let message2 = 'Example progress message 2';
let percentage1 = 50;
let percentage2 = 99.9;
let severity = 'info';
let notification;
let updatedPercentage;
let updatedMessage;
beforeAll(() => {
notification = notificationAPIInstance.progress(title, percentage1, message1);
notification.on('progress', (percentage, text) => {
updatedPercentage = percentage;
updatedMessage = text;
});
});
afterAll(() => {
notificationAPIInstance.dismissAllNotifications();
});
it ('shows a notification with a message, progress message, percentage and info severity', () => {
expect(notification.model.message).toEqual(title);
expect(notification.model.severity).toEqual(severity);
expect(notification.model.progressText).toEqual(message1);
expect(notification.model.progressPerc).toEqual(percentage1);
});
it ('allows dynamically updating the progress attributes', () => {
notification.progress(percentage2, message2);
expect(updatedPercentage).toEqual(percentage2);
expect(updatedMessage).toEqual(message2);
});
it ('allows dynamically dismissing of progress notification', () => {
notification.dismiss();
expect(notificationAPIInstance.notifications.length).toEqual(0);
});
it('shows a string message, with alert severity', () => {
expect(notificationModel.message).toEqual(message);
expect(notificationModel.severity).toEqual(severity);
});
it('does not auto dismiss the notification', (done) => {
window.setTimeout(() => {
expect(notificationAPIInstance.notifications.length).toEqual(1);
done();
}, defaultTimeout);
});
});
describe('the error method', () => {
let message = 'Example error message';
let severity = 'error';
let notificationModel;
beforeAll(() => {
notificationModel = notificationAPIInstance.error(message).model;
});
afterAll(() => {
notificationAPIInstance.dismissAllNotifications();
});
it('shows a string message, with severity error', () => {
expect(notificationModel.message).toEqual(message);
expect(notificationModel.severity).toEqual(severity);
});
it('does not auto dismiss the notification', (done) => {
window.setTimeout(() => {
expect(notificationAPIInstance.notifications.length).toEqual(1);
done();
}, defaultTimeout);
});
});
describe('the error method notificiation', () => {
let message = 'Minimized error message';
afterAll(() => {
notificationAPIInstance.dismissAllNotifications();
});
it('is not shown if configured to show minimized', (done) => {
notificationAPIInstance.activeNotification = undefined;
notificationAPIInstance.error(message, { minimized: true });
window.setTimeout(() => {
expect(notificationAPIInstance.notifications.length).toEqual(1);
expect(notificationAPIInstance.activeNotification).toEqual(undefined);
done();
}, defaultTimeout);
});
});
describe('the progress method', () => {
let title = 'This is a progress notification';
let message1 = 'Example progress message 1';
let message2 = 'Example progress message 2';
let percentage1 = 50;
let percentage2 = 99.9;
let severity = 'info';
let notification;
let updatedPercentage;
let updatedMessage;
beforeAll(() => {
notification = notificationAPIInstance.progress(title, percentage1, message1);
notification.on('progress', (percentage, text) => {
updatedPercentage = percentage;
updatedMessage = text;
});
});
afterAll(() => {
notificationAPIInstance.dismissAllNotifications();
});
it('shows a notification with a message, progress message, percentage and info severity', () => {
expect(notification.model.message).toEqual(title);
expect(notification.model.severity).toEqual(severity);
expect(notification.model.progressText).toEqual(message1);
expect(notification.model.progressPerc).toEqual(percentage1);
});
it('allows dynamically updating the progress attributes', () => {
notification.progress(percentage2, message2);
expect(updatedPercentage).toEqual(percentage2);
expect(updatedMessage).toEqual(message2);
});
it('allows dynamically dismissing of progress notification', () => {
notification.dismiss();
expect(notificationAPIInstance.notifications.length).toEqual(0);
});
});
});

View File

@@ -1,2 +1 @@
export default class ConflictError extends Error {
}
export default class ConflictError extends Error {}

File diff suppressed because it is too large Load Diff

View File

@@ -24,182 +24,180 @@
* Module defining InMemorySearchWorker. Created by deeptailor on 10/03/2019.
*/
(function () {
// An object composed of domain object IDs and models
// {id: domainObject's ID, name: domainObject's name}
const indexedDomainObjects = {};
const indexedAnnotationsByDomainObject = {};
const indexedAnnotationsByTag = {};
// An object composed of domain object IDs and models
// {id: domainObject's ID, name: domainObject's name}
const indexedDomainObjects = {};
const indexedAnnotationsByDomainObject = {};
const indexedAnnotationsByTag = {};
self.onconnect = function (e) {
const port = e.ports[0];
port.onmessage = function (event) {
const requestType = event.data.request;
if (requestType === 'index') {
indexItem(event.data.keyString, event.data.model);
} else if (requestType === 'OBJECTS') {
port.postMessage(searchForObjects(event.data));
} else if (requestType === 'ANNOTATIONS') {
port.postMessage(searchForAnnotations(event.data));
} else if (requestType === 'TAGS') {
port.postMessage(searchForTags(event.data));
} else {
throw new Error(`Unknown request ${event.data.request}`);
}
};
port.start();
self.onconnect = function (e) {
const port = e.ports[0];
port.onmessage = function (event) {
const requestType = event.data.request;
if (requestType === 'index') {
indexItem(event.data.keyString, event.data.model);
} else if (requestType === 'OBJECTS') {
port.postMessage(searchForObjects(event.data));
} else if (requestType === 'ANNOTATIONS') {
port.postMessage(searchForAnnotations(event.data));
} else if (requestType === 'TAGS') {
port.postMessage(searchForTags(event.data));
} else {
throw new Error(`Unknown request ${event.data.request}`);
}
};
self.onerror = function (error) {
//do nothing
console.error('Error on feed', error);
port.start();
};
self.onerror = function (error) {
//do nothing
console.error('Error on feed', error);
};
function indexAnnotation(objectToIndex, model) {
Object.keys(model.targets).forEach((targetID) => {
if (!indexedAnnotationsByDomainObject[targetID]) {
indexedAnnotationsByDomainObject[targetID] = [];
}
objectToIndex.targets = model.targets;
objectToIndex.tags = model.tags;
const existsInIndex = indexedAnnotationsByDomainObject[targetID].some((indexedObject) => {
return indexedObject.keyString === objectToIndex.keyString;
});
if (!existsInIndex) {
indexedAnnotationsByDomainObject[targetID].push(objectToIndex);
}
});
}
function indexTags(keyString, objectToIndex, model) {
// add new tags
model.tags.forEach((tagID) => {
if (!indexedAnnotationsByTag[tagID]) {
indexedAnnotationsByTag[tagID] = [];
}
const existsInIndex = indexedAnnotationsByTag[tagID].some((indexedObject) => {
return indexedObject.keyString === objectToIndex.keyString;
});
if (!existsInIndex) {
indexedAnnotationsByTag[tagID].push(objectToIndex);
}
});
// remove old tags
const tagsToRemoveFromIndex = Object.keys(indexedAnnotationsByTag).filter((indexedTag) => {
return !model.tags.includes(indexedTag);
});
tagsToRemoveFromIndex.forEach((tagToRemoveFromIndex) => {
indexedAnnotationsByTag[tagToRemoveFromIndex] = indexedAnnotationsByTag[
tagToRemoveFromIndex
].filter((indexedAnnotation) => {
const shouldKeep = indexedAnnotation.keyString !== keyString;
return shouldKeep;
});
});
}
function indexItem(keyString, model) {
const objectToIndex = {
type: model.type,
name: model.name,
keyString
};
if (model && model.type === 'annotation') {
if (model.targets) {
indexAnnotation(objectToIndex, model);
}
if (model.tags) {
indexTags(keyString, objectToIndex, model);
}
} else {
indexedDomainObjects[keyString] = objectToIndex;
}
}
/**
* Gets search results from the indexedItems based on provided search
* input. Returns matching results from indexedItems
*
* @param data An object which contains:
* * input: The original string which we are searching with
* * maxResults: The maximum number of search results desired
* * queryId: an id identifying this query, will be returned.
*/
function searchForObjects(data) {
let results = [];
const input = data.input.trim().toLowerCase();
const message = {
request: 'searchForObjects',
results: [],
total: 0,
queryId: data.queryId
};
function indexAnnotation(objectToIndex, model) {
Object.keys(model.targets).forEach(targetID => {
if (!indexedAnnotationsByDomainObject[targetID]) {
indexedAnnotationsByDomainObject[targetID] = [];
}
results =
Object.values(indexedDomainObjects).filter((indexedItem) => {
return indexedItem.name.toLowerCase().includes(input);
}) || [];
objectToIndex.targets = model.targets;
objectToIndex.tags = model.tags;
const existsInIndex = indexedAnnotationsByDomainObject[targetID].some(indexedObject => {
return indexedObject.keyString === objectToIndex.keyString;
message.total = results.length;
message.results = results.slice(0, data.maxResults);
return message;
}
function searchForAnnotations(data) {
let results = [];
const message = {
request: 'searchForAnnotations',
results: [],
total: 0,
queryId: data.queryId
};
results = indexedAnnotationsByDomainObject[data.input] || [];
message.total = results.length;
message.results = results.slice(0, data.maxResults);
return message;
}
function searchForTags(data) {
let results = [];
const message = {
request: 'searchForTags',
results: [],
total: 0,
queryId: data.queryId
};
if (data.input) {
data.input.forEach((matchingTag) => {
const matchingAnnotations = indexedAnnotationsByTag[matchingTag];
if (matchingAnnotations) {
matchingAnnotations.forEach((matchingAnnotation) => {
const existsInResults = results.some((indexedObject) => {
return matchingAnnotation.keyString === indexedObject.keyString;
});
if (!existsInIndex) {
indexedAnnotationsByDomainObject[targetID].push(objectToIndex);
if (!existsInResults) {
results.push(matchingAnnotation);
}
});
}
function indexTags(keyString, objectToIndex, model) {
// add new tags
model.tags.forEach(tagID => {
if (!indexedAnnotationsByTag[tagID]) {
indexedAnnotationsByTag[tagID] = [];
}
const existsInIndex = indexedAnnotationsByTag[tagID].some(indexedObject => {
return indexedObject.keyString === objectToIndex.keyString;
});
if (!existsInIndex) {
indexedAnnotationsByTag[tagID].push(objectToIndex);
}
});
// remove old tags
const tagsToRemoveFromIndex = Object.keys(indexedAnnotationsByTag).filter(indexedTag => {
return !(model.tags.includes(indexedTag));
});
tagsToRemoveFromIndex.forEach(tagToRemoveFromIndex => {
indexedAnnotationsByTag[tagToRemoveFromIndex] = indexedAnnotationsByTag[tagToRemoveFromIndex].filter(indexedAnnotation => {
const shouldKeep = indexedAnnotation.keyString !== keyString;
return shouldKeep;
});
});
}
function indexItem(keyString, model) {
const objectToIndex = {
type: model.type,
name: model.name,
keyString
};
if (model && (model.type === 'annotation')) {
if (model.targets) {
indexAnnotation(objectToIndex, model);
}
if (model.tags) {
indexTags(keyString, objectToIndex, model);
}
} else {
indexedDomainObjects[keyString] = objectToIndex;
});
}
});
}
/**
* Gets search results from the indexedItems based on provided search
* input. Returns matching results from indexedItems
*
* @param data An object which contains:
* * input: The original string which we are searching with
* * maxResults: The maximum number of search results desired
* * queryId: an id identifying this query, will be returned.
*/
function searchForObjects(data) {
let results = [];
const input = data.input.trim().toLowerCase();
const message = {
request: 'searchForObjects',
results: [],
total: 0,
queryId: data.queryId
};
message.total = results.length;
message.results = results.slice(0, data.maxResults);
results = Object.values(indexedDomainObjects).filter((indexedItem) => {
return indexedItem.name.toLowerCase().includes(input);
}) || [];
message.total = results.length;
message.results = results
.slice(0, data.maxResults);
return message;
}
function searchForAnnotations(data) {
let results = [];
const message = {
request: 'searchForAnnotations',
results: [],
total: 0,
queryId: data.queryId
};
results = indexedAnnotationsByDomainObject[data.input] || [];
message.total = results.length;
message.results = results
.slice(0, data.maxResults);
return message;
}
function searchForTags(data) {
let results = [];
const message = {
request: 'searchForTags',
results: [],
total: 0,
queryId: data.queryId
};
if (data.input) {
data.input.forEach(matchingTag => {
const matchingAnnotations = indexedAnnotationsByTag[matchingTag];
if (matchingAnnotations) {
matchingAnnotations.forEach(matchingAnnotation => {
const existsInResults = results.some(indexedObject => {
return matchingAnnotation.keyString === indexedObject.keyString;
});
if (!existsInResults) {
results.push(matchingAnnotation);
}
});
}
});
}
message.total = results.length;
message.results = results
.slice(0, data.maxResults);
return message;
}
}());
return message;
}
})();

View File

@@ -21,54 +21,54 @@
*****************************************************************************/
const DEFAULT_INTERCEPTOR_PRIORITY = 0;
export default class InterceptorRegistry {
/**
* A InterceptorRegistry maintains the definitions for different interceptors that may be invoked on domain objects.
* @interface InterceptorRegistry
* @memberof module:openmct
*/
constructor() {
this.interceptors = [];
}
/**
* @interface InterceptorDef
* @property {function} appliesTo function that determines if this interceptor should be called for the given identifier/object
* @property {function} invoke function that transforms the provided domain object and returns the transformed domain object
* @property {function} priority the priority for this interceptor. A higher number returned has more weight than a lower number
* @memberof module:openmct InterceptorRegistry#
*/
/**
* Register a new object interceptor.
*
* @param {module:openmct.InterceptorDef} interceptorDef the interceptor to add
* @method addInterceptor
* @memberof module:openmct.InterceptorRegistry#
*/
addInterceptor(interceptorDef) {
this.interceptors.push(interceptorDef);
}
/**
* Retrieve all interceptors applicable to a domain object.
* @method getInterceptors
* @returns [module:openmct.InterceptorDef] the registered interceptors for this identifier/object
* @memberof module:openmct.InterceptorRegistry#
*/
getInterceptors(identifier, object) {
function byPriority(interceptorA, interceptorB) {
let priorityA = interceptorA.priority ?? DEFAULT_INTERCEPTOR_PRIORITY;
let priorityB = interceptorB.priority ?? DEFAULT_INTERCEPTOR_PRIORITY;
return priorityB - priorityA;
}
return this.interceptors.filter(interceptor => {
return typeof interceptor.appliesTo === 'function'
&& interceptor.appliesTo(identifier, object);
}).sort(byPriority);
/**
* A InterceptorRegistry maintains the definitions for different interceptors that may be invoked on domain objects.
* @interface InterceptorRegistry
* @memberof module:openmct
*/
constructor() {
this.interceptors = [];
}
/**
* @interface InterceptorDef
* @property {function} appliesTo function that determines if this interceptor should be called for the given identifier/object
* @property {function} invoke function that transforms the provided domain object and returns the transformed domain object
* @property {function} priority the priority for this interceptor. A higher number returned has more weight than a lower number
* @memberof module:openmct InterceptorRegistry#
*/
/**
* Register a new object interceptor.
*
* @param {module:openmct.InterceptorDef} interceptorDef the interceptor to add
* @method addInterceptor
* @memberof module:openmct.InterceptorRegistry#
*/
addInterceptor(interceptorDef) {
this.interceptors.push(interceptorDef);
}
/**
* Retrieve all interceptors applicable to a domain object.
* @method getInterceptors
* @returns [module:openmct.InterceptorDef] the registered interceptors for this identifier/object
* @memberof module:openmct.InterceptorRegistry#
*/
getInterceptors(identifier, object) {
function byPriority(interceptorA, interceptorB) {
let priorityA = interceptorA.priority ?? DEFAULT_INTERCEPTOR_PRIORITY;
let priorityB = interceptorB.priority ?? DEFAULT_INTERCEPTOR_PRIORITY;
return priorityB - priorityA;
}
return this.interceptors
.filter((interceptor) => {
return (
typeof interceptor.appliesTo === 'function' && interceptor.appliesTo(identifier, object)
);
})
.sort(byPriority);
}
}

View File

@@ -40,112 +40,122 @@ const ANY_OBJECT_EVENT = 'mutation';
* @memberof module:openmct
*/
class MutableDomainObject {
constructor(eventEmitter) {
Object.defineProperties(this, {
_globalEventEmitter: {
value: eventEmitter,
// Property should not be serialized
enumerable: false
},
_instanceEventEmitter: {
value: new EventEmitter(),
// Property should not be serialized
enumerable: false
},
_observers: {
value: [],
// Property should not be serialized
enumerable: false
},
isMutable: {
value: true,
// Property should not be serialized
enumerable: false
}
});
}
$observe(path, callback) {
let fullPath = qualifiedEventName(this, path);
let eventOff =
this._globalEventEmitter.off.bind(this._globalEventEmitter, fullPath, callback);
constructor(eventEmitter) {
Object.defineProperties(this, {
_globalEventEmitter: {
value: eventEmitter,
// Property should not be serialized
enumerable: false
},
_instanceEventEmitter: {
value: new EventEmitter(),
// Property should not be serialized
enumerable: false
},
_observers: {
value: [],
// Property should not be serialized
enumerable: false
},
isMutable: {
value: true,
// Property should not be serialized
enumerable: false
}
});
}
$observe(path, callback) {
let fullPath = qualifiedEventName(this, path);
let eventOff = this._globalEventEmitter.off.bind(this._globalEventEmitter, fullPath, callback);
this._globalEventEmitter.on(fullPath, callback);
this._observers.push(eventOff);
this._globalEventEmitter.on(fullPath, callback);
this._observers.push(eventOff);
return eventOff;
}
$set(path, value) {
const oldModel = JSON.parse(JSON.stringify(this));
const oldValue = _.get(oldModel, path);
MutableDomainObject.mutateObject(this, path, value);
return eventOff;
}
$set(path, value) {
const oldModel = JSON.parse(JSON.stringify(this));
const oldValue = _.get(oldModel, path);
MutableDomainObject.mutateObject(this, path, value);
//Emit secret synchronization event first, so that all objects are in sync before subsequent events fired.
this._globalEventEmitter.emit(qualifiedEventName(this, '$_synchronize_model'), this);
//Emit secret synchronization event first, so that all objects are in sync before subsequent events fired.
this._globalEventEmitter.emit(qualifiedEventName(this, '$_synchronize_model'), this);
//Emit a general "any object" event
this._globalEventEmitter.emit(ANY_OBJECT_EVENT, this, oldModel);
//Emit wildcard event, with path so that callback knows what changed
this._globalEventEmitter.emit(qualifiedEventName(this, '*'), this, path, value, oldModel, oldValue);
//Emit a general "any object" event
this._globalEventEmitter.emit(ANY_OBJECT_EVENT, this, oldModel);
//Emit wildcard event, with path so that callback knows what changed
this._globalEventEmitter.emit(
qualifiedEventName(this, '*'),
this,
path,
value,
oldModel,
oldValue
);
//Emit events specific to properties affected
let parentPropertiesList = path.split('.');
for (let index = parentPropertiesList.length; index > 0; index--) {
let parentPropertyPath = parentPropertiesList.slice(0, index).join('.');
this._globalEventEmitter.emit(qualifiedEventName(this, parentPropertyPath), _.get(this, parentPropertyPath), _.get(oldModel, parentPropertyPath));
}
//TODO: Emit events for listeners of child properties when parent changes.
// Do it at observer time - also register observers for parent attribute path.
//Emit events specific to properties affected
let parentPropertiesList = path.split('.');
for (let index = parentPropertiesList.length; index > 0; index--) {
let parentPropertyPath = parentPropertiesList.slice(0, index).join('.');
this._globalEventEmitter.emit(
qualifiedEventName(this, parentPropertyPath),
_.get(this, parentPropertyPath),
_.get(oldModel, parentPropertyPath)
);
}
$refresh(model) {
//TODO: Currently we are updating the entire object.
// In the future we could update a specific property of the object using the 'path' parameter.
this._globalEventEmitter.emit(qualifiedEventName(this, '$_synchronize_model'), model);
//TODO: Emit events for listeners of child properties when parent changes.
// Do it at observer time - also register observers for parent attribute path.
}
//Emit wildcard event, with path so that callback knows what changed
this._globalEventEmitter.emit(qualifiedEventName(this, '*'), this);
$refresh(model) {
//TODO: Currently we are updating the entire object.
// In the future we could update a specific property of the object using the 'path' parameter.
this._globalEventEmitter.emit(qualifiedEventName(this, '$_synchronize_model'), model);
//Emit wildcard event, with path so that callback knows what changed
this._globalEventEmitter.emit(qualifiedEventName(this, '*'), this);
}
$on(event, callback) {
this._instanceEventEmitter.on(event, callback);
return () => this._instanceEventEmitter.off(event, callback);
}
$destroy() {
while (this._observers.length > 0) {
const observer = this._observers.pop();
observer();
}
$on(event, callback) {
this._instanceEventEmitter.on(event, callback);
this._instanceEventEmitter.emit('$_destroy');
}
return () => this._instanceEventEmitter.off(event, callback);
}
$destroy() {
while (this._observers.length > 0) {
const observer = this._observers.pop();
observer();
}
static createMutable(object, mutationTopic) {
let mutable = Object.create(new MutableDomainObject(mutationTopic));
Object.assign(mutable, object);
this._instanceEventEmitter.emit('$_destroy');
mutable.$observe('$_synchronize_model', (updatedObject) => {
let clone = JSON.parse(JSON.stringify(updatedObject));
utils.refresh(mutable, clone);
});
return mutable;
}
static mutateObject(object, path, value) {
if (path !== 'persisted') {
_.set(object, 'modified', Date.now());
}
static createMutable(object, mutationTopic) {
let mutable = Object.create(new MutableDomainObject(mutationTopic));
Object.assign(mutable, object);
mutable.$observe('$_synchronize_model', (updatedObject) => {
let clone = JSON.parse(JSON.stringify(updatedObject));
utils.refresh(mutable, clone);
});
return mutable;
}
static mutateObject(object, path, value) {
if (path !== 'persisted') {
_.set(object, 'modified', Date.now());
}
_.set(object, path, value);
}
_.set(object, path, value);
}
}
function qualifiedEventName(object, eventName) {
let keystring = utils.makeKeyString(object.identifier);
let keystring = utils.makeKeyString(object.identifier);
return [keystring, eventName].join(':');
return [keystring, eventName].join(':');
}
export default MutableDomainObject;

File diff suppressed because it is too large Load Diff

View File

@@ -1,223 +1,223 @@
import { createOpenMct, resetApplicationState } from '../../utils/testing';
describe("The Object API Search Function", () => {
describe("The infrastructure", () => {
const MOCK_PROVIDER_KEY = 'mockProvider';
const ANOTHER_MOCK_PROVIDER_KEY = 'anotherMockProvider';
const MOCK_PROVIDER_SEARCH_DELAY = 15000;
const ANOTHER_MOCK_PROVIDER_SEARCH_DELAY = 20000;
const TOTAL_TIME_ELAPSED = 21000;
const BASE_TIME = new Date(2021, 0, 1);
describe('The Object API Search Function', () => {
describe('The infrastructure', () => {
const MOCK_PROVIDER_KEY = 'mockProvider';
const ANOTHER_MOCK_PROVIDER_KEY = 'anotherMockProvider';
const MOCK_PROVIDER_SEARCH_DELAY = 15000;
const ANOTHER_MOCK_PROVIDER_SEARCH_DELAY = 20000;
const TOTAL_TIME_ELAPSED = 21000;
const BASE_TIME = new Date(2021, 0, 1);
let mockObjectProvider;
let anotherMockObjectProvider;
let openmct;
let mockObjectProvider;
let anotherMockObjectProvider;
let openmct;
beforeEach((done) => {
openmct = createOpenMct();
beforeEach((done) => {
openmct = createOpenMct();
mockObjectProvider = jasmine.createSpyObj("mock object provider", [
"search", "supportsSearchType"
]);
anotherMockObjectProvider = jasmine.createSpyObj("another mock object provider", [
"search", "supportsSearchType"
]);
openmct.objects.addProvider('objects', mockObjectProvider);
openmct.objects.addProvider('other-objects', anotherMockObjectProvider);
mockObjectProvider.supportsSearchType.and.callFake(() => {
return true;
});
mockObjectProvider.search.and.callFake(() => {
return new Promise(resolve => {
const mockProviderSearch = {
name: MOCK_PROVIDER_KEY,
start: new Date()
};
mockObjectProvider = jasmine.createSpyObj('mock object provider', [
'search',
'supportsSearchType'
]);
anotherMockObjectProvider = jasmine.createSpyObj('another mock object provider', [
'search',
'supportsSearchType'
]);
openmct.objects.addProvider('objects', mockObjectProvider);
openmct.objects.addProvider('other-objects', anotherMockObjectProvider);
mockObjectProvider.supportsSearchType.and.callFake(() => {
return true;
});
mockObjectProvider.search.and.callFake(() => {
return new Promise((resolve) => {
const mockProviderSearch = {
name: MOCK_PROVIDER_KEY,
start: new Date()
};
setTimeout(() => {
mockProviderSearch.end = new Date();
setTimeout(() => {
mockProviderSearch.end = new Date();
return resolve(mockProviderSearch);
}, MOCK_PROVIDER_SEARCH_DELAY);
});
});
anotherMockObjectProvider.supportsSearchType.and.callFake(() => {
return true;
});
anotherMockObjectProvider.search.and.callFake(() => {
return new Promise(resolve => {
const anotherMockProviderSearch = {
name: ANOTHER_MOCK_PROVIDER_KEY,
start: new Date()
};
setTimeout(() => {
anotherMockProviderSearch.end = new Date();
return resolve(anotherMockProviderSearch);
}, ANOTHER_MOCK_PROVIDER_SEARCH_DELAY);
});
});
openmct.on('start', () => {
done();
});
openmct.startHeadless();
return resolve(mockProviderSearch);
}, MOCK_PROVIDER_SEARCH_DELAY);
});
afterEach(async () => {
await resetApplicationState(openmct);
});
it("uses each objects given provider's search function", () => {
openmct.objects.search('foo');
expect(mockObjectProvider.search).toHaveBeenCalled();
});
it("provides each providers results as promises that resolve in parallel", async () => {
jasmine.clock().install();
jasmine.clock().mockDate(BASE_TIME);
const resultsPromises = openmct.objects.search('foo');
jasmine.clock().tick(TOTAL_TIME_ELAPSED);
const results = await Promise.all(resultsPromises);
const mockProviderResults = results.find(
result => result.name === MOCK_PROVIDER_KEY
);
const anotherMockProviderResults = results.find(
result => result.name === ANOTHER_MOCK_PROVIDER_KEY
);
const mockProviderStart = mockProviderResults.start.getTime();
const mockProviderEnd = mockProviderResults.end.getTime();
const anotherMockProviderStart = anotherMockProviderResults.start.getTime();
const anotherMockProviderEnd = anotherMockProviderResults.end.getTime();
const searchElapsedTime = Math.max(mockProviderEnd, anotherMockProviderEnd)
- Math.min(mockProviderEnd, anotherMockProviderEnd);
});
anotherMockObjectProvider.supportsSearchType.and.callFake(() => {
return true;
});
anotherMockObjectProvider.search.and.callFake(() => {
return new Promise((resolve) => {
const anotherMockProviderSearch = {
name: ANOTHER_MOCK_PROVIDER_KEY,
start: new Date()
};
expect(mockProviderStart).toBeLessThan(anotherMockProviderEnd);
expect(anotherMockProviderStart).toBeLessThan(mockProviderEnd);
expect(searchElapsedTime).toBeLessThan(
MOCK_PROVIDER_SEARCH_DELAY
+ ANOTHER_MOCK_PROVIDER_SEARCH_DELAY
);
setTimeout(() => {
anotherMockProviderSearch.end = new Date();
jasmine.clock().uninstall();
return resolve(anotherMockProviderSearch);
}, ANOTHER_MOCK_PROVIDER_SEARCH_DELAY);
});
});
openmct.on('start', () => {
done();
});
openmct.startHeadless();
});
afterEach(async () => {
await resetApplicationState(openmct);
});
it("uses each objects given provider's search function", () => {
openmct.objects.search('foo');
expect(mockObjectProvider.search).toHaveBeenCalled();
});
it('provides each providers results as promises that resolve in parallel', async () => {
jasmine.clock().install();
jasmine.clock().mockDate(BASE_TIME);
const resultsPromises = openmct.objects.search('foo');
jasmine.clock().tick(TOTAL_TIME_ELAPSED);
const results = await Promise.all(resultsPromises);
const mockProviderResults = results.find((result) => result.name === MOCK_PROVIDER_KEY);
const anotherMockProviderResults = results.find(
(result) => result.name === ANOTHER_MOCK_PROVIDER_KEY
);
const mockProviderStart = mockProviderResults.start.getTime();
const mockProviderEnd = mockProviderResults.end.getTime();
const anotherMockProviderStart = anotherMockProviderResults.start.getTime();
const anotherMockProviderEnd = anotherMockProviderResults.end.getTime();
const searchElapsedTime =
Math.max(mockProviderEnd, anotherMockProviderEnd) -
Math.min(mockProviderEnd, anotherMockProviderEnd);
expect(mockProviderStart).toBeLessThan(anotherMockProviderEnd);
expect(anotherMockProviderStart).toBeLessThan(mockProviderEnd);
expect(searchElapsedTime).toBeLessThan(
MOCK_PROVIDER_SEARCH_DELAY + ANOTHER_MOCK_PROVIDER_SEARCH_DELAY
);
jasmine.clock().uninstall();
});
});
describe('The in-memory search indexer', () => {
let openmct;
let mockDomainObject1;
let mockIdentifier1;
let mockDomainObject2;
let mockIdentifier2;
let mockDomainObject3;
let mockIdentifier3;
beforeEach((done) => {
openmct = createOpenMct();
const defaultObjectProvider = openmct.objects.getProvider({
key: '',
namespace: ''
});
openmct.objects.addProvider('foo', defaultObjectProvider);
spyOn(openmct.objects.inMemorySearchProvider, 'search').and.callThrough();
spyOn(openmct.objects.inMemorySearchProvider, 'localSearchForObjects').and.callThrough();
openmct.on('start', async () => {
mockIdentifier1 = {
key: 'some-object',
namespace: 'foo'
};
mockDomainObject1 = {
type: 'clock',
name: 'fooRabbit',
identifier: mockIdentifier1
};
mockIdentifier2 = {
key: 'some-other-object',
namespace: 'foo'
};
mockDomainObject2 = {
type: 'clock',
name: 'fooBear',
identifier: mockIdentifier2
};
mockIdentifier3 = {
key: 'yet-another-object',
namespace: 'foo'
};
mockDomainObject3 = {
type: 'clock',
name: 'redBear',
identifier: mockIdentifier3
};
await openmct.objects.inMemorySearchProvider.index(mockDomainObject1);
await openmct.objects.inMemorySearchProvider.index(mockDomainObject2);
await openmct.objects.inMemorySearchProvider.index(mockDomainObject3);
done();
});
openmct.startHeadless();
});
describe("The in-memory search indexer", () => {
let openmct;
let mockDomainObject1;
let mockIdentifier1;
let mockDomainObject2;
let mockIdentifier2;
let mockDomainObject3;
let mockIdentifier3;
beforeEach((done) => {
openmct = createOpenMct();
const defaultObjectProvider = openmct.objects.getProvider({
key: '',
namespace: ''
});
openmct.objects.addProvider('foo', defaultObjectProvider);
spyOn(openmct.objects.inMemorySearchProvider, "search").and.callThrough();
spyOn(openmct.objects.inMemorySearchProvider, "localSearchForObjects").and.callThrough();
openmct.on('start', async () => {
mockIdentifier1 = {
key: 'some-object',
namespace: 'foo'
};
mockDomainObject1 = {
type: 'clock',
name: 'fooRabbit',
identifier: mockIdentifier1
};
mockIdentifier2 = {
key: 'some-other-object',
namespace: 'foo'
};
mockDomainObject2 = {
type: 'clock',
name: 'fooBear',
identifier: mockIdentifier2
};
mockIdentifier3 = {
key: 'yet-another-object',
namespace: 'foo'
};
mockDomainObject3 = {
type: 'clock',
name: 'redBear',
identifier: mockIdentifier3
};
await openmct.objects.inMemorySearchProvider.index(mockDomainObject1);
await openmct.objects.inMemorySearchProvider.index(mockDomainObject2);
await openmct.objects.inMemorySearchProvider.index(mockDomainObject3);
done();
});
openmct.startHeadless();
});
afterEach(async () => {
await resetApplicationState(openmct);
});
it("can provide indexing without a provider", () => {
openmct.objects.search('foo');
expect(openmct.objects.inMemorySearchProvider.search).toHaveBeenCalled();
});
it("can do partial search", async () => {
const searchPromises = openmct.objects.search('foo');
const searchResults = await Promise.all(searchPromises);
expect(searchResults[0].length).toBe(2);
});
it("returns nothing when appropriate", async () => {
const searchPromises = openmct.objects.search('laser');
const searchResults = await Promise.all(searchPromises);
expect(searchResults[0].length).toBe(0);
});
it("returns exact matches", async () => {
const searchPromises = openmct.objects.search('redBear');
const searchResults = await Promise.all(searchPromises);
expect(searchResults[0].length).toBe(1);
});
describe("Without Shared Workers", () => {
let sharedWorkerToRestore;
beforeEach(async () => {
// use local worker
sharedWorkerToRestore = openmct.objects.inMemorySearchProvider.worker;
openmct.objects.inMemorySearchProvider.worker = null;
// reindex locally
await openmct.objects.inMemorySearchProvider.index(mockDomainObject1);
await openmct.objects.inMemorySearchProvider.index(mockDomainObject2);
await openmct.objects.inMemorySearchProvider.index(mockDomainObject3);
});
afterEach(() => {
openmct.objects.inMemorySearchProvider.worker = sharedWorkerToRestore;
});
it("calls local search", () => {
openmct.objects.search('foo');
expect(openmct.objects.inMemorySearchProvider.localSearchForObjects).toHaveBeenCalled();
});
it("can do partial search", async () => {
const searchPromises = openmct.objects.search('foo');
const searchResults = await Promise.all(searchPromises);
expect(searchResults[0].length).toBe(2);
});
it("returns nothing when appropriate", async () => {
const searchPromises = openmct.objects.search('laser');
const searchResults = await Promise.all(searchPromises);
expect(searchResults[0].length).toBe(0);
});
it("returns exact matches", async () => {
const searchPromises = openmct.objects.search('redBear');
const searchResults = await Promise.all(searchPromises);
expect(searchResults[0].length).toBe(1);
});
});
afterEach(async () => {
await resetApplicationState(openmct);
});
it('can provide indexing without a provider', () => {
openmct.objects.search('foo');
expect(openmct.objects.inMemorySearchProvider.search).toHaveBeenCalled();
});
it('can do partial search', async () => {
const searchPromises = openmct.objects.search('foo');
const searchResults = await Promise.all(searchPromises);
expect(searchResults[0].length).toBe(2);
});
it('returns nothing when appropriate', async () => {
const searchPromises = openmct.objects.search('laser');
const searchResults = await Promise.all(searchPromises);
expect(searchResults[0].length).toBe(0);
});
it('returns exact matches', async () => {
const searchPromises = openmct.objects.search('redBear');
const searchResults = await Promise.all(searchPromises);
expect(searchResults[0].length).toBe(1);
});
describe('Without Shared Workers', () => {
let sharedWorkerToRestore;
beforeEach(async () => {
// use local worker
sharedWorkerToRestore = openmct.objects.inMemorySearchProvider.worker;
openmct.objects.inMemorySearchProvider.worker = null;
// reindex locally
await openmct.objects.inMemorySearchProvider.index(mockDomainObject1);
await openmct.objects.inMemorySearchProvider.index(mockDomainObject2);
await openmct.objects.inMemorySearchProvider.index(mockDomainObject3);
});
afterEach(() => {
openmct.objects.inMemorySearchProvider.worker = sharedWorkerToRestore;
});
it('calls local search', () => {
openmct.objects.search('foo');
expect(openmct.objects.inMemorySearchProvider.localSearchForObjects).toHaveBeenCalled();
});
it('can do partial search', async () => {
const searchPromises = openmct.objects.search('foo');
const searchResults = await Promise.all(searchPromises);
expect(searchResults[0].length).toBe(2);
});
it('returns nothing when appropriate', async () => {
const searchPromises = openmct.objects.search('laser');
const searchResults = await Promise.all(searchPromises);
expect(searchResults[0].length).toBe(0);
});
it('returns exact matches', async () => {
const searchPromises = openmct.objects.search('redBear');
const searchResults = await Promise.all(searchPromises);
expect(searchResults[0].length).toBe(1);
});
});
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -21,41 +21,41 @@
*****************************************************************************/
class RootObjectProvider {
constructor(rootRegistry) {
if (!RootObjectProvider.instance) {
this.rootRegistry = rootRegistry;
this.rootObject = {
identifier: {
key: "ROOT",
namespace: ""
},
name: 'Open MCT',
type: 'root',
composition: []
};
RootObjectProvider.instance = this;
} else if (rootRegistry) {
// if called twice, update instance rootRegistry
RootObjectProvider.instance.rootRegistry = rootRegistry;
}
return RootObjectProvider.instance; // eslint-disable-line no-constructor-return
constructor(rootRegistry) {
if (!RootObjectProvider.instance) {
this.rootRegistry = rootRegistry;
this.rootObject = {
identifier: {
key: 'ROOT',
namespace: ''
},
name: 'Open MCT',
type: 'root',
composition: []
};
RootObjectProvider.instance = this;
} else if (rootRegistry) {
// if called twice, update instance rootRegistry
RootObjectProvider.instance.rootRegistry = rootRegistry;
}
updateName(name) {
this.rootObject.name = name;
}
return RootObjectProvider.instance; // eslint-disable-line no-constructor-return
}
async get() {
let roots = await this.rootRegistry.getRoots();
this.rootObject.composition = roots;
updateName(name) {
this.rootObject.name = name;
}
return this.rootObject;
}
async get() {
let roots = await this.rootRegistry.getRoots();
this.rootObject.composition = roots;
return this.rootObject;
}
}
function instance(rootRegistry) {
return new RootObjectProvider(rootRegistry);
return new RootObjectProvider(rootRegistry);
}
export default instance;

View File

@@ -23,40 +23,38 @@
import utils from './object-utils';
export default class RootRegistry {
constructor(openmct) {
this._rootItems = [];
this._openmct = openmct;
}
constructor(openmct) {
this._rootItems = [];
this._openmct = openmct;
getRoots() {
const sortedItems = this._rootItems.sort((a, b) => b.priority - a.priority);
const promises = sortedItems.map((rootItem) => rootItem.provider());
return Promise.all(promises).then((rootItems) => rootItems.flat());
}
addRoot(rootItem, priority) {
if (!this._isValid(rootItem)) {
return;
}
getRoots() {
const sortedItems = this._rootItems.sort((a, b) => b.priority - a.priority);
const promises = sortedItems.map((rootItem) => rootItem.provider());
this._rootItems.push({
priority: priority || this._openmct.priority.DEFAULT,
provider: typeof rootItem === 'function' ? rootItem : () => rootItem
});
}
return Promise.all(promises).then(rootItems => rootItems.flat());
_isValid(rootItem) {
if (utils.isIdentifier(rootItem) || typeof rootItem === 'function') {
return true;
}
addRoot(rootItem, priority) {
if (!this._isValid(rootItem)) {
return;
}
this._rootItems.push({
priority: priority || this._openmct.priority.DEFAULT,
provider: typeof rootItem === 'function' ? rootItem : () => rootItem
});
if (Array.isArray(rootItem)) {
return rootItem.every(utils.isIdentifier);
}
_isValid(rootItem) {
if (utils.isIdentifier(rootItem) || typeof rootItem === 'function') {
return true;
}
if (Array.isArray(rootItem)) {
return rootItem.every(utils.isIdentifier);
}
return false;
}
return false;
}
}

View File

@@ -21,66 +21,66 @@
*****************************************************************************/
export default class Transaction {
constructor(objectAPI) {
this.dirtyObjects = {};
this.objectAPI = objectAPI;
}
constructor(objectAPI) {
this.dirtyObjects = {};
this.objectAPI = objectAPI;
}
add(object) {
const key = this.objectAPI.makeKeyString(object.identifier);
add(object) {
const key = this.objectAPI.makeKeyString(object.identifier);
this.dirtyObjects[key] = object;
}
this.dirtyObjects[key] = object;
}
cancel() {
return this._clear();
}
cancel() {
return this._clear();
}
commit() {
const promiseArray = [];
const save = this.objectAPI.save.bind(this.objectAPI);
commit() {
const promiseArray = [];
const save = this.objectAPI.save.bind(this.objectAPI);
Object.values(this.dirtyObjects).forEach(object => {
promiseArray.push(this.createDirtyObjectPromise(object, save));
});
Object.values(this.dirtyObjects).forEach((object) => {
promiseArray.push(this.createDirtyObjectPromise(object, save));
});
return Promise.all(promiseArray);
}
return Promise.all(promiseArray);
}
createDirtyObjectPromise(object, action) {
return new Promise((resolve, reject) => {
action(object)
.then((success) => {
const key = this.objectAPI.makeKeyString(object.identifier);
createDirtyObjectPromise(object, action) {
return new Promise((resolve, reject) => {
action(object)
.then((success) => {
const key = this.objectAPI.makeKeyString(object.identifier);
delete this.dirtyObjects[key];
resolve(success);
})
.catch(reject);
});
}
delete this.dirtyObjects[key];
resolve(success);
})
.catch(reject);
});
}
getDirtyObject(identifier) {
let dirtyObject;
getDirtyObject(identifier) {
let dirtyObject;
Object.values(this.dirtyObjects).forEach(object => {
const areIdsEqual = this.objectAPI.areIdsEqual(object.identifier, identifier);
if (areIdsEqual) {
dirtyObject = object;
}
});
Object.values(this.dirtyObjects).forEach((object) => {
const areIdsEqual = this.objectAPI.areIdsEqual(object.identifier, identifier);
if (areIdsEqual) {
dirtyObject = object;
}
});
return dirtyObject;
}
return dirtyObject;
}
_clear() {
const promiseArray = [];
const refresh = this.objectAPI.refresh.bind(this.objectAPI);
_clear() {
const promiseArray = [];
const refresh = this.objectAPI.refresh.bind(this.objectAPI);
Object.values(this.dirtyObjects).forEach(object => {
promiseArray.push(this.createDirtyObjectPromise(object, refresh));
});
Object.values(this.dirtyObjects).forEach((object) => {
promiseArray.push(this.createDirtyObjectPromise(object, refresh));
});
return Promise.all(promiseArray);
}
return Promise.all(promiseArray);
}
}

View File

@@ -1,111 +1,116 @@
import Transaction from "./Transaction";
import Transaction from './Transaction';
import utils from 'objectUtils';
let openmct = {};
let objectAPI;
let transaction;
describe("Transaction Class", () => {
beforeEach(() => {
objectAPI = {
makeKeyString: (identifier) => utils.makeKeyString(identifier),
save: () => Promise.resolve(true),
mutate: (object, prop, value) => {
object[prop] = value;
describe('Transaction Class', () => {
beforeEach(() => {
objectAPI = {
makeKeyString: (identifier) => utils.makeKeyString(identifier),
save: () => Promise.resolve(true),
mutate: (object, prop, value) => {
object[prop] = value;
return object;
},
refresh: (object) => Promise.resolve(object),
areIdsEqual: (...identifiers) => {
return identifiers.map(utils.parseKeyString)
.every(identifier => {
return identifier === identifiers[0]
|| (identifier.namespace === identifiers[0].namespace
&& identifier.key === identifiers[0].key);
});
}
};
return object;
},
refresh: (object) => Promise.resolve(object),
areIdsEqual: (...identifiers) => {
return identifiers.map(utils.parseKeyString).every((identifier) => {
return (
identifier === identifiers[0] ||
(identifier.namespace === identifiers[0].namespace &&
identifier.key === identifiers[0].key)
);
});
}
};
transaction = new Transaction(objectAPI);
transaction = new Transaction(objectAPI);
openmct.editor = {
isEditing: () => true
};
});
openmct.editor = {
isEditing: () => true
};
});
it('has no dirty objects', () => {
it('has no dirty objects', () => {
expect(Object.keys(transaction.dirtyObjects).length).toEqual(0);
});
it('add(), adds object to dirtyObjects', () => {
const mockDomainObjects = createMockDomainObjects();
transaction.add(mockDomainObjects[0]);
expect(Object.keys(transaction.dirtyObjects).length).toEqual(1);
});
it('cancel(), clears all dirtyObjects', (done) => {
const mockDomainObjects = createMockDomainObjects(3);
mockDomainObjects.forEach(transaction.add.bind(transaction));
expect(Object.keys(transaction.dirtyObjects).length).toEqual(3);
transaction
.cancel()
.then((success) => {
expect(Object.keys(transaction.dirtyObjects).length).toEqual(0);
});
})
.finally(done);
});
it('add(), adds object to dirtyObjects', () => {
const mockDomainObjects = createMockDomainObjects();
transaction.add(mockDomainObjects[0]);
expect(Object.keys(transaction.dirtyObjects).length).toEqual(1);
});
it('commit(), saves all dirtyObjects', (done) => {
const mockDomainObjects = createMockDomainObjects(3);
mockDomainObjects.forEach(transaction.add.bind(transaction));
it('cancel(), clears all dirtyObjects', (done) => {
const mockDomainObjects = createMockDomainObjects(3);
mockDomainObjects.forEach(transaction.add.bind(transaction));
expect(Object.keys(transaction.dirtyObjects).length).toEqual(3);
transaction.cancel()
.then(success => {
expect(Object.keys(transaction.dirtyObjects).length).toEqual(0);
}).finally(done);
});
it('commit(), saves all dirtyObjects', (done) => {
const mockDomainObjects = createMockDomainObjects(3);
mockDomainObjects.forEach(transaction.add.bind(transaction));
expect(Object.keys(transaction.dirtyObjects).length).toEqual(3);
spyOn(objectAPI, 'save').and.callThrough();
transaction.commit()
.then(success => {
expect(Object.keys(transaction.dirtyObjects).length).toEqual(0);
expect(objectAPI.save.calls.count()).toEqual(3);
}).finally(done);
});
it('getDirtyObject(), returns correct dirtyObject', () => {
const mockDomainObjects = createMockDomainObjects();
transaction.add(mockDomainObjects[0]);
expect(Object.keys(transaction.dirtyObjects).length).toEqual(1);
const dirtyObject = transaction.getDirtyObject(mockDomainObjects[0].identifier);
expect(dirtyObject).toEqual(mockDomainObjects[0]);
});
it('getDirtyObject(), returns empty dirtyObject for no active transaction', () => {
const mockDomainObjects = createMockDomainObjects();
expect(Object.keys(transaction.dirtyObjects).length).toEqual(3);
spyOn(objectAPI, 'save').and.callThrough();
transaction
.commit()
.then((success) => {
expect(Object.keys(transaction.dirtyObjects).length).toEqual(0);
const dirtyObject = transaction.getDirtyObject(mockDomainObjects[0].identifier);
expect(objectAPI.save.calls.count()).toEqual(3);
})
.finally(done);
});
expect(dirtyObject).toEqual(undefined);
});
it('getDirtyObject(), returns correct dirtyObject', () => {
const mockDomainObjects = createMockDomainObjects();
transaction.add(mockDomainObjects[0]);
expect(Object.keys(transaction.dirtyObjects).length).toEqual(1);
const dirtyObject = transaction.getDirtyObject(mockDomainObjects[0].identifier);
expect(dirtyObject).toEqual(mockDomainObjects[0]);
});
it('getDirtyObject(), returns empty dirtyObject for no active transaction', () => {
const mockDomainObjects = createMockDomainObjects();
expect(Object.keys(transaction.dirtyObjects).length).toEqual(0);
const dirtyObject = transaction.getDirtyObject(mockDomainObjects[0].identifier);
expect(dirtyObject).toEqual(undefined);
});
});
function createMockDomainObjects(size = 1) {
const objects = [];
const objects = [];
while (size > 0) {
const mockDomainObject = {
identifier: {
namespace: 'test-namespace',
key: `test-key-${size}`
},
name: `test object ${size}`,
type: 'test-type'
};
while (size > 0) {
const mockDomainObject = {
identifier: {
namespace: 'test-namespace',
key: `test-key-${size}`
},
name: `test object ${size}`,
type: 'test-type'
};
objects.push(mockDomainObject);
objects.push(mockDomainObject);
size--;
}
size--;
}
return objects;
return objects;
}

View File

@@ -20,169 +20,163 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
define([], function () {
/**
* Utility for checking if a thing is an Open MCT Identifier.
* @private
*/
function isIdentifier(thing) {
return (
typeof thing === 'object' &&
Object.prototype.hasOwnProperty.call(thing, 'key') &&
Object.prototype.hasOwnProperty.call(thing, 'namespace')
);
}
], function (
/**
* Utility for checking if a thing is a key string. Not perfect.
* @private
*/
function isKeyString(thing) {
return typeof thing === 'string';
}
) {
/**
* Utility for checking if a thing is an Open MCT Identifier.
* @private
*/
function isIdentifier(thing) {
return typeof thing === 'object'
&& Object.prototype.hasOwnProperty.call(thing, 'key')
&& Object.prototype.hasOwnProperty.call(thing, 'namespace');
/**
* Convert a keyString into an Open MCT Identifier, ex:
* 'scratch:root' ==> {namespace: 'scratch', key: 'root'}
*
* Idempotent.
*
* @param keyString
* @returns identifier
*/
function parseKeyString(keyString) {
if (isIdentifier(keyString)) {
return keyString;
}
/**
* Utility for checking if a thing is a key string. Not perfect.
* @private
*/
function isKeyString(thing) {
return typeof thing === 'string';
let namespace = '';
let key = keyString;
for (let i = 0; i < key.length; i++) {
if (key[i] === '\\' && key[i + 1] === ':') {
i++; // skip escape character.
} else if (key[i] === ':') {
key = key.slice(i + 1);
break;
}
namespace += key[i];
}
/**
* Convert a keyString into an Open MCT Identifier, ex:
* 'scratch:root' ==> {namespace: 'scratch', key: 'root'}
*
* Idempotent.
*
* @param keyString
* @returns identifier
*/
function parseKeyString(keyString) {
if (isIdentifier(keyString)) {
return keyString;
}
let namespace = '';
let key = keyString;
for (let i = 0; i < key.length; i++) {
if (key[i] === "\\" && key[i + 1] === ":") {
i++; // skip escape character.
} else if (key[i] === ":") {
key = key.slice(i + 1);
break;
}
namespace += key[i];
}
if (keyString === namespace) {
namespace = '';
}
return {
namespace: namespace,
key: key
};
}
/**
* Convert an Open MCT Identifier into a keyString, ex:
* {namespace: 'scratch', key: 'root'} ==> 'scratch:root'
*
* Idempotent
*
* @param identifier
* @returns keyString
*/
function makeKeyString(identifier) {
if (!identifier) {
throw new Error("Cannot make key string from null identifier");
}
if (isKeyString(identifier)) {
return identifier;
}
if (!identifier.namespace) {
return identifier.key;
}
return [
identifier.namespace.replace(/:/g, '\\:'),
identifier.key
].join(':');
}
/**
* Convert a new domain object into an old format model, removing the
* identifier and converting the composition array from Open MCT Identifiers
* to old format keyStrings.
*
* @param domainObject
* @returns oldFormatModel
*/
function toOldFormat(model) {
model = JSON.parse(JSON.stringify(model));
delete model.identifier;
if (model.composition) {
model.composition = model.composition.map(makeKeyString);
}
return model;
}
/**
* Convert an old format domain object model into a new format domain
* object. Adds an identifier using the provided keyString, and converts
* the composition array to utilize Open MCT Identifiers.
*
* @param model
* @param keyString
* @returns domainObject
*/
function toNewFormat(model, keyString) {
model = JSON.parse(JSON.stringify(model));
model.identifier = parseKeyString(keyString);
if (model.composition) {
model.composition = model.composition.map(parseKeyString);
}
return model;
}
/**
* Compare two Open MCT Identifiers, returning true if they are equal.
*
* @param identifier
* @param otherIdentifier
* @returns Boolean true if identifiers are equal.
*/
function identifierEquals(a, b) {
return a.key === b.key && a.namespace === b.namespace;
}
/**
* Compare two domain objects, return true if they're the same object.
* Equality is determined by identifier.
*
* @param domainObject
* @param otherDomainOBject
* @returns Boolean true if objects are equal.
*/
function objectEquals(a, b) {
return identifierEquals(a.identifier, b.identifier);
}
function refresh(oldObject, newObject) {
let deleted = _.difference(Object.keys(oldObject), Object.keys(newObject));
deleted.forEach((propertyName) => delete oldObject[propertyName]);
Object.assign(oldObject, newObject);
if (keyString === namespace) {
namespace = '';
}
return {
isIdentifier: isIdentifier,
toOldFormat: toOldFormat,
toNewFormat: toNewFormat,
makeKeyString: makeKeyString,
parseKeyString: parseKeyString,
equals: objectEquals,
identifierEquals: identifierEquals,
refresh: refresh
namespace: namespace,
key: key
};
}
/**
* Convert an Open MCT Identifier into a keyString, ex:
* {namespace: 'scratch', key: 'root'} ==> 'scratch:root'
*
* Idempotent
*
* @param identifier
* @returns keyString
*/
function makeKeyString(identifier) {
if (!identifier) {
throw new Error('Cannot make key string from null identifier');
}
if (isKeyString(identifier)) {
return identifier;
}
if (!identifier.namespace) {
return identifier.key;
}
return [identifier.namespace.replace(/:/g, '\\:'), identifier.key].join(':');
}
/**
* Convert a new domain object into an old format model, removing the
* identifier and converting the composition array from Open MCT Identifiers
* to old format keyStrings.
*
* @param domainObject
* @returns oldFormatModel
*/
function toOldFormat(model) {
model = JSON.parse(JSON.stringify(model));
delete model.identifier;
if (model.composition) {
model.composition = model.composition.map(makeKeyString);
}
return model;
}
/**
* Convert an old format domain object model into a new format domain
* object. Adds an identifier using the provided keyString, and converts
* the composition array to utilize Open MCT Identifiers.
*
* @param model
* @param keyString
* @returns domainObject
*/
function toNewFormat(model, keyString) {
model = JSON.parse(JSON.stringify(model));
model.identifier = parseKeyString(keyString);
if (model.composition) {
model.composition = model.composition.map(parseKeyString);
}
return model;
}
/**
* Compare two Open MCT Identifiers, returning true if they are equal.
*
* @param identifier
* @param otherIdentifier
* @returns Boolean true if identifiers are equal.
*/
function identifierEquals(a, b) {
return a.key === b.key && a.namespace === b.namespace;
}
/**
* Compare two domain objects, return true if they're the same object.
* Equality is determined by identifier.
*
* @param domainObject
* @param otherDomainOBject
* @returns Boolean true if objects are equal.
*/
function objectEquals(a, b) {
return identifierEquals(a.identifier, b.identifier);
}
function refresh(oldObject, newObject) {
let deleted = _.difference(Object.keys(oldObject), Object.keys(newObject));
deleted.forEach((propertyName) => delete oldObject[propertyName]);
Object.assign(oldObject, newObject);
}
return {
isIdentifier: isIdentifier,
toOldFormat: toOldFormat,
toNewFormat: toNewFormat,
makeKeyString: makeKeyString,
parseKeyString: parseKeyString,
equals: objectEquals,
identifierEquals: identifierEquals,
refresh: refresh
};
});

View File

@@ -22,30 +22,30 @@
import RootObjectProvider from '../RootObjectProvider';
describe('RootObjectProvider', function () {
const ROOT_NAME = 'Open MCT';
let rootObjectProvider;
let roots = ['some root'];
let rootRegistry = {
getRoots: () => {
return Promise.resolve(roots);
}
};
const ROOT_NAME = 'Open MCT';
let rootObjectProvider;
let roots = ['some root'];
let rootRegistry = {
getRoots: () => {
return Promise.resolve(roots);
}
};
beforeEach(function () {
rootObjectProvider = new RootObjectProvider(rootRegistry);
});
it('supports fetching root', async () => {
let root = await rootObjectProvider.get();
expect(root).toEqual({
identifier: {
key: "ROOT",
namespace: ""
},
name: ROOT_NAME,
type: 'root',
composition: ['some root']
});
beforeEach(function () {
rootObjectProvider = new RootObjectProvider(rootRegistry);
});
it('supports fetching root', async () => {
let root = await rootObjectProvider.get();
expect(root).toEqual({
identifier: {
key: 'ROOT',
namespace: ''
},
name: ROOT_NAME,
type: 'root',
composition: ['some root']
});
});
});

View File

@@ -23,109 +23,102 @@
import { createOpenMct, resetApplicationState } from '../../../utils/testing';
describe('RootRegistry', () => {
let openmct;
let idA;
let idB;
let idC;
let idD;
let openmct;
let idA;
let idB;
let idC;
let idD;
beforeEach((done) => {
openmct = createOpenMct();
idA = {
key: 'keyA',
namespace: 'something'
};
idB = {
key: 'keyB',
namespace: 'something'
};
idC = {
key: 'keyC',
namespace: 'something'
};
idD = {
key: 'keyD',
namespace: 'something'
};
beforeEach((done) => {
openmct = createOpenMct();
idA = {
key: 'keyA',
namespace: 'something'
};
idB = {
key: 'keyB',
namespace: 'something'
};
idC = {
key: 'keyC',
namespace: 'something'
};
idD = {
key: 'keyD',
namespace: 'something'
};
openmct.on('start', done);
openmct.startHeadless();
openmct.on('start', done);
openmct.startHeadless();
});
afterEach(async () => {
await resetApplicationState(openmct);
});
it('can register a root by identifier', () => {
openmct.objects.addRoot(idA);
return openmct.objects.getRoot().then((rootObject) => {
expect(rootObject.composition).toEqual([idA]);
});
});
afterEach(async () => {
await resetApplicationState(openmct);
it('can register multiple roots by identifier', () => {
openmct.objects.addRoot([idA, idB]);
return openmct.objects.getRoot().then((rootObject) => {
expect(rootObject.composition).toEqual([idA, idB]);
});
});
it('can register a root by identifier', () => {
openmct.objects.addRoot(idA);
it('can register an asynchronous root ', () => {
openmct.objects.addRoot(() => Promise.resolve(idA));
return openmct.objects.getRoot()
.then((rootObject) => {
expect(rootObject.composition).toEqual([idA]);
});
return openmct.objects.getRoot().then((rootObject) => {
expect(rootObject.composition).toEqual([idA]);
});
});
it('can register multiple roots by identifier', () => {
openmct.objects.addRoot([idA, idB]);
it('can register multiple asynchronous roots', () => {
openmct.objects.addRoot(() => Promise.resolve([idA, idB]));
return openmct.objects.getRoot()
.then((rootObject) => {
expect(rootObject.composition).toEqual([idA, idB]);
});
return openmct.objects.getRoot().then((rootObject) => {
expect(rootObject.composition).toEqual([idA, idB]);
});
});
it('can register an asynchronous root ', () => {
openmct.objects.addRoot(() => Promise.resolve(idA));
it('can combine different types of registration', () => {
openmct.objects.addRoot([idA, idB]);
openmct.objects.addRoot(() => Promise.resolve([idC]));
return openmct.objects.getRoot()
.then((rootObject) => {
expect(rootObject.composition).toEqual([idA]);
});
return openmct.objects.getRoot().then((rootObject) => {
expect(rootObject.composition).toEqual([idA, idB, idC]);
});
});
it('can register multiple asynchronous roots', () => {
openmct.objects.addRoot(() => Promise.resolve([idA, idB]));
it('supports priority ordering for identifiers', () => {
openmct.objects.addRoot(idA, openmct.priority.LOW);
openmct.objects.addRoot(idB, openmct.priority.HIGH);
openmct.objects.addRoot(idC); // DEFAULT
return openmct.objects.getRoot()
.then((rootObject) => {
expect(rootObject.composition).toEqual([idA, idB]);
});
return openmct.objects.getRoot().then((rootObject) => {
expect(rootObject.composition[0]).toEqual(idB);
expect(rootObject.composition[1]).toEqual(idC);
expect(rootObject.composition[2]).toEqual(idA);
});
});
it('can combine different types of registration', () => {
openmct.objects.addRoot([idA, idB]);
openmct.objects.addRoot(() => Promise.resolve([idC]));
it('supports priority ordering for different types of registration', () => {
openmct.objects.addRoot(() => Promise.resolve([idC]), openmct.priority.LOW);
openmct.objects.addRoot(idB, openmct.priority.HIGH);
openmct.objects.addRoot([idA, idD]); // default
return openmct.objects.getRoot()
.then((rootObject) => {
expect(rootObject.composition).toEqual([idA, idB, idC]);
});
});
it('supports priority ordering for identifiers', () => {
openmct.objects.addRoot(idA, openmct.priority.LOW);
openmct.objects.addRoot(idB, openmct.priority.HIGH);
openmct.objects.addRoot(idC); // DEFAULT
return openmct.objects.getRoot()
.then((rootObject) => {
expect(rootObject.composition[0]).toEqual(idB);
expect(rootObject.composition[1]).toEqual(idC);
expect(rootObject.composition[2]).toEqual(idA);
});
});
it('supports priority ordering for different types of registration', () => {
openmct.objects.addRoot(() => Promise.resolve([idC]), openmct.priority.LOW);
openmct.objects.addRoot(idB, openmct.priority.HIGH);
openmct.objects.addRoot([idA, idD]); // default
return openmct.objects.getRoot()
.then((rootObject) => {
expect(rootObject.composition[0]).toEqual(idB);
expect(rootObject.composition[1]).toEqual(idA);
expect(rootObject.composition[2]).toEqual(idD);
expect(rootObject.composition[3]).toEqual(idC);
});
return openmct.objects.getRoot().then((rootObject) => {
expect(rootObject.composition[0]).toEqual(idB);
expect(rootObject.composition[1]).toEqual(idA);
expect(rootObject.composition[2]).toEqual(idD);
expect(rootObject.composition[3]).toEqual(idC);
});
});
});

View File

@@ -1,152 +1,147 @@
define([
'objectUtils'
], function (
objectUtils
) {
describe('objectUtils', function () {
define(['objectUtils'], function (objectUtils) {
describe('objectUtils', function () {
describe('keyString util', function () {
const EXPECTATIONS = {
ROOT: {
namespace: '',
key: 'ROOT'
},
mine: {
namespace: '',
key: 'mine'
},
'extended:something:with:colons': {
key: 'something:with:colons',
namespace: 'extended'
},
'https\\://some/url:resourceId': {
key: 'resourceId',
namespace: 'https://some/url'
},
'scratch:root': {
namespace: 'scratch',
key: 'root'
},
'thingy\\:thing:abc123': {
namespace: 'thingy:thing',
key: 'abc123'
}
};
describe('keyString util', function () {
const EXPECTATIONS = {
'ROOT': {
namespace: '',
key: 'ROOT'
},
'mine': {
namespace: '',
key: 'mine'
},
'extended:something:with:colons': {
key: 'something:with:colons',
namespace: 'extended'
},
'https\\://some/url:resourceId': {
key: 'resourceId',
namespace: 'https://some/url'
},
'scratch:root': {
namespace: 'scratch',
key: 'root'
},
'thingy\\:thing:abc123': {
namespace: 'thingy:thing',
key: 'abc123'
}
};
Object.keys(EXPECTATIONS).forEach(function (keyString) {
it('parses "' + keyString + '".', function () {
expect(objectUtils.parseKeyString(keyString))
.toEqual(EXPECTATIONS[keyString]);
});
it('parses and re-encodes "' + keyString + '"', function () {
const identifier = objectUtils.parseKeyString(keyString);
expect(objectUtils.makeKeyString(identifier))
.toEqual(keyString);
});
it('is idempotent for "' + keyString + '".', function () {
const identifier = objectUtils.parseKeyString(keyString);
let again = objectUtils.parseKeyString(identifier);
expect(identifier).toEqual(again);
again = objectUtils.parseKeyString(again);
again = objectUtils.parseKeyString(again);
expect(identifier).toEqual(again);
let againKeyString = objectUtils.makeKeyString(again);
expect(againKeyString).toEqual(keyString);
againKeyString = objectUtils.makeKeyString(againKeyString);
againKeyString = objectUtils.makeKeyString(againKeyString);
againKeyString = objectUtils.makeKeyString(againKeyString);
expect(againKeyString).toEqual(keyString);
});
});
Object.keys(EXPECTATIONS).forEach(function (keyString) {
it('parses "' + keyString + '".', function () {
expect(objectUtils.parseKeyString(keyString)).toEqual(EXPECTATIONS[keyString]);
});
describe('old object conversions', function () {
it('translate ids', function () {
expect(objectUtils.toNewFormat({
prop: 'someValue'
}, 'objId'))
.toEqual({
prop: 'someValue',
identifier: {
namespace: '',
key: 'objId'
}
});
});
it('translates composition', function () {
expect(objectUtils.toNewFormat({
prop: 'someValue',
composition: [
'anotherObjectId',
'scratch:anotherObjectId'
]
}, 'objId'))
.toEqual({
prop: 'someValue',
composition: [
{
namespace: '',
key: 'anotherObjectId'
},
{
namespace: 'scratch',
key: 'anotherObjectId'
}
],
identifier: {
namespace: '',
key: 'objId'
}
});
});
it('parses and re-encodes "' + keyString + '"', function () {
const identifier = objectUtils.parseKeyString(keyString);
expect(objectUtils.makeKeyString(identifier)).toEqual(keyString);
});
describe('new object conversions', function () {
it('is idempotent for "' + keyString + '".', function () {
const identifier = objectUtils.parseKeyString(keyString);
let again = objectUtils.parseKeyString(identifier);
expect(identifier).toEqual(again);
again = objectUtils.parseKeyString(again);
again = objectUtils.parseKeyString(again);
expect(identifier).toEqual(again);
it('removes ids', function () {
expect(objectUtils.toOldFormat({
prop: 'someValue',
identifier: {
namespace: '',
key: 'objId'
}
}))
.toEqual({
prop: 'someValue'
});
});
it('translates composition', function () {
expect(objectUtils.toOldFormat({
prop: 'someValue',
composition: [
{
namespace: '',
key: 'anotherObjectId'
},
{
namespace: 'scratch',
key: 'anotherObjectId'
}
],
identifier: {
namespace: '',
key: 'objId'
}
}))
.toEqual({
prop: 'someValue',
composition: [
'anotherObjectId',
'scratch:anotherObjectId'
]
});
});
let againKeyString = objectUtils.makeKeyString(again);
expect(againKeyString).toEqual(keyString);
againKeyString = objectUtils.makeKeyString(againKeyString);
againKeyString = objectUtils.makeKeyString(againKeyString);
againKeyString = objectUtils.makeKeyString(againKeyString);
expect(againKeyString).toEqual(keyString);
});
});
});
describe('old object conversions', function () {
it('translate ids', function () {
expect(
objectUtils.toNewFormat(
{
prop: 'someValue'
},
'objId'
)
).toEqual({
prop: 'someValue',
identifier: {
namespace: '',
key: 'objId'
}
});
});
it('translates composition', function () {
expect(
objectUtils.toNewFormat(
{
prop: 'someValue',
composition: ['anotherObjectId', 'scratch:anotherObjectId']
},
'objId'
)
).toEqual({
prop: 'someValue',
composition: [
{
namespace: '',
key: 'anotherObjectId'
},
{
namespace: 'scratch',
key: 'anotherObjectId'
}
],
identifier: {
namespace: '',
key: 'objId'
}
});
});
});
describe('new object conversions', function () {
it('removes ids', function () {
expect(
objectUtils.toOldFormat({
prop: 'someValue',
identifier: {
namespace: '',
key: 'objId'
}
})
).toEqual({
prop: 'someValue'
});
});
it('translates composition', function () {
expect(
objectUtils.toOldFormat({
prop: 'someValue',
composition: [
{
namespace: '',
key: 'anotherObjectId'
},
{
namespace: 'scratch',
key: 'anotherObjectId'
}
],
identifier: {
namespace: '',
key: 'objId'
}
})
).toEqual({
prop: 'someValue',
composition: ['anotherObjectId', 'scratch:anotherObjectId']
});
});
});
});
});

View File

@@ -3,33 +3,32 @@ import Overlay from './Overlay';
import Vue from 'vue';
class Dialog extends Overlay {
constructor({iconClass, message, title, hint, timestamp, ...options}) {
constructor({ iconClass, message, title, hint, timestamp, ...options }) {
let component = new Vue({
components: {
DialogComponent: DialogComponent
},
provide: {
iconClass,
message,
title,
hint,
timestamp
},
template: '<dialog-component></dialog-component>'
}).$mount();
let component = new Vue({
components: {
DialogComponent: DialogComponent
},
provide: {
iconClass,
message,
title,
hint,
timestamp
},
template: '<dialog-component></dialog-component>'
}).$mount();
super({
element: component.$el,
size: 'fit',
dismissable: false,
...options
});
super({
element: component.$el,
size: 'fit',
dismissable: false,
...options
});
this.once('destroy', () => {
component.$destroy();
});
}
this.once('destroy', () => {
component.$destroy();
});
}
}
export default Dialog;

View File

@@ -3,72 +3,72 @@ import EventEmitter from 'EventEmitter';
import Vue from 'vue';
const cssClasses = {
large: 'l-overlay-large',
small: 'l-overlay-small',
fit: 'l-overlay-fit',
fullscreen: 'l-overlay-fullscreen',
dialog: 'l-overlay-dialog'
large: 'l-overlay-large',
small: 'l-overlay-small',
fit: 'l-overlay-fit',
fullscreen: 'l-overlay-fullscreen',
dialog: 'l-overlay-dialog'
};
class Overlay extends EventEmitter {
constructor({
buttons,
autoHide = true,
dismissable = true,
constructor({
buttons,
autoHide = true,
dismissable = true,
element,
onDestroy,
onDismiss,
size
} = {}) {
super();
this.container = document.createElement('div');
this.container.classList.add('l-overlay-wrapper', cssClasses[size]);
this.autoHide = autoHide;
this.dismissable = dismissable !== false;
this.component = new Vue({
components: {
OverlayComponent: OverlayComponent
},
provide: {
dismiss: this.notifyAndDismiss.bind(this),
element,
onDestroy,
onDismiss,
size
} = {}) {
super();
buttons,
dismissable: this.dismissable
},
template: '<overlay-component></overlay-component>'
});
this.container = document.createElement('div');
this.container.classList.add('l-overlay-wrapper', cssClasses[size]);
this.autoHide = autoHide;
this.dismissable = dismissable !== false;
this.component = new Vue({
components: {
OverlayComponent: OverlayComponent
},
provide: {
dismiss: this.notifyAndDismiss.bind(this),
element,
buttons,
dismissable: this.dismissable
},
template: '<overlay-component></overlay-component>'
});
if (onDestroy) {
this.once('destroy', onDestroy);
}
if (onDismiss) {
this.once('dismiss', onDismiss);
}
if (onDestroy) {
this.once('destroy', onDestroy);
}
dismiss() {
this.emit('destroy');
document.body.removeChild(this.container);
this.component.$destroy();
if (onDismiss) {
this.once('dismiss', onDismiss);
}
}
//Ensures that any callers are notified that the overlay is dismissed
notifyAndDismiss() {
this.emit('dismiss');
this.dismiss();
}
dismiss() {
this.emit('destroy');
document.body.removeChild(this.container);
this.component.$destroy();
}
/**
* @private
**/
show() {
document.body.appendChild(this.container);
this.container.appendChild(this.component.$mount().$el);
}
//Ensures that any callers are notified that the overlay is dismissed
notifyAndDismiss() {
this.emit('dismiss');
this.dismiss();
}
/**
* @private
**/
show() {
document.body.appendChild(this.container);
this.container.appendChild(this.component.$mount().$el);
}
}
export default Overlay;

View File

@@ -9,129 +9,127 @@ import ProgressDialog from './ProgressDialog';
*
* @memberof api/overlays
* @constructor
*/
*/
class OverlayAPI {
constructor() {
this.activeOverlays = [];
constructor() {
this.activeOverlays = [];
this.dismissLastOverlay = this.dismissLastOverlay.bind(this);
this.dismissLastOverlay = this.dismissLastOverlay.bind(this);
document.addEventListener('keyup', (event) => {
if (event.key === 'Escape') {
this.dismissLastOverlay();
}
});
document.addEventListener('keyup', (event) => {
if (event.key === 'Escape') {
this.dismissLastOverlay();
}
});
}
/**
* private
*/
showOverlay(overlay) {
if (this.activeOverlays.length) {
const previousOverlay = this.activeOverlays[this.activeOverlays.length - 1];
if (previousOverlay.autoHide) {
previousOverlay.container.classList.add('invisible');
}
}
/**
* private
*/
showOverlay(overlay) {
if (this.activeOverlays.length) {
const previousOverlay = this.activeOverlays[this.activeOverlays.length - 1];
if (previousOverlay.autoHide) {
previousOverlay.container.classList.add('invisible');
}
}
this.activeOverlays.push(overlay);
this.activeOverlays.push(overlay);
overlay.once('destroy', () => {
this.activeOverlays.splice(this.activeOverlays.indexOf(overlay), 1);
overlay.once('destroy', () => {
this.activeOverlays.splice(this.activeOverlays.indexOf(overlay), 1);
if (this.activeOverlays.length) {
this.activeOverlays[this.activeOverlays.length - 1].container.classList.remove('invisible');
}
});
if (this.activeOverlays.length) {
this.activeOverlays[this.activeOverlays.length - 1].container.classList.remove('invisible');
}
});
overlay.show();
}
overlay.show();
/**
* private
*/
dismissLastOverlay() {
let lastOverlay = this.activeOverlays[this.activeOverlays.length - 1];
if (lastOverlay && lastOverlay.dismissable) {
lastOverlay.notifyAndDismiss();
}
}
/**
* private
*/
dismissLastOverlay() {
let lastOverlay = this.activeOverlays[this.activeOverlays.length - 1];
if (lastOverlay && lastOverlay.dismissable) {
lastOverlay.notifyAndDismiss();
}
}
/**
* A description of option properties that can be passed into the overlay
* @typedef options
* @property {object} element DOMElement that is to be inserted/shown on the overlay
* @property {string} size preferred size of the overlay (large, small, fit)
* @property {array} buttons optional button objects with label and callback properties
* @property {function} onDestroy callback to be called when overlay is destroyed
* @property {boolean} dismissable allow user to dismiss overlay by using esc, and clicking away
* from overlay. Unless set to false, all overlays will be dismissable by default.
*/
overlay(options) {
let overlay = new Overlay(options);
/**
* A description of option properties that can be passed into the overlay
* @typedef options
* @property {object} element DOMElement that is to be inserted/shown on the overlay
* @property {string} size preferred size of the overlay (large, small, fit)
* @property {array} buttons optional button objects with label and callback properties
* @property {function} onDestroy callback to be called when overlay is destroyed
* @property {boolean} dismissable allow user to dismiss overlay by using esc, and clicking away
* from overlay. Unless set to false, all overlays will be dismissable by default.
*/
overlay(options) {
let overlay = new Overlay(options);
this.showOverlay(overlay);
this.showOverlay(overlay);
return overlay;
}
return overlay;
}
/**
* Displays a blocking (modal) dialog. This dialog can be used for
* displaying messages that require the user's
* immediate attention.
* @param {model} options defines options for the dialog
* @returns {object} with an object with a dismiss function that can be called from the calling code
* to dismiss/destroy the dialog
*
* A description of the model options that may be passed to the
* dialog method. Note that the DialogModel described
* here is shared with the Notifications framework.
* @see NotificationService
*
* @typedef options
* @property {string} title the title to use for the dialog
* @property {string} iconClass class to apply to icon that is shown on dialog
* @property {string} message text that indicates a current message,
* @property {buttons[]} buttons a list of buttons with title and callback properties that will
* be added to the dialog.
*/
dialog(options) {
let dialog = new Dialog(options);
/**
* Displays a blocking (modal) dialog. This dialog can be used for
* displaying messages that require the user's
* immediate attention.
* @param {model} options defines options for the dialog
* @returns {object} with an object with a dismiss function that can be called from the calling code
* to dismiss/destroy the dialog
*
* A description of the model options that may be passed to the
* dialog method. Note that the DialogModel described
* here is shared with the Notifications framework.
* @see NotificationService
*
* @typedef options
* @property {string} title the title to use for the dialog
* @property {string} iconClass class to apply to icon that is shown on dialog
* @property {string} message text that indicates a current message,
* @property {buttons[]} buttons a list of buttons with title and callback properties that will
* be added to the dialog.
*/
dialog(options) {
let dialog = new Dialog(options);
this.showOverlay(dialog);
this.showOverlay(dialog);
return dialog;
}
return dialog;
}
/**
* Displays a blocking (modal) progress dialog. This dialog can be used for
* displaying messages that require the user's attention, and show progress
* @param {model} options defines options for the dialog
* @returns {object} with an object with a dismiss function that can be called from the calling code
* to dismiss/destroy the dialog and an updateProgress function that takes progressPercentage(Number 0-100)
* and progressText (string)
*
* A description of the model options that may be passed to the
* dialog method. Note that the DialogModel described
* here is shared with the Notifications framework.
* @see NotificationService
*
* @typedef options
* @property {number} progressPerc the initial progress value (0-100) or {string} 'unknown' for anonymous progress
* @property {string} progressText the initial text to be shown under the progress bar
* @property {buttons[]} buttons a list of buttons with title and callback properties that will
* be added to the dialog.
*/
progressDialog(options) {
let progressDialog = new ProgressDialog(options);
/**
* Displays a blocking (modal) progress dialog. This dialog can be used for
* displaying messages that require the user's attention, and show progress
* @param {model} options defines options for the dialog
* @returns {object} with an object with a dismiss function that can be called from the calling code
* to dismiss/destroy the dialog and an updateProgress function that takes progressPercentage(Number 0-100)
* and progressText (string)
*
* A description of the model options that may be passed to the
* dialog method. Note that the DialogModel described
* here is shared with the Notifications framework.
* @see NotificationService
*
* @typedef options
* @property {number} progressPerc the initial progress value (0-100) or {string} 'unknown' for anonymous progress
* @property {string} progressText the initial text to be shown under the progress bar
* @property {buttons[]} buttons a list of buttons with title and callback properties that will
* be added to the dialog.
*/
progressDialog(options) {
let progressDialog = new ProgressDialog(options);
this.showOverlay(progressDialog);
return progressDialog;
}
this.showOverlay(progressDialog);
return progressDialog;
}
}
export default OverlayAPI;

View File

@@ -5,45 +5,54 @@ import Vue from 'vue';
let component;
class ProgressDialog extends Overlay {
constructor({progressPerc, progressText, iconClass, message, title, hint, timestamp, ...options}) {
component = new Vue({
components: {
ProgressDialogComponent: ProgressDialogComponent
},
provide: {
iconClass,
message,
title,
hint,
timestamp
},
data() {
return {
model: {
progressPerc: progressPerc || 0,
progressText
}
};
},
template: '<progress-dialog-component :model="model"></progress-dialog-component>'
}).$mount();
constructor({
progressPerc,
progressText,
iconClass,
message,
title,
hint,
timestamp,
...options
}) {
component = new Vue({
components: {
ProgressDialogComponent: ProgressDialogComponent
},
provide: {
iconClass,
message,
title,
hint,
timestamp
},
data() {
return {
model: {
progressPerc: progressPerc || 0,
progressText
}
};
},
template: '<progress-dialog-component :model="model"></progress-dialog-component>'
}).$mount();
super({
element: component.$el,
size: 'fit',
dismissable: false,
...options
});
super({
element: component.$el,
size: 'fit',
dismissable: false,
...options
});
this.once('destroy', () => {
component.$destroy();
});
}
this.once('destroy', () => {
component.$destroy();
});
}
updateProgress(progressPerc, progressText) {
component.model.progressPerc = progressPerc;
component.model.progressText = progressText;
}
updateProgress(progressPerc, progressText) {
component.model.progressPerc = progressPerc;
component.model.progressText = progressText;
}
}
export default ProgressDialog;

View File

@@ -20,42 +20,30 @@
at runtime from the About dialog for additional information.
-->
<template>
<div class="c-message">
<div class="c-message">
<!--Uses flex-row -->
<div
class="c-message__icon"
:class="['u-icon-bg-color-' + iconClass]"
></div>
<div class="c-message__icon" :class="['u-icon-bg-color-' + iconClass]"></div>
<div class="c-message__text">
<!-- Uses flex-column -->
<div
v-if="title"
class="c-message__title"
>
{{ title }}
</div>
<!-- Uses flex-column -->
<div v-if="title" class="c-message__title">
{{ title }}
</div>
<div
v-if="hint"
class="c-message__hint"
>
{{ hint }}
<span v-if="timestamp">[{{ timestamp }}]</span>
</div>
<div v-if="hint" class="c-message__hint">
{{ hint }}
<span v-if="timestamp">[{{ timestamp }}]</span>
</div>
<div
v-if="message"
class="c-message__action-text"
>
{{ message }}
</div>
<slot></slot>
<div v-if="message" class="c-message__action-text">
{{ message }}
</div>
<slot></slot>
</div>
</div>
</div>
</template>
<script>
export default {
inject: ['iconClass', 'title', 'hint', 'timestamp', 'message']
inject: ['iconClass', 'title', 'hint', 'timestamp', 'message']
};
</script>

View File

@@ -20,92 +20,86 @@
at runtime from the About dialog for additional information.
-->
<template>
<div class="c-overlay js-overlay">
<div
class="c-overlay__blocker"
@click="destroy"
></div>
<div class="c-overlay js-overlay">
<div class="c-overlay__blocker" @click="destroy"></div>
<div class="c-overlay__outer">
<button
v-if="dismissable"
aria-label="Close"
class="c-click-icon c-overlay__close-button icon-x"
@click="destroy"
></button>
<div
ref="element"
class="c-overlay__contents js-notebook-snapshot-item-wrapper"
tabindex="0"
aria-modal="true"
role="dialog"
></div>
<div v-if="buttons" class="c-overlay__button-bar">
<button
v-if="dismissable"
aria-label="Close"
class="c-click-icon c-overlay__close-button icon-x"
@click="destroy"
></button>
<div
ref="element"
class="c-overlay__contents js-notebook-snapshot-item-wrapper"
tabindex="0"
aria-modal="true"
role="dialog"
></div>
<div
v-if="buttons"
class="c-overlay__button-bar"
v-for="(button, index) in buttons"
ref="buttons"
:key="index"
class="c-button js-overlay__button"
tabindex="0"
:class="{ 'c-button--major': focusIndex === index }"
@focus="focusIndex = index"
@click="buttonClickHandler(button.callback)"
>
<button
v-for="(button, index) in buttons"
ref="buttons"
:key="index"
class="c-button js-overlay__button"
tabindex="0"
:class="{'c-button--major': focusIndex===index}"
@focus="focusIndex=index"
@click="buttonClickHandler(button.callback)"
>
{{ button.label }}
</button>
</div>
{{ button.label }}
</button>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
inject: ['dismiss', 'element', 'buttons', 'dismissable'],
data: function () {
return {
focusIndex: -1
};
inject: ['dismiss', 'element', 'buttons', 'dismissable'],
data: function () {
return {
focusIndex: -1
};
},
mounted() {
const element = this.$refs.element;
element.appendChild(this.element);
const elementForFocus = this.getElementForFocus() || element;
this.$nextTick(() => {
elementForFocus.focus();
});
},
methods: {
destroy: function () {
if (this.dismissable) {
this.dismiss();
}
},
mounted() {
const element = this.$refs.element;
element.appendChild(this.element);
const elementForFocus = this.getElementForFocus() || element;
this.$nextTick(() => {
elementForFocus.focus();
});
buttonClickHandler: function (method) {
method();
this.$emit('destroy');
},
methods: {
destroy: function () {
if (this.dismissable) {
this.dismiss();
}
},
buttonClickHandler: function (method) {
method();
this.$emit('destroy');
},
getElementForFocus: function () {
const defaultElement = this.$refs.element;
if (!this.$refs.buttons) {
return defaultElement;
}
getElementForFocus: function () {
const defaultElement = this.$refs.element;
if (!this.$refs.buttons) {
return defaultElement;
}
const focusButton = this.$refs.buttons.filter((button, index) => {
if (this.buttons[index].emphasis) {
this.focusIndex = index;
}
return this.buttons[index].emphasis;
});
if (!focusButton.length) {
return defaultElement;
}
return focusButton[0];
const focusButton = this.$refs.buttons.filter((button, index) => {
if (this.buttons[index].emphasis) {
this.focusIndex = index;
}
return this.buttons[index].emphasis;
});
if (!focusButton.length) {
return defaultElement;
}
return focusButton[0];
}
}
};
</script>

View File

@@ -20,9 +20,9 @@
at runtime from the About dialog for additional information.
-->
<template>
<dialog-component>
<dialog-component>
<progress-component :model="model" />
</dialog-component>
</dialog-component>
</template>
<script>
@@ -30,16 +30,16 @@ import ProgressComponent from '../../../ui/components/ProgressBar.vue';
import DialogComponent from './DialogComponent.vue';
export default {
components: {
DialogComponent: DialogComponent,
ProgressComponent: ProgressComponent
},
inject: ['iconClass', 'title', 'hint', 'timestamp', 'message'],
props: {
model: {
type: Object,
required: true
}
components: {
DialogComponent: DialogComponent,
ProgressComponent: ProgressComponent
},
inject: ['iconClass', 'title', 'hint', 'timestamp', 'message'],
props: {
model: {
type: Object,
required: true
}
}
};
</script>

View File

@@ -1,81 +1,81 @@
@mixin legacyMessage() {
flex: 0 1 auto;
font-family: symbolsfont;
font-size: $messageIconD; // Singleton message in a dialog
margin-right: $interiorMarginLg;
flex: 0 1 auto;
font-family: symbolsfont;
font-size: $messageIconD; // Singleton message in a dialog
margin-right: $interiorMarginLg;
}
.c-message {
display: flex;
align-items: center;
> * + * {
margin-left: $interiorMarginLg;
}
&__icon {
// Holds a background SVG graphic
$s: 80px;
flex: 0 0 auto;
min-width: $s;
min-height: $s;
}
&__text {
display: flex;
align-items: center;
flex-direction: column;
flex: 1 1 auto;
> * + * {
margin-left: $interiorMarginLg;
margin-top: $interiorMargin;
}
}
// __text elements
&__action-text {
font-size: 1.2em;
}
&__title {
font-size: 1.5em;
font-weight: bold;
}
&--simple {
// Icon and text elements only
&:before {
font-size: 30px !important;
}
&__icon {
// Holds a background SVG graphic
$s: 80px;
flex: 0 0 auto;
min-width: $s;
min-height: $s;
[class*='__text'] {
font-size: 1.25em;
}
}
&__text {
display: flex;
flex-direction: column;
flex: 1 1 auto;
/************************** LEGACY */
&.message-severity-info:before {
@include legacyMessage();
content: $glyph-icon-info;
color: $colorInfo;
}
> * + * {
margin-top: $interiorMargin;
}
}
// __text elements
&__action-text {
font-size: 1.2em;
}
&__title {
font-size: 1.5em;
font-weight: bold;
}
&--simple {
// Icon and text elements only
&:before {
font-size: 30px !important;
}
[class*='__text'] {
font-size: 1.25em;
}
}
/************************** LEGACY */
&.message-severity-info:before {
@include legacyMessage();
content: $glyph-icon-info;
color: $colorInfo;
}
&.message-severity-alert:before {
@include legacyMessage();
content: $glyph-icon-alert-rect;
color: $colorWarningLo;
}
&.message-severity-error:before {
@include legacyMessage();
content: $glyph-icon-alert-triangle;
color: $colorWarningHi;
}
// Messages in a list
.c-overlay__messages & {
padding: $interiorMarginLg;
&:before {
font-size: $messageListIconD;
}
&.message-severity-alert:before {
@include legacyMessage();
content: $glyph-icon-alert-rect;
color: $colorWarningLo;
}
&.message-severity-error:before {
@include legacyMessage();
content: $glyph-icon-alert-triangle;
color: $colorWarningHi;
}
// Messages in a list
.c-overlay__messages & {
padding: $interiorMarginLg;
&:before {
font-size: $messageListIconD;
}
}
}

View File

@@ -1,161 +1,168 @@
@mixin overlaySizing($marginTB: auto, $marginLR: auto, $width: auto, $height: auto) {
position: absolute;
top: $marginTB; right: $marginLR; bottom: $marginTB; left: $marginLR;
width: $width;
height: $height;
position: absolute;
top: $marginTB;
right: $marginLR;
bottom: $marginTB;
left: $marginLR;
width: $width;
height: $height;
}
.l-overlay-wrapper {
// Created by overlayService.js, contains this template.
// Acts as an anchor for one or more overlays.
display: contents;
// Created by overlayService.js, contains this template.
// Acts as an anchor for one or more overlays.
display: contents;
}
.c-overlay {
@include abs();
z-index: 70;
&__blocker {
display: none; // Mobile-first
}
&__outer {
@include abs();
z-index: 70;
background: $colorBodyBg;
display: flex;
flex-direction: column;
padding: $overlayInnerMargin;
}
&__blocker {
display: none; // Mobile-first
&__close-button {
$p: $interiorMargin + 2px;
font-size: 1.5em;
position: absolute;
top: $p;
right: $p;
z-index: 99;
}
&__contents {
flex: 1 1 auto;
display: flex;
flex-direction: column;
outline: none;
overflow: auto;
}
&__top-bar {
flex: 0 0 auto;
flex-direction: column;
display: flex;
> * {
flex: 0 0 auto;
margin-bottom: $interiorMargin;
}
}
&__outer {
@include abs();
background: $colorBodyBg;
display: flex;
flex-direction: column;
padding: $overlayInnerMargin;
&__dialog-title {
@include ellipsize();
font-size: 1.5em;
line-height: 120%;
}
&__contents-main {
display: flex;
flex-direction: column;
flex: 1 1 auto;
height: 0; // Chrome 73 overflow bug fix
overflow: auto;
padding-right: $interiorMargin; // fend off scroll bar
}
&__button-bar {
flex: 0 0 auto;
display: flex;
justify-content: flex-end;
margin-top: $interiorMargin;
> * + * {
margin-left: $interiorMargin;
}
}
&__close-button {
$p: $interiorMargin + 2px;
font-size: 1.5em;
position: absolute;
top: $p; right: $p;
z-index: 99;
}
&__contents {
flex: 1 1 auto;
display: flex;
flex-direction: column;
outline: none;
overflow: auto;
}
&__top-bar {
flex: 0 0 auto;
flex-direction: column;
display: flex;
> * {
flex: 0 0 auto;
margin-bottom: $interiorMargin;
}
}
&__dialog-title {
@include ellipsize();
font-size: 1.5em;
line-height: 120%;
}
&__contents-main {
display: flex;
flex-direction: column;
flex: 1 1 auto;
height: 0; // Chrome 73 overflow bug fix
overflow: auto;
padding-right: $interiorMargin; // fend off scroll bar
}
&__button-bar {
flex: 0 0 auto;
display: flex;
justify-content: flex-end;
margin-top: $interiorMargin;
> * + * {
margin-left: $interiorMargin;
}
}
.c-object-label__name {
color: $objectLabelNameColorFg;
}
.c-object-label__name {
color: $objectLabelNameColorFg;
}
}
body.desktop {
.c-overlay {
&__blocker {
@include abs();
background: $colorOvrBlocker;
cursor: pointer;
display: block;
}
}
// Overlay types, styling for desktop. Appended to .l-overlay-wrapper element.
.l-overlay-large,
.l-overlay-small,
.l-overlay-dialog,
.l-overlay-fit {
.c-overlay__outer {
border-radius: $overlayCr;
box-shadow: rgba(black, 0.5) 0 2px 25px;
}
}
.l-overlay-fullscreen {
// Used by About > Licenses display
.c-overlay__outer {
@include overlaySizing(
nth($overlayOuterMarginFullscreen, 1),
nth($overlayOuterMarginFullscreen, 2)
);
}
}
.l-overlay-large {
// Default
$pad: $interiorMarginLg;
$tbPad: floor($pad * 0.8);
$lrPad: $pad;
.c-overlay {
&__blocker {
@include abs();
background: $colorOvrBlocker;
cursor: pointer;
display: block;
}
&__outer {
@include overlaySizing(nth($overlayOuterMarginLarge, 1), nth($overlayOuterMarginLarge, 2));
padding: $tbPad $lrPad;
}
&__close-button {
//top: $interiorMargin;
//right: $interiorMargin;
}
}
// Overlay types, styling for desktop. Appended to .l-overlay-wrapper element.
.l-overlay-large,
.l-overlay-small,
.l-overlay-dialog,
.l-browse-bar {
margin-right: 50px; // Don't cover close button
margin-bottom: $interiorMargin;
}
}
.l-overlay-small {
.c-overlay__outer {
@include overlaySizing(nth($overlayOuterMarginSmall, 1), nth($overlayOuterMarginSmall, 2));
}
}
.l-overlay-dialog {
.c-overlay__outer {
@include overlaySizing(nth($overlayOuterMarginDialog, 1), nth($overlayOuterMarginDialog, 2));
}
}
.t-dialog-sm .l-overlay-small, // Legacy dialog support
.l-overlay-fit {
.c-overlay__outer {
border-radius: $overlayCr;
box-shadow: rgba(black, 0.5) 0 2px 25px;
}
}
.l-overlay-fullscreen {
// Used by About > Licenses display
.c-overlay__outer {
@include overlaySizing(nth($overlayOuterMarginFullscreen, 1), nth($overlayOuterMarginFullscreen, 2));
}
}
.l-overlay-large {
// Default
$pad: $interiorMarginLg;
$tbPad: floor($pad * 0.8);
$lrPad: $pad;
.c-overlay {
&__outer {
@include overlaySizing(nth($overlayOuterMarginLarge, 1), nth($overlayOuterMarginLarge, 2));
padding: $tbPad $lrPad;
}
&__close-button {
//top: $interiorMargin;
//right: $interiorMargin;
}
}
.l-browse-bar {
margin-right: 50px; // Don't cover close button
margin-bottom: $interiorMargin;
}
}
.l-overlay-small {
.c-overlay__outer {
@include overlaySizing(nth($overlayOuterMarginSmall, 1), nth($overlayOuterMarginSmall, 2));
}
}
.l-overlay-dialog {
.c-overlay__outer {
@include overlaySizing(nth($overlayOuterMarginDialog, 1), nth($overlayOuterMarginDialog, 2));
}
}
.t-dialog-sm .l-overlay-small, // Legacy dialog support
.l-overlay-fit {
.c-overlay__outer {
@include overlaySizing(auto, auto);
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
min-width: 20%;
}
.c-overlay__outer {
@include overlaySizing(auto, auto);
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
min-width: 20%;
}
}
}

View File

@@ -21,8 +21,8 @@
*****************************************************************************/
const PRIORITIES = Object.freeze({
HIGH: 1000,
DEFAULT: 0,
LOW: -1000
HIGH: 1000,
DEFAULT: 0,
LOW: -1000
});
export default PRIORITIES;

View File

@@ -23,45 +23,45 @@
import EventEmitter from 'EventEmitter';
export default class StatusAPI extends EventEmitter {
constructor(openmct) {
super();
constructor(openmct) {
super();
this._openmct = openmct;
this._statusCache = {};
this._openmct = openmct;
this._statusCache = {};
this.get = this.get.bind(this);
this.set = this.set.bind(this);
this.observe = this.observe.bind(this);
}
this.get = this.get.bind(this);
this.set = this.set.bind(this);
this.observe = this.observe.bind(this);
}
get(identifier) {
let keyString = this._openmct.objects.makeKeyString(identifier);
get(identifier) {
let keyString = this._openmct.objects.makeKeyString(identifier);
return this._statusCache[keyString];
}
return this._statusCache[keyString];
}
set(identifier, value) {
let keyString = this._openmct.objects.makeKeyString(identifier);
set(identifier, value) {
let keyString = this._openmct.objects.makeKeyString(identifier);
this._statusCache[keyString] = value;
this.emit(keyString, value);
}
this._statusCache[keyString] = value;
this.emit(keyString, value);
}
delete(identifier) {
let keyString = this._openmct.objects.makeKeyString(identifier);
delete(identifier) {
let keyString = this._openmct.objects.makeKeyString(identifier);
this._statusCache[keyString] = undefined;
this.emit(keyString, undefined);
delete this._statusCache[keyString];
}
this._statusCache[keyString] = undefined;
this.emit(keyString, undefined);
delete this._statusCache[keyString];
}
observe(identifier, callback) {
let key = this._openmct.objects.makeKeyString(identifier);
observe(identifier, callback) {
let key = this._openmct.objects.makeKeyString(identifier);
this.on(key, callback);
this.on(key, callback);
return () => {
this.off(key, callback);
};
}
return () => {
this.off(key, callback);
};
}
}

View File

@@ -1,85 +1,84 @@
import StatusAPI from './StatusAPI.js';
import { createOpenMct, resetApplicationState } from '../../utils/testing';
describe("The Status API", () => {
let statusAPI;
let openmct;
let identifier;
let status;
let status2;
let callback;
describe('The Status API', () => {
let statusAPI;
let openmct;
let identifier;
let status;
let status2;
let callback;
beforeEach(() => {
openmct = createOpenMct();
statusAPI = new StatusAPI(openmct);
identifier = {
namespace: "test-namespace",
key: "test-key"
};
status = "test-status";
status2 = 'test-status-deux';
callback = jasmine.createSpy('callback', (statusUpdate) => statusUpdate);
beforeEach(() => {
openmct = createOpenMct();
statusAPI = new StatusAPI(openmct);
identifier = {
namespace: 'test-namespace',
key: 'test-key'
};
status = 'test-status';
status2 = 'test-status-deux';
callback = jasmine.createSpy('callback', (statusUpdate) => statusUpdate);
});
afterEach(() => {
return resetApplicationState(openmct);
});
describe('set function', () => {
it('sets status for identifier', () => {
statusAPI.set(identifier, status);
let resultingStatus = statusAPI.get(identifier);
expect(resultingStatus).toEqual(status);
});
});
describe('get function', () => {
it('returns status for identifier', () => {
statusAPI.set(identifier, status2);
let resultingStatus = statusAPI.get(identifier);
expect(resultingStatus).toEqual(status2);
});
});
describe('delete function', () => {
it('deletes status for identifier', () => {
statusAPI.set(identifier, status);
let resultingStatus = statusAPI.get(identifier);
expect(resultingStatus).toEqual(status);
statusAPI.delete(identifier);
resultingStatus = statusAPI.get(identifier);
expect(resultingStatus).toBeUndefined();
});
});
describe('observe function', () => {
it('allows callbacks to be attached to status set and delete events', () => {
let unsubscribe = statusAPI.observe(identifier, callback);
statusAPI.set(identifier, status);
expect(callback).toHaveBeenCalledWith(status);
statusAPI.delete(identifier);
expect(callback).toHaveBeenCalledWith(undefined);
unsubscribe();
});
afterEach(() => {
return resetApplicationState(openmct);
});
describe("set function", () => {
it("sets status for identifier", () => {
statusAPI.set(identifier, status);
let resultingStatus = statusAPI.get(identifier);
expect(resultingStatus).toEqual(status);
});
});
describe("get function", () => {
it("returns status for identifier", () => {
statusAPI.set(identifier, status2);
let resultingStatus = statusAPI.get(identifier);
expect(resultingStatus).toEqual(status2);
});
});
describe("delete function", () => {
it("deletes status for identifier", () => {
statusAPI.set(identifier, status);
let resultingStatus = statusAPI.get(identifier);
expect(resultingStatus).toEqual(status);
statusAPI.delete(identifier);
resultingStatus = statusAPI.get(identifier);
expect(resultingStatus).toBeUndefined();
});
});
describe("observe function", () => {
it("allows callbacks to be attached to status set and delete events", () => {
let unsubscribe = statusAPI.observe(identifier, callback);
statusAPI.set(identifier, status);
expect(callback).toHaveBeenCalledWith(status);
statusAPI.delete(identifier);
expect(callback).toHaveBeenCalledWith(undefined);
unsubscribe();
});
it("returns a unsubscribe function", () => {
let unsubscribe = statusAPI.observe(identifier, callback);
unsubscribe();
statusAPI.set(identifier, status);
expect(callback).toHaveBeenCalledTimes(0);
});
it('returns a unsubscribe function', () => {
let unsubscribe = statusAPI.observe(identifier, callback);
unsubscribe();
statusAPI.set(identifier, status);
expect(callback).toHaveBeenCalledTimes(0);
});
});
});

View File

@@ -20,109 +20,105 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'lodash'
], function (
_
) {
define(['lodash'], function (_) {
/**
* This is the default metadata provider; for any object with a "telemetry"
* property, this provider will return the value of that property as the
* telemetry metadata.
*
* This provider also implements legacy support for telemetry metadata
* defined on the type. Telemetry metadata definitions on type will be
* depreciated in the future.
*/
function DefaultMetadataProvider(openmct) {
this.openmct = openmct;
}
/**
* This is the default metadata provider; for any object with a "telemetry"
* property, this provider will return the value of that property as the
* telemetry metadata.
*
* This provider also implements legacy support for telemetry metadata
* defined on the type. Telemetry metadata definitions on type will be
* depreciated in the future.
*/
function DefaultMetadataProvider(openmct) {
this.openmct = openmct;
/**
* Applies to any domain object with a telemetry property, or whose type
* definition has a telemetry property.
*/
DefaultMetadataProvider.prototype.supportsMetadata = function (domainObject) {
return Boolean(domainObject.telemetry) || Boolean(this.typeHasTelemetry(domainObject));
};
/**
* Retrieves valueMetadata from legacy metadata.
* @private
*/
function valueMetadatasFromOldFormat(metadata) {
const valueMetadatas = [];
valueMetadatas.push({
key: 'name',
name: 'Name'
});
metadata.domains.forEach(function (domain, index) {
const valueMetadata = _.clone(domain);
valueMetadata.hints = {
domain: index + 1
};
valueMetadatas.push(valueMetadata);
});
metadata.ranges.forEach(function (range, index) {
const valueMetadata = _.clone(range);
valueMetadata.hints = {
range: index,
priority: index + metadata.domains.length + 1
};
if (valueMetadata.type === 'enum') {
valueMetadata.key = 'enum';
valueMetadata.hints.y -= 10;
valueMetadata.hints.range -= 10;
valueMetadata.enumerations = _.sortBy(
valueMetadata.enumerations.map(function (e) {
return {
string: e.string,
value: Number(e.value)
};
}),
'e.value'
);
valueMetadata.values = valueMetadata.enumerations.map((e) => e.value);
valueMetadata.max = Math.max(valueMetadata.values);
valueMetadata.min = Math.min(valueMetadata.values);
}
valueMetadatas.push(valueMetadata);
});
return valueMetadatas;
}
/**
* Returns telemetry metadata for a given domain object.
*/
DefaultMetadataProvider.prototype.getMetadata = function (domainObject) {
const metadata = domainObject.telemetry || {};
if (this.typeHasTelemetry(domainObject)) {
const typeMetadata = this.openmct.types.get(domainObject.type).definition.telemetry;
Object.assign(metadata, typeMetadata);
if (!metadata.values) {
metadata.values = valueMetadatasFromOldFormat(metadata);
}
}
/**
* Applies to any domain object with a telemetry property, or whose type
* definition has a telemetry property.
*/
DefaultMetadataProvider.prototype.supportsMetadata = function (domainObject) {
return Boolean(domainObject.telemetry) || Boolean(this.typeHasTelemetry(domainObject));
};
return metadata;
};
/**
* Retrieves valueMetadata from legacy metadata.
* @private
*/
function valueMetadatasFromOldFormat(metadata) {
const valueMetadatas = [];
/**
* @private
*/
DefaultMetadataProvider.prototype.typeHasTelemetry = function (domainObject) {
const type = this.openmct.types.get(domainObject.type);
valueMetadatas.push({
key: 'name',
name: 'Name'
});
metadata.domains.forEach(function (domain, index) {
const valueMetadata = _.clone(domain);
valueMetadata.hints = {
domain: index + 1
};
valueMetadatas.push(valueMetadata);
});
metadata.ranges.forEach(function (range, index) {
const valueMetadata = _.clone(range);
valueMetadata.hints = {
range: index,
priority: index + metadata.domains.length + 1
};
if (valueMetadata.type === 'enum') {
valueMetadata.key = 'enum';
valueMetadata.hints.y -= 10;
valueMetadata.hints.range -= 10;
valueMetadata.enumerations =
_.sortBy(valueMetadata.enumerations.map(function (e) {
return {
string: e.string,
value: Number(e.value)
};
}), 'e.value');
valueMetadata.values = valueMetadata.enumerations.map(e => e.value);
valueMetadata.max = Math.max(valueMetadata.values);
valueMetadata.min = Math.min(valueMetadata.values);
}
valueMetadatas.push(valueMetadata);
});
return valueMetadatas;
}
/**
* Returns telemetry metadata for a given domain object.
*/
DefaultMetadataProvider.prototype.getMetadata = function (domainObject) {
const metadata = domainObject.telemetry || {};
if (this.typeHasTelemetry(domainObject)) {
const typeMetadata = this.openmct.types.get(domainObject.type).definition.telemetry;
Object.assign(metadata, typeMetadata);
if (!metadata.values) {
metadata.values = valueMetadatasFromOldFormat(metadata);
}
}
return metadata;
};
/**
* @private
*/
DefaultMetadataProvider.prototype.typeHasTelemetry = function (domainObject) {
const type = this.openmct.types.get(domainObject.type);
return Boolean(type.definition.telemetry);
};
return DefaultMetadataProvider;
return Boolean(type.definition.telemetry);
};
return DefaultMetadataProvider;
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -27,462 +27,454 @@ import { LOADED_ERROR, TIMESYSTEM_KEY_NOTIFICATION, TIMESYSTEM_KEY_WARNING } fro
/** Class representing a Telemetry Collection. */
export default class TelemetryCollection extends EventEmitter {
/**
* Creates a Telemetry Collection
*
* @param {OpenMCT} openmct - Open MCT
* @param {module:openmct.DomainObject} domainObject - Domain Object to use for telemetry collection
* @param {object} options - Any options passed in for request/subscribe
*/
constructor(openmct, domainObject, options) {
super();
/**
* Creates a Telemetry Collection
*
* @param {OpenMCT} openmct - Open MCT
* @param {module:openmct.DomainObject} domainObject - Domain Object to use for telemetry collection
* @param {object} options - Any options passed in for request/subscribe
*/
constructor(openmct, domainObject, options) {
super();
this.loaded = false;
this.openmct = openmct;
this.domainObject = domainObject;
this.boundedTelemetry = [];
this.futureBuffer = [];
this.parseTime = undefined;
this.metadata = this.openmct.telemetry.getMetadata(domainObject);
this.unsubscribe = undefined;
this.options = options;
this.pageState = undefined;
this.lastBounds = undefined;
this.requestAbort = undefined;
this.isStrategyLatest = this.options.strategy === 'latest';
this.dataOutsideTimeBounds = false;
this.loaded = false;
this.openmct = openmct;
this.domainObject = domainObject;
this.boundedTelemetry = [];
this.futureBuffer = [];
this.parseTime = undefined;
this.metadata = this.openmct.telemetry.getMetadata(domainObject);
this.unsubscribe = undefined;
this.options = options;
this.pageState = undefined;
this.lastBounds = undefined;
this.requestAbort = undefined;
this.isStrategyLatest = this.options.strategy === 'latest';
this.dataOutsideTimeBounds = false;
}
/**
* This will start the requests for historical and realtime data,
* as well as setting up initial values and watchers
*/
load() {
if (this.loaded) {
this._error(LOADED_ERROR);
}
/**
* This will start the requests for historical and realtime data,
* as well as setting up initial values and watchers
*/
load() {
if (this.loaded) {
this._error(LOADED_ERROR);
}
this._setTimeSystem(this.openmct.time.timeSystem());
this.lastBounds = this.openmct.time.bounds();
this._setTimeSystem(this.openmct.time.timeSystem());
this.lastBounds = this.openmct.time.bounds();
this._watchBounds();
this._watchTimeSystem();
this._watchBounds();
this._watchTimeSystem();
this._requestHistoricalTelemetry();
this._initiateSubscriptionTelemetry();
this._requestHistoricalTelemetry();
this._initiateSubscriptionTelemetry();
this.loaded = true;
}
this.loaded = true;
/**
* can/should be called by the requester of the telemetry collection
* to remove any listeners
*/
destroy() {
if (this.requestAbort) {
this.requestAbort.abort();
}
/**
* can/should be called by the requester of the telemetry collection
* to remove any listeners
*/
destroy() {
if (this.requestAbort) {
this.requestAbort.abort();
}
this._unwatchBounds();
this._unwatchTimeSystem();
if (this.unsubscribe) {
this.unsubscribe();
}
this.removeAllListeners();
this._unwatchBounds();
this._unwatchTimeSystem();
if (this.unsubscribe) {
this.unsubscribe();
}
/**
* This will start the requests for historical and realtime data,
* as well as setting up initial values and watchers
*/
getAll() {
return this.boundedTelemetry;
this.removeAllListeners();
}
/**
* This will start the requests for historical and realtime data,
* as well as setting up initial values and watchers
*/
getAll() {
return this.boundedTelemetry;
}
/**
* If a historical provider exists, then historical requests will be made
* @private
*/
async _requestHistoricalTelemetry() {
let options = { ...this.options };
let historicalProvider;
this.openmct.telemetry.standardizeRequestOptions(options);
historicalProvider = this.openmct.telemetry.findRequestProvider(this.domainObject, options);
if (!historicalProvider) {
return;
}
/**
* If a historical provider exists, then historical requests will be made
* @private
*/
async _requestHistoricalTelemetry() {
let options = { ...this.options };
let historicalProvider;
let historicalData;
this.openmct.telemetry.standardizeRequestOptions(options);
historicalProvider = this.openmct.telemetry.
findRequestProvider(this.domainObject, options);
options.onPartialResponse = this._processNewTelemetry.bind(this);
if (!historicalProvider) {
return;
}
let historicalData;
options.onPartialResponse = this._processNewTelemetry.bind(this);
try {
if (this.requestAbort) {
this.requestAbort.abort();
}
this.requestAbort = new AbortController();
options.signal = this.requestAbort.signal;
this.emit('requestStarted');
const modifiedOptions = await this.openmct.telemetry.applyRequestInterceptors(this.domainObject, options);
historicalData = await historicalProvider.request(this.domainObject, modifiedOptions);
} catch (error) {
if (error.name !== 'AbortError') {
console.error('Error requesting telemetry data...');
this._error(error);
}
}
this.emit('requestEnded');
this.requestAbort = undefined;
if (!historicalData || !historicalData.length) {
return;
}
this._processNewTelemetry(historicalData);
try {
if (this.requestAbort) {
this.requestAbort.abort();
}
this.requestAbort = new AbortController();
options.signal = this.requestAbort.signal;
this.emit('requestStarted');
const modifiedOptions = await this.openmct.telemetry.applyRequestInterceptors(
this.domainObject,
options
);
historicalData = await historicalProvider.request(this.domainObject, modifiedOptions);
} catch (error) {
if (error.name !== 'AbortError') {
console.error('Error requesting telemetry data...');
this._error(error);
}
}
/**
* This uses the built in subscription function from Telemetry API
* @private
*/
_initiateSubscriptionTelemetry() {
this.emit('requestEnded');
this.requestAbort = undefined;
if (this.unsubscribe) {
this.unsubscribe();
}
this.unsubscribe = this.openmct.telemetry
.subscribe(
this.domainObject,
datum => this._processNewTelemetry(datum),
this.options
);
if (!historicalData || !historicalData.length) {
return;
}
/**
* Filter any new telemetry (add/page, historical, subscription) based on
* time bounds and dupes
*
* @param {(Object|Object[])} telemetryData - telemetry data object or
* array of telemetry data objects
* @private
*/
_processNewTelemetry(telemetryData) {
if (telemetryData === undefined) {
return;
}
this._processNewTelemetry(historicalData);
}
let latestBoundedDatum = this.boundedTelemetry[this.boundedTelemetry.length - 1];
let data = Array.isArray(telemetryData) ? telemetryData : [telemetryData];
let parsedValue;
let beforeStartOfBounds;
let afterEndOfBounds;
let added = [];
let addedIndices = [];
let hasDataBeforeStartBound = false;
// loop through, sort and dedupe
for (let datum of data) {
parsedValue = this.parseTime(datum);
beforeStartOfBounds = parsedValue < this.lastBounds.start;
afterEndOfBounds = parsedValue > this.lastBounds.end;
if (!afterEndOfBounds && (!beforeStartOfBounds || (this.isStrategyLatest && this.openmct.telemetry.greedyLAD()))) {
let isDuplicate = false;
let startIndex = this._sortedIndex(datum);
let endIndex = undefined;
// dupe check
if (startIndex !== this.boundedTelemetry.length) {
endIndex = _.sortedLastIndexBy(
this.boundedTelemetry,
datum,
boundedDatum => this.parseTime(boundedDatum)
);
if (endIndex > startIndex) {
let potentialDupes = this.boundedTelemetry.slice(startIndex, endIndex);
isDuplicate = potentialDupes.some(_.isEqual.bind(undefined, datum));
}
} else if (startIndex === this.boundedTelemetry.length) {
isDuplicate = _.isEqual(datum, this.boundedTelemetry[this.boundedTelemetry.length - 1]);
}
if (!isDuplicate) {
let index = endIndex || startIndex;
this.boundedTelemetry.splice(index, 0, datum);
addedIndices.push(index);
added.push(datum);
if (!hasDataBeforeStartBound && beforeStartOfBounds) {
hasDataBeforeStartBound = true;
}
}
} else if (afterEndOfBounds) {
this.futureBuffer.push(datum);
}
}
if (added.length) {
// if latest strategy is requested, we need to check if the value is the latest unemitted value
if (this.isStrategyLatest) {
this.boundedTelemetry = [this.boundedTelemetry[this.boundedTelemetry.length - 1]];
// if true, then this value has yet to be emitted
if (this.boundedTelemetry[0] !== latestBoundedDatum) {
if (hasDataBeforeStartBound) {
this._handleDataOutsideBounds();
} else {
this._handleDataInsideBounds();
}
this.emit('add', this.boundedTelemetry);
}
} else {
this.emit('add', added, addedIndices);
}
}
/**
* This uses the built in subscription function from Telemetry API
* @private
*/
_initiateSubscriptionTelemetry() {
if (this.unsubscribe) {
this.unsubscribe();
}
/**
* Finds the correct insertion point for the given telemetry datum.
* Leverages lodash's `sortedIndexBy` function which implements a binary search.
* @private
*/
_sortedIndex(datum) {
if (this.boundedTelemetry.length === 0) {
return 0;
this.unsubscribe = this.openmct.telemetry.subscribe(
this.domainObject,
(datum) => this._processNewTelemetry(datum),
this.options
);
}
/**
* Filter any new telemetry (add/page, historical, subscription) based on
* time bounds and dupes
*
* @param {(Object|Object[])} telemetryData - telemetry data object or
* array of telemetry data objects
* @private
*/
_processNewTelemetry(telemetryData) {
if (telemetryData === undefined) {
return;
}
let latestBoundedDatum = this.boundedTelemetry[this.boundedTelemetry.length - 1];
let data = Array.isArray(telemetryData) ? telemetryData : [telemetryData];
let parsedValue;
let beforeStartOfBounds;
let afterEndOfBounds;
let added = [];
let addedIndices = [];
let hasDataBeforeStartBound = false;
// loop through, sort and dedupe
for (let datum of data) {
parsedValue = this.parseTime(datum);
beforeStartOfBounds = parsedValue < this.lastBounds.start;
afterEndOfBounds = parsedValue > this.lastBounds.end;
if (
!afterEndOfBounds &&
(!beforeStartOfBounds || (this.isStrategyLatest && this.openmct.telemetry.greedyLAD()))
) {
let isDuplicate = false;
let startIndex = this._sortedIndex(datum);
let endIndex = undefined;
// dupe check
if (startIndex !== this.boundedTelemetry.length) {
endIndex = _.sortedLastIndexBy(this.boundedTelemetry, datum, (boundedDatum) =>
this.parseTime(boundedDatum)
);
if (endIndex > startIndex) {
let potentialDupes = this.boundedTelemetry.slice(startIndex, endIndex);
isDuplicate = potentialDupes.some(_.isEqual.bind(undefined, datum));
}
} else if (startIndex === this.boundedTelemetry.length) {
isDuplicate = _.isEqual(datum, this.boundedTelemetry[this.boundedTelemetry.length - 1]);
}
let parsedValue = this.parseTime(datum);
let lastValue = this.parseTime(this.boundedTelemetry[this.boundedTelemetry.length - 1]);
if (!isDuplicate) {
let index = endIndex || startIndex;
if (parsedValue > lastValue || parsedValue === lastValue) {
return this.boundedTelemetry.length;
this.boundedTelemetry.splice(index, 0, datum);
addedIndices.push(index);
added.push(datum);
if (!hasDataBeforeStartBound && beforeStartOfBounds) {
hasDataBeforeStartBound = true;
}
}
} else if (afterEndOfBounds) {
this.futureBuffer.push(datum);
}
}
if (added.length) {
// if latest strategy is requested, we need to check if the value is the latest unemitted value
if (this.isStrategyLatest) {
this.boundedTelemetry = [this.boundedTelemetry[this.boundedTelemetry.length - 1]];
// if true, then this value has yet to be emitted
if (this.boundedTelemetry[0] !== latestBoundedDatum) {
if (hasDataBeforeStartBound) {
this._handleDataOutsideBounds();
} else {
this._handleDataInsideBounds();
}
this.emit('add', this.boundedTelemetry);
}
} else {
this.emit('add', added, addedIndices);
}
}
}
/**
* Finds the correct insertion point for the given telemetry datum.
* Leverages lodash's `sortedIndexBy` function which implements a binary search.
* @private
*/
_sortedIndex(datum) {
if (this.boundedTelemetry.length === 0) {
return 0;
}
let parsedValue = this.parseTime(datum);
let lastValue = this.parseTime(this.boundedTelemetry[this.boundedTelemetry.length - 1]);
if (parsedValue > lastValue || parsedValue === lastValue) {
return this.boundedTelemetry.length;
} else {
return _.sortedIndexBy(this.boundedTelemetry, datum, (boundedDatum) =>
this.parseTime(boundedDatum)
);
}
}
/**
* when the start time, end time, or both have been updated.
* data could be added OR removed here we update the current
* bounded telemetry
*
* @param {TimeConductorBounds} bounds The newly updated bounds
* @param {boolean} [tick] `true` if the bounds update was due to
* a "tick" event (ie. was an automatic update), false otherwise.
* @private
*/
_bounds(bounds, isTick) {
let startChanged = this.lastBounds.start !== bounds.start;
let endChanged = this.lastBounds.end !== bounds.end;
this.lastBounds = bounds;
if (isTick) {
if (this.timeKey === undefined) {
return;
}
// need to check futureBuffer and need to check
// if anything has fallen out of bounds
let startIndex = 0;
let endIndex = 0;
let discarded = [];
let added = [];
let testDatum = {};
if (endChanged) {
testDatum[this.timeKey] = bounds.end;
// Calculate the new index of the last item in bounds
endIndex = _.sortedLastIndexBy(this.futureBuffer, testDatum, (datum) =>
this.parseTime(datum)
);
added = this.futureBuffer.splice(0, endIndex);
}
if (startChanged) {
testDatum[this.timeKey] = bounds.start;
// a little more complicated if not latest strategy
if (!this.isStrategyLatest) {
// Calculate the new index of the first item within the bounds
startIndex = _.sortedIndexBy(this.boundedTelemetry, testDatum, (datum) =>
this.parseTime(datum)
);
discarded = this.boundedTelemetry.splice(0, startIndex);
} else if (this.parseTime(testDatum) > this.parseTime(this.boundedTelemetry[0])) {
// if greedyLAD is active and there is no new data to replace, don't discard
const isGreedyLAD = this.openmct.telemetry.greedyLAD();
const shouldRemove = !isGreedyLAD || (isGreedyLAD && added.length > 0);
if (shouldRemove) {
discarded = this.boundedTelemetry;
this.boundedTelemetry = [];
// since it IS strategy latest, we can assume there will be at least 1 datum
// unless no data was returned in the first request, we need to account for that
} else if (this.boundedTelemetry.length === 1) {
this._handleDataOutsideBounds();
}
}
}
if (discarded.length > 0) {
this.emit('remove', discarded);
}
if (added.length > 0) {
if (!this.isStrategyLatest) {
this.boundedTelemetry = [...this.boundedTelemetry, ...added];
} else {
return _.sortedIndexBy(
this.boundedTelemetry,
datum,
boundedDatum => this.parseTime(boundedDatum)
);
}
}
this._handleDataInsideBounds();
/**
* when the start time, end time, or both have been updated.
* data could be added OR removed here we update the current
* bounded telemetry
*
* @param {TimeConductorBounds} bounds The newly updated bounds
* @param {boolean} [tick] `true` if the bounds update was due to
* a "tick" event (ie. was an automatic update), false otherwise.
* @private
*/
_bounds(bounds, isTick) {
let startChanged = this.lastBounds.start !== bounds.start;
let endChanged = this.lastBounds.end !== bounds.end;
this.lastBounds = bounds;
if (isTick) {
if (this.timeKey === undefined) {
return;
}
// need to check futureBuffer and need to check
// if anything has fallen out of bounds
let startIndex = 0;
let endIndex = 0;
let discarded = [];
let added = [];
let testDatum = {};
if (endChanged) {
testDatum[this.timeKey] = bounds.end;
// Calculate the new index of the last item in bounds
endIndex = _.sortedLastIndexBy(
this.futureBuffer,
testDatum,
datum => this.parseTime(datum)
);
added = this.futureBuffer.splice(0, endIndex);
}
if (startChanged) {
testDatum[this.timeKey] = bounds.start;
// a little more complicated if not latest strategy
if (!this.isStrategyLatest) {
// Calculate the new index of the first item within the bounds
startIndex = _.sortedIndexBy(
this.boundedTelemetry,
testDatum,
datum => this.parseTime(datum)
);
discarded = this.boundedTelemetry.splice(0, startIndex);
} else if (this.parseTime(testDatum) > this.parseTime(this.boundedTelemetry[0])) {
// if greedyLAD is active and there is no new data to replace, don't discard
const isGreedyLAD = this.openmct.telemetry.greedyLAD();
const shouldRemove = (!isGreedyLAD || (isGreedyLAD && added.length > 0));
if (shouldRemove) {
discarded = this.boundedTelemetry;
this.boundedTelemetry = [];
// since it IS strategy latest, we can assume there will be at least 1 datum
// unless no data was returned in the first request, we need to account for that
} else if (this.boundedTelemetry.length === 1) {
this._handleDataOutsideBounds();
}
}
}
if (discarded.length > 0) {
this.emit('remove', discarded);
}
if (added.length > 0) {
if (!this.isStrategyLatest) {
this.boundedTelemetry = [...this.boundedTelemetry, ...added];
} else {
this._handleDataInsideBounds();
added = [added[added.length - 1]];
this.boundedTelemetry = added;
}
// Assumption is that added will be of length 1 here, so just send the last index of the boundedTelemetry in the add event
this.emit('add', added, [this.boundedTelemetry.length]);
}
} else {
// user bounds change, reset
this._reset();
added = [added[added.length - 1]];
this.boundedTelemetry = added;
}
// Assumption is that added will be of length 1 here, so just send the last index of the boundedTelemetry in the add event
this.emit('add', added, [this.boundedTelemetry.length]);
}
} else {
// user bounds change, reset
this._reset();
}
}
_handleDataInsideBounds() {
if (this.dataOutsideTimeBounds) {
this.dataOutsideTimeBounds = false;
this.emit('dataInsideTimeBounds');
}
}
_handleDataOutsideBounds() {
if (!this.dataOutsideTimeBounds) {
this.dataOutsideTimeBounds = true;
this.emit('dataOutsideTimeBounds');
}
}
/**
* whenever the time system is updated need to update related values in
* the Telemetry Collection and reset the telemetry collection
*
* @param {TimeSystem} timeSystem - the value of the currently applied
* Time System
* @private
*/
_setTimeSystem(timeSystem) {
let domains = [];
let metadataValue = { format: timeSystem.key };
if (this.metadata) {
domains = this.metadata.valuesForHints(['domain']);
metadataValue = this.metadata.value(timeSystem.key) || { format: timeSystem.key };
}
_handleDataInsideBounds() {
if (this.dataOutsideTimeBounds) {
this.dataOutsideTimeBounds = false;
this.emit('dataInsideTimeBounds');
}
let domain = domains.find((d) => d.key === timeSystem.key);
if (domain !== undefined) {
// timeKey is used to create a dummy datum used for sorting
this.timeKey = domain.source;
} else {
this.timeKey = undefined;
this._warn(TIMESYSTEM_KEY_WARNING);
this.openmct.notifications.alert(TIMESYSTEM_KEY_NOTIFICATION);
}
_handleDataOutsideBounds() {
if (!this.dataOutsideTimeBounds) {
this.dataOutsideTimeBounds = true;
this.emit('dataOutsideTimeBounds');
}
}
let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
/**
* whenever the time system is updated need to update related values in
* the Telemetry Collection and reset the telemetry collection
*
* @param {TimeSystem} timeSystem - the value of the currently applied
* Time System
* @private
*/
_setTimeSystem(timeSystem) {
let domains = [];
let metadataValue = { format: timeSystem.key };
this.parseTime = (datum) => {
return valueFormatter.parse(datum);
};
}
if (this.metadata) {
domains = this.metadata.valuesForHints(['domain']);
metadataValue = this.metadata.value(timeSystem.key) || { format: timeSystem.key };
}
_setTimeSystemAndFetchData(timeSystem) {
this._setTimeSystem(timeSystem);
this._reset();
}
let domain = domains.find((d) => d.key === timeSystem.key);
/**
* Reset the telemetry data of the collection, and re-request
* historical telemetry
* @private
*
* @todo handle subscriptions more granually
*/
_reset() {
this.boundedTelemetry = [];
this.futureBuffer = [];
if (domain !== undefined) {
// timeKey is used to create a dummy datum used for sorting
this.timeKey = domain.source;
} else {
this.timeKey = undefined;
this.emit('clear');
this._warn(TIMESYSTEM_KEY_WARNING);
this.openmct.notifications.alert(TIMESYSTEM_KEY_NOTIFICATION);
}
this._requestHistoricalTelemetry();
}
let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
/**
* adds the _bounds callback to the 'bounds' timeAPI listener
* @private
*/
_watchBounds() {
this.openmct.time.on('bounds', this._bounds, this);
}
this.parseTime = (datum) => {
return valueFormatter.parse(datum);
};
}
/**
* removes the _bounds callback from the 'bounds' timeAPI listener
* @private
*/
_unwatchBounds() {
this.openmct.time.off('bounds', this._bounds, this);
}
_setTimeSystemAndFetchData(timeSystem) {
this._setTimeSystem(timeSystem);
this._reset();
}
/**
* adds the _setTimeSystemAndFetchData callback to the 'timeSystem' timeAPI listener
* @private
*/
_watchTimeSystem() {
this.openmct.time.on('timeSystem', this._setTimeSystemAndFetchData, this);
}
/**
* Reset the telemetry data of the collection, and re-request
* historical telemetry
* @private
*
* @todo handle subscriptions more granually
*/
_reset() {
this.boundedTelemetry = [];
this.futureBuffer = [];
/**
* removes the _setTimeSystemAndFetchData callback from the 'timeSystem' timeAPI listener
* @private
*/
_unwatchTimeSystem() {
this.openmct.time.off('timeSystem', this._setTimeSystemAndFetchData, this);
}
this.emit('clear');
/**
* will throw a new Error, for passed in message
* @param {string} message Message describing the error
* @private
*/
_error(message) {
throw new Error(message);
}
this._requestHistoricalTelemetry();
}
/**
* adds the _bounds callback to the 'bounds' timeAPI listener
* @private
*/
_watchBounds() {
this.openmct.time.on('bounds', this._bounds, this);
}
/**
* removes the _bounds callback from the 'bounds' timeAPI listener
* @private
*/
_unwatchBounds() {
this.openmct.time.off('bounds', this._bounds, this);
}
/**
* adds the _setTimeSystemAndFetchData callback to the 'timeSystem' timeAPI listener
* @private
*/
_watchTimeSystem() {
this.openmct.time.on('timeSystem', this._setTimeSystemAndFetchData, this);
}
/**
* removes the _setTimeSystemAndFetchData callback from the 'timeSystem' timeAPI listener
* @private
*/
_unwatchTimeSystem() {
this.openmct.time.off('timeSystem', this._setTimeSystemAndFetchData, this);
}
/**
* will throw a new Error, for passed in message
* @param {string} message Message describing the error
* @private
*/
_error(message) {
throw new Error(message);
}
_warn(message) {
console.warn(message);
}
_warn(message) {
console.warn(message);
}
}

View File

@@ -20,82 +20,79 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import {
createOpenMct,
resetApplicationState
} from 'utils/testing';
import { createOpenMct, resetApplicationState } from 'utils/testing';
import { TIMESYSTEM_KEY_WARNING } from './constants';
describe('Telemetry Collection', () => {
let openmct;
let mockMetadataProvider;
let mockMetadata = {};
let domainObject;
let openmct;
let mockMetadataProvider;
let mockMetadata = {};
let domainObject;
beforeEach(done => {
openmct = createOpenMct();
openmct.on('start', done);
beforeEach((done) => {
openmct = createOpenMct();
openmct.on('start', done);
domainObject = {
identifier: {
key: 'a',
namespace: 'b'
},
type: 'sample-type'
};
domainObject = {
identifier: {
key: 'a',
namespace: 'b'
},
type: 'sample-type'
};
mockMetadataProvider = {
key: 'mockMetadataProvider',
supportsMetadata() {
return true;
},
getMetadata() {
return mockMetadata;
}
};
mockMetadataProvider = {
key: 'mockMetadataProvider',
supportsMetadata() {
return true;
},
getMetadata() {
return mockMetadata;
}
};
openmct.telemetry.addProvider(mockMetadataProvider);
openmct.startHeadless();
});
openmct.telemetry.addProvider(mockMetadataProvider);
openmct.startHeadless();
});
afterEach(() => {
return resetApplicationState();
});
afterEach(() => {
return resetApplicationState();
});
it('Warns if telemetry metadata does not match the active timesystem', () => {
mockMetadata.values = [
{
key: 'foo',
name: 'Bar',
hints: {
domain: 1
}
}
];
it('Warns if telemetry metadata does not match the active timesystem', () => {
mockMetadata.values = [
{
key: 'foo',
name: 'Bar',
hints: {
domain: 1
}
}
];
const telemetryCollection = openmct.telemetry.requestCollection(domainObject);
spyOn(telemetryCollection, '_warn');
telemetryCollection.load();
const telemetryCollection = openmct.telemetry.requestCollection(domainObject);
spyOn(telemetryCollection, '_warn');
telemetryCollection.load();
expect(telemetryCollection._warn).toHaveBeenCalledOnceWith(TIMESYSTEM_KEY_WARNING);
});
expect(telemetryCollection._warn).toHaveBeenCalledOnceWith(TIMESYSTEM_KEY_WARNING);
});
it('Does not warn if telemetry metadata matches the active timesystem', () => {
mockMetadata.values = [
{
key: 'utc',
name: 'Timestamp',
format: 'utc',
hints: {
domain: 1
}
}
];
it('Does not warn if telemetry metadata matches the active timesystem', () => {
mockMetadata.values = [
{
key: 'utc',
name: 'Timestamp',
format: 'utc',
hints: {
domain: 1
}
}
];
const telemetryCollection = openmct.telemetry.requestCollection(domainObject);
spyOn(telemetryCollection, '_warn');
telemetryCollection.load();
const telemetryCollection = openmct.telemetry.requestCollection(domainObject);
spyOn(telemetryCollection, '_warn');
telemetryCollection.load();
expect(telemetryCollection._warn).not.toHaveBeenCalled();
});
expect(telemetryCollection._warn).not.toHaveBeenCalled();
});
});

View File

@@ -20,139 +20,135 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'lodash'
], function (
_
) {
define(['lodash'], function (_) {
function applyReasonableDefaults(valueMetadata, index) {
valueMetadata.source = valueMetadata.source || valueMetadata.key;
valueMetadata.hints = valueMetadata.hints || {};
function applyReasonableDefaults(valueMetadata, index) {
valueMetadata.source = valueMetadata.source || valueMetadata.key;
valueMetadata.hints = valueMetadata.hints || {};
if (Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'x')) {
if (!Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'domain')) {
valueMetadata.hints.domain = valueMetadata.hints.x;
}
if (Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'x')) {
if (!Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'domain')) {
valueMetadata.hints.domain = valueMetadata.hints.x;
}
delete valueMetadata.hints.x;
}
if (Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'y')) {
if (!Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'range')) {
valueMetadata.hints.range = valueMetadata.hints.y;
}
delete valueMetadata.hints.y;
}
if (valueMetadata.format === 'enum') {
if (!valueMetadata.values) {
valueMetadata.values = valueMetadata.enumerations.map(e => e.value);
}
if (!Object.prototype.hasOwnProperty.call(valueMetadata, 'max')) {
valueMetadata.max = Math.max(valueMetadata.values) + 1;
}
if (!Object.prototype.hasOwnProperty.call(valueMetadata, 'min')) {
valueMetadata.min = Math.min(valueMetadata.values) - 1;
}
}
if (!Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'priority')) {
valueMetadata.hints.priority = index;
}
return valueMetadata;
delete valueMetadata.hints.x;
}
/**
* Utility class for handling and inspecting telemetry metadata. Applies
* reasonable defaults to simplify the task of providing metadata, while
* also providing methods for interrogating telemetry metadata.
*/
function TelemetryMetadataManager(metadata) {
this.metadata = metadata;
if (Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'y')) {
if (!Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'range')) {
valueMetadata.hints.range = valueMetadata.hints.y;
}
this.valueMetadatas = this.metadata.values ? this.metadata.values.map(applyReasonableDefaults) : [];
delete valueMetadata.hints.y;
}
/**
* Get value metadata for a single key.
*/
TelemetryMetadataManager.prototype.value = function (key) {
return this.valueMetadatas.filter(function (metadata) {
return metadata.key === key;
})[0];
};
if (valueMetadata.format === 'enum') {
if (!valueMetadata.values) {
valueMetadata.values = valueMetadata.enumerations.map((e) => e.value);
}
/**
* Returns all value metadatas, sorted by priority.
*/
TelemetryMetadataManager.prototype.values = function () {
return this.valuesForHints(['priority']);
};
if (!Object.prototype.hasOwnProperty.call(valueMetadata, 'max')) {
valueMetadata.max = Math.max(valueMetadata.values) + 1;
}
/**
* Get an array of valueMetadatas that posess all hints requested.
* Array is sorted based on hint priority.
*
*/
TelemetryMetadataManager.prototype.valuesForHints = function (
hints
) {
function hasHint(hint) {
// eslint-disable-next-line no-invalid-this
return Object.prototype.hasOwnProperty.call(this.hints, hint);
}
if (!Object.prototype.hasOwnProperty.call(valueMetadata, 'min')) {
valueMetadata.min = Math.min(valueMetadata.values) - 1;
}
}
function hasHints(metadata) {
return hints.every(hasHint, metadata);
}
if (!Object.prototype.hasOwnProperty.call(valueMetadata.hints, 'priority')) {
valueMetadata.hints.priority = index;
}
const matchingMetadata = this.valueMetadatas.filter(hasHints);
let iteratees = hints.map(hint => {
return (metadata) => {
return metadata.hints[hint];
};
});
return valueMetadata;
}
return _.sortBy(matchingMetadata, ...iteratees);
};
/**
* Utility class for handling and inspecting telemetry metadata. Applies
* reasonable defaults to simplify the task of providing metadata, while
* also providing methods for interrogating telemetry metadata.
*/
function TelemetryMetadataManager(metadata) {
this.metadata = metadata;
/**
* check out of a given metadata has array values
*/
TelemetryMetadataManager.prototype.isArrayValue = function (metadata) {
const regex = /\[\]$/g;
if (!metadata.format && !metadata.formatString) {
return false;
}
this.valueMetadatas = this.metadata.values
? this.metadata.values.map(applyReasonableDefaults)
: [];
}
return (metadata.format || metadata.formatString).match(regex) !== null;
};
/**
* Get value metadata for a single key.
*/
TelemetryMetadataManager.prototype.value = function (key) {
return this.valueMetadatas.filter(function (metadata) {
return metadata.key === key;
})[0];
};
TelemetryMetadataManager.prototype.getFilterableValues = function () {
return this.valueMetadatas.filter(metadatum => metadatum.filters && metadatum.filters.length > 0);
};
/**
* Returns all value metadatas, sorted by priority.
*/
TelemetryMetadataManager.prototype.values = function () {
return this.valuesForHints(['priority']);
};
TelemetryMetadataManager.prototype.getDefaultDisplayValue = function () {
let valueMetadata = this.valuesForHints(['range'])[0];
/**
* Get an array of valueMetadatas that posess all hints requested.
* Array is sorted based on hint priority.
*
*/
TelemetryMetadataManager.prototype.valuesForHints = function (hints) {
function hasHint(hint) {
// eslint-disable-next-line no-invalid-this
return Object.prototype.hasOwnProperty.call(this.hints, hint);
}
if (valueMetadata === undefined) {
valueMetadata = this.values().filter(values => {
return !(values.hints.domain);
})[0];
}
function hasHints(metadata) {
return hints.every(hasHint, metadata);
}
if (valueMetadata === undefined) {
valueMetadata = this.values()[0];
}
const matchingMetadata = this.valueMetadatas.filter(hasHints);
let iteratees = hints.map((hint) => {
return (metadata) => {
return metadata.hints[hint];
};
});
return valueMetadata;
};
return _.sortBy(matchingMetadata, ...iteratees);
};
return TelemetryMetadataManager;
/**
* check out of a given metadata has array values
*/
TelemetryMetadataManager.prototype.isArrayValue = function (metadata) {
const regex = /\[\]$/g;
if (!metadata.format && !metadata.formatString) {
return false;
}
return (metadata.format || metadata.formatString).match(regex) !== null;
};
TelemetryMetadataManager.prototype.getFilterableValues = function () {
return this.valueMetadatas.filter(
(metadatum) => metadatum.filters && metadatum.filters.length > 0
);
};
TelemetryMetadataManager.prototype.getDefaultDisplayValue = function () {
let valueMetadata = this.valuesForHints(['range'])[0];
if (valueMetadata === undefined) {
valueMetadata = this.values().filter((values) => {
return !values.hints.domain;
})[0];
}
if (valueMetadata === undefined) {
valueMetadata = this.values()[0];
}
return valueMetadata;
};
return TelemetryMetadataManager;
});

View File

@@ -21,48 +21,47 @@
*****************************************************************************/
export default class TelemetryRequestInterceptorRegistry {
/**
* A TelemetryRequestInterceptorRegistry maintains the definitions for different interceptors that may be invoked on telemetry
* requests.
* @interface TelemetryRequestInterceptorRegistry
* @memberof module:openmct
*/
constructor() {
this.interceptors = [];
}
/**
* A TelemetryRequestInterceptorRegistry maintains the definitions for different interceptors that may be invoked on telemetry
* requests.
* @interface TelemetryRequestInterceptorRegistry
* @memberof module:openmct
*/
constructor() {
this.interceptors = [];
}
/**
* @interface TelemetryRequestInterceptorDef
* @property {function} appliesTo function that determines if this interceptor should be called for the given identifier/request
* @property {function} invoke function that transforms the provided request and returns the transformed request
* @property {function} priority the priority for this interceptor. A higher number returned has more weight than a lower number
* @memberof module:openmct TelemetryRequestInterceptorRegistry#
*/
/**
* @interface TelemetryRequestInterceptorDef
* @property {function} appliesTo function that determines if this interceptor should be called for the given identifier/request
* @property {function} invoke function that transforms the provided request and returns the transformed request
* @property {function} priority the priority for this interceptor. A higher number returned has more weight than a lower number
* @memberof module:openmct TelemetryRequestInterceptorRegistry#
*/
/**
* Register a new telemetry request interceptor.
*
* @param {module:openmct.RequestInterceptorDef} requestInterceptorDef the interceptor to add
* @method addInterceptor
* @memberof module:openmct.TelemetryRequestInterceptorRegistry#
*/
addInterceptor(interceptorDef) {
//TODO: sort by priority
this.interceptors.push(interceptorDef);
}
/**
* Retrieve all interceptors applicable to a domain object/request.
* @method getInterceptors
* @returns [module:openmct.RequestInterceptorDef] the registered interceptors for this identifier/request
* @memberof module:openmct.TelemetryRequestInterceptorRegistry#
*/
getInterceptors(identifier, request) {
return this.interceptors.filter(interceptor => {
return typeof interceptor.appliesTo === 'function'
&& interceptor.appliesTo(identifier, request);
});
}
/**
* Register a new telemetry request interceptor.
*
* @param {module:openmct.RequestInterceptorDef} requestInterceptorDef the interceptor to add
* @method addInterceptor
* @memberof module:openmct.TelemetryRequestInterceptorRegistry#
*/
addInterceptor(interceptorDef) {
//TODO: sort by priority
this.interceptors.push(interceptorDef);
}
/**
* Retrieve all interceptors applicable to a domain object/request.
* @method getInterceptors
* @returns [module:openmct.RequestInterceptorDef] the registered interceptors for this identifier/request
* @memberof module:openmct.TelemetryRequestInterceptorRegistry#
*/
getInterceptors(identifier, request) {
return this.interceptors.filter((interceptor) => {
return (
typeof interceptor.appliesTo === 'function' && interceptor.appliesTo(identifier, request)
);
});
}
}

View File

@@ -20,137 +20,133 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'lodash',
'printj'
], function (
_,
printj
) {
define(['lodash', 'printj'], function (_, printj) {
// TODO: needs reference to formatService;
function TelemetryValueFormatter(valueMetadata, formatMap) {
const numberFormatter = {
parse: function (x) {
return Number(x);
},
format: function (x) {
return x;
},
validate: function (x) {
return true;
}
};
// TODO: needs reference to formatService;
function TelemetryValueFormatter(valueMetadata, formatMap) {
const numberFormatter = {
parse: function (x) {
return Number(x);
},
format: function (x) {
return x;
},
validate: function (x) {
return true;
}
};
this.valueMetadata = valueMetadata;
this.valueMetadata = valueMetadata;
function getNonArrayValue(value) {
//metadata format could have array formats ex. string[]/number[]
const arrayRegex = /\[\]$/g;
if (value && value.match(arrayRegex)) {
return value.replace(arrayRegex, '');
}
function getNonArrayValue(value) {
//metadata format could have array formats ex. string[]/number[]
const arrayRegex = /\[\]$/g;
if (value && value.match(arrayRegex)) {
return value.replace(arrayRegex, '');
}
return value;
}
let valueMetadataFormat = getNonArrayValue(valueMetadata.format);
//Is there an existing formatter for the format specified? If not, default to number format
this.formatter = formatMap.get(valueMetadataFormat) || numberFormatter;
if (valueMetadataFormat === 'enum') {
this.formatter = {};
this.enumerations = valueMetadata.enumerations.reduce(function (vm, e) {
vm.byValue[e.value] = e.string;
vm.byString[e.string] = e.value;
return vm;
}, {
byValue: {},
byString: {}
});
this.formatter.format = function (value) {
if (Object.prototype.hasOwnProperty.call(this.enumerations.byValue, value)) {
return this.enumerations.byValue[value];
}
return value;
}.bind(this);
this.formatter.parse = function (string) {
if (typeof string === "string") {
if (Object.prototype.hasOwnProperty.call(this.enumerations.byString, string)) {
return this.enumerations.byString[string];
}
}
return Number(string);
}.bind(this);
}
// Check for formatString support once instead of per format call.
if (valueMetadata.formatString) {
const baseFormat = this.formatter.format;
const formatString = getNonArrayValue(valueMetadata.formatString);
this.formatter.format = function (value) {
return printj.sprintf(formatString, baseFormat.call(this, value));
};
}
if (valueMetadataFormat === 'string') {
this.formatter.parse = function (value) {
if (value === undefined) {
return '';
}
if (typeof value === 'string') {
return value;
} else {
return value.toString();
}
};
this.formatter.format = function (value) {
return value;
};
this.formatter.validate = function (value) {
return typeof value === 'string';
};
}
return value;
}
TelemetryValueFormatter.prototype.parse = function (datum) {
const isDatumArray = Array.isArray(datum);
if (_.isObject(datum)) {
const objectDatum = isDatumArray ? datum : datum[this.valueMetadata.source];
if (Array.isArray(objectDatum)) {
return objectDatum.map((item) => {
return this.formatter.parse(item);
});
} else {
return this.formatter.parse(objectDatum);
}
let valueMetadataFormat = getNonArrayValue(valueMetadata.format);
//Is there an existing formatter for the format specified? If not, default to number format
this.formatter = formatMap.get(valueMetadataFormat) || numberFormatter;
if (valueMetadataFormat === 'enum') {
this.formatter = {};
this.enumerations = valueMetadata.enumerations.reduce(
function (vm, e) {
vm.byValue[e.value] = e.string;
vm.byString[e.string] = e.value;
return vm;
},
{
byValue: {},
byString: {}
}
);
this.formatter.format = function (value) {
if (Object.prototype.hasOwnProperty.call(this.enumerations.byValue, value)) {
return this.enumerations.byValue[value];
}
return this.formatter.parse(datum);
};
TelemetryValueFormatter.prototype.format = function (datum) {
const isDatumArray = Array.isArray(datum);
if (_.isObject(datum)) {
const objectDatum = isDatumArray ? datum : datum[this.valueMetadata.source];
if (Array.isArray(objectDatum)) {
return objectDatum.map((item) => {
return this.formatter.format(item);
});
} else {
return this.formatter.format(objectDatum);
}
return value;
}.bind(this);
this.formatter.parse = function (string) {
if (typeof string === 'string') {
if (Object.prototype.hasOwnProperty.call(this.enumerations.byString, string)) {
return this.enumerations.byString[string];
}
}
return this.formatter.format(datum);
};
return Number(string);
}.bind(this);
}
return TelemetryValueFormatter;
// Check for formatString support once instead of per format call.
if (valueMetadata.formatString) {
const baseFormat = this.formatter.format;
const formatString = getNonArrayValue(valueMetadata.formatString);
this.formatter.format = function (value) {
return printj.sprintf(formatString, baseFormat.call(this, value));
};
}
if (valueMetadataFormat === 'string') {
this.formatter.parse = function (value) {
if (value === undefined) {
return '';
}
if (typeof value === 'string') {
return value;
} else {
return value.toString();
}
};
this.formatter.format = function (value) {
return value;
};
this.formatter.validate = function (value) {
return typeof value === 'string';
};
}
}
TelemetryValueFormatter.prototype.parse = function (datum) {
const isDatumArray = Array.isArray(datum);
if (_.isObject(datum)) {
const objectDatum = isDatumArray ? datum : datum[this.valueMetadata.source];
if (Array.isArray(objectDatum)) {
return objectDatum.map((item) => {
return this.formatter.parse(item);
});
} else {
return this.formatter.parse(objectDatum);
}
}
return this.formatter.parse(datum);
};
TelemetryValueFormatter.prototype.format = function (datum) {
const isDatumArray = Array.isArray(datum);
if (_.isObject(datum)) {
const objectDatum = isDatumArray ? datum : datum[this.valueMetadata.source];
if (Array.isArray(objectDatum)) {
return objectDatum.map((item) => {
return this.formatter.format(item);
});
} else {
return this.formatter.format(objectDatum);
}
}
return this.formatter.format(datum);
};
return TelemetryValueFormatter;
});

View File

@@ -20,6 +20,8 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
export const TIMESYSTEM_KEY_WARNING = 'All telemetry metadata must have a telemetry value with a key that matches the key of the active time system.';
export const TIMESYSTEM_KEY_NOTIFICATION = 'Telemetry metadata does not match the active time system.';
export const TIMESYSTEM_KEY_WARNING =
'All telemetry metadata must have a telemetry value with a key that matches the key of the active time system.';
export const TIMESYSTEM_KEY_NOTIFICATION =
'Telemetry metadata does not match the active time system.';
export const LOADED_ERROR = 'Telemetry Collection has already been loaded.';

View File

@@ -20,87 +20,87 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import TimeContext from "./TimeContext";
import TimeContext from './TimeContext';
/**
* The GlobalContext handles getting and setting time of the openmct application in general.
* Views will use this context unless they specify an alternate/independent time context
*/
class GlobalTimeContext extends TimeContext {
constructor() {
super();
constructor() {
super();
//The Time Of Interest
this.toi = undefined;
//The Time Of Interest
this.toi = undefined;
}
/**
* Get or set the start and end time of the time conductor. Basic validation
* of bounds is performed.
*
* @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds
* @throws {Error} Validation error
* @fires module:openmct.TimeAPI~bounds
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
* @memberof module:openmct.TimeAPI#
* @method bounds
*/
bounds(newBounds) {
if (arguments.length > 0) {
super.bounds.call(this, ...arguments);
// If a bounds change results in a TOI outside of the current
// bounds, unset it
if (this.toi < newBounds.start || this.toi > newBounds.end) {
this.timeOfInterest(undefined);
}
}
/**
* Get or set the start and end time of the time conductor. Basic validation
* of bounds is performed.
*
* @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds
* @throws {Error} Validation error
* @fires module:openmct.TimeAPI~bounds
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
* @memberof module:openmct.TimeAPI#
* @method bounds
*/
bounds(newBounds) {
if (arguments.length > 0) {
super.bounds.call(this, ...arguments);
// If a bounds change results in a TOI outside of the current
// bounds, unset it
if (this.toi < newBounds.start || this.toi > newBounds.end) {
this.timeOfInterest(undefined);
}
}
//Return a copy to prevent direct mutation of time conductor bounds.
return JSON.parse(JSON.stringify(this.boundsVal));
}
//Return a copy to prevent direct mutation of time conductor bounds.
return JSON.parse(JSON.stringify(this.boundsVal));
/**
* Update bounds based on provided time and current offsets
* @private
* @param {number} timestamp A time from which bounds will be calculated
* using current offsets.
*/
tick(timestamp) {
super.tick.call(this, ...arguments);
// If a bounds change results in a TOI outside of the current
// bounds, unset it
if (this.toi < this.boundsVal.start || this.toi > this.boundsVal.end) {
this.timeOfInterest(undefined);
}
}
/**
* Get or set the Time of Interest. The Time of Interest is a single point
* in time, and constitutes the temporal focus of application views. It can
* be manipulated by the user from the time conductor or from other views.
* The time of interest can effectively be unset by assigning a value of
* 'undefined'.
* @fires module:openmct.TimeAPI~timeOfInterest
* @param newTOI
* @returns {number} the current time of interest
* @memberof module:openmct.TimeAPI#
* @method timeOfInterest
*/
timeOfInterest(newTOI) {
if (arguments.length > 0) {
this.toi = newTOI;
/**
* The Time of Interest has moved.
* @event timeOfInterest
* @memberof module:openmct.TimeAPI~
* @property {number} Current time of interest
*/
this.emit('timeOfInterest', this.toi);
}
/**
* Update bounds based on provided time and current offsets
* @private
* @param {number} timestamp A time from which bounds will be calculated
* using current offsets.
*/
tick(timestamp) {
super.tick.call(this, ...arguments);
// If a bounds change results in a TOI outside of the current
// bounds, unset it
if (this.toi < this.boundsVal.start || this.toi > this.boundsVal.end) {
this.timeOfInterest(undefined);
}
}
/**
* Get or set the Time of Interest. The Time of Interest is a single point
* in time, and constitutes the temporal focus of application views. It can
* be manipulated by the user from the time conductor or from other views.
* The time of interest can effectively be unset by assigning a value of
* 'undefined'.
* @fires module:openmct.TimeAPI~timeOfInterest
* @param newTOI
* @returns {number} the current time of interest
* @memberof module:openmct.TimeAPI#
* @method timeOfInterest
*/
timeOfInterest(newTOI) {
if (arguments.length > 0) {
this.toi = newTOI;
/**
* The Time of Interest has moved.
* @event timeOfInterest
* @memberof module:openmct.TimeAPI~
* @property {number} Current time of interest
*/
this.emit('timeOfInterest', this.toi);
}
return this.toi;
}
return this.toi;
}
}
export default GlobalTimeContext;

View File

@@ -20,252 +20,250 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import TimeContext, { TIME_CONTEXT_EVENTS } from "./TimeContext";
import TimeContext, { TIME_CONTEXT_EVENTS } from './TimeContext';
/**
* The IndependentTimeContext handles getting and setting time of the openmct application in general.
* Views will use the GlobalTimeContext unless they specify an alternate/independent time context here.
*/
class IndependentTimeContext extends TimeContext {
constructor(openmct, globalTimeContext, objectPath) {
super();
this.openmct = openmct;
this.unlisteners = [];
this.globalTimeContext = globalTimeContext;
// We always start with the global time context.
// This upstream context will be undefined when an independent time context is added later.
this.upstreamTimeContext = this.globalTimeContext;
this.objectPath = objectPath;
this.refreshContext = this.refreshContext.bind(this);
this.resetContext = this.resetContext.bind(this);
this.removeIndependentContext = this.removeIndependentContext.bind(this);
constructor(openmct, globalTimeContext, objectPath) {
super();
this.openmct = openmct;
this.unlisteners = [];
this.globalTimeContext = globalTimeContext;
// We always start with the global time context.
// This upstream context will be undefined when an independent time context is added later.
this.upstreamTimeContext = this.globalTimeContext;
this.objectPath = objectPath;
this.refreshContext = this.refreshContext.bind(this);
this.resetContext = this.resetContext.bind(this);
this.removeIndependentContext = this.removeIndependentContext.bind(this);
this.refreshContext();
this.refreshContext();
this.globalTimeContext.on('refreshContext', this.refreshContext);
this.globalTimeContext.on('removeOwnContext', this.removeIndependentContext);
this.globalTimeContext.on('refreshContext', this.refreshContext);
this.globalTimeContext.on('removeOwnContext', this.removeIndependentContext);
}
bounds(newBounds) {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.bounds(...arguments);
} else {
return super.bounds(...arguments);
}
}
tick(timestamp) {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.tick(...arguments);
} else {
return super.tick(...arguments);
}
}
clockOffsets(offsets) {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.clockOffsets(...arguments);
} else {
return super.clockOffsets(...arguments);
}
}
stopClock() {
if (this.upstreamTimeContext) {
this.upstreamTimeContext.stopClock();
} else {
super.stopClock();
}
}
timeOfInterest(newTOI) {
return this.globalTimeContext.timeOfInterest(...arguments);
}
timeSystem(timeSystemOrKey, bounds) {
return this.globalTimeContext.timeSystem(...arguments);
}
/**
* Set the active clock. Tick source will be immediately subscribed to
* and ticking will begin. Offsets from 'now' must also be provided. A clock
* can be unset by calling {@link stopClock}.
*
* @param {Clock || string} keyOrClock The clock to activate, or its key
* @param {ClockOffsets} offsets on each tick these will be used to calculate
* the start and end bounds. This maintains a sliding time window of a fixed
* width that automatically updates.
* @fires module:openmct.TimeAPI~clock
* @return {Clock} the currently active clock;
*/
clock(keyOrClock, offsets) {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.clock(...arguments);
}
bounds(newBounds) {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.bounds(...arguments);
} else {
return super.bounds(...arguments);
if (arguments.length === 2) {
let clock;
if (typeof keyOrClock === 'string') {
clock = this.globalTimeContext.clocks.get(keyOrClock);
if (clock === undefined) {
throw "Unknown clock '" + keyOrClock + "'. Has it been registered with 'addClock'?";
}
}
tick(timestamp) {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.tick(...arguments);
} else {
return super.tick(...arguments);
} else if (typeof keyOrClock === 'object') {
clock = keyOrClock;
if (!this.globalTimeContext.clocks.has(clock.key)) {
throw "Unknown clock '" + keyOrClock.key + "'. Has it been registered with 'addClock'?";
}
}
const previousClock = this.activeClock;
if (previousClock !== undefined) {
previousClock.off('tick', this.tick);
}
this.activeClock = clock;
/**
* The active clock has changed. Clock can be unset by calling {@link stopClock}
* @event clock
* @memberof module:openmct.TimeAPI~
* @property {Clock} clock The newly activated clock, or undefined
* if the system is no longer following a clock source
*/
this.emit('clock', this.activeClock);
if (this.activeClock !== undefined) {
this.clockOffsets(offsets);
this.activeClock.on('tick', this.tick);
}
} else if (arguments.length === 1) {
throw 'When setting the clock, clock offsets must also be provided';
}
clockOffsets(offsets) {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.clockOffsets(...arguments);
} else {
return super.clockOffsets(...arguments);
}
}
return this.activeClock;
}
stopClock() {
if (this.upstreamTimeContext) {
this.upstreamTimeContext.stopClock();
} else {
super.stopClock();
}
}
timeOfInterest(newTOI) {
return this.globalTimeContext.timeOfInterest(...arguments);
}
timeSystem(timeSystemOrKey, bounds) {
return this.globalTimeContext.timeSystem(...arguments);
}
/**
* Set the active clock. Tick source will be immediately subscribed to
* and ticking will begin. Offsets from 'now' must also be provided. A clock
* can be unset by calling {@link stopClock}.
*
* @param {Clock || string} keyOrClock The clock to activate, or its key
* @param {ClockOffsets} offsets on each tick these will be used to calculate
* the start and end bounds. This maintains a sliding time window of a fixed
* width that automatically updates.
* @fires module:openmct.TimeAPI~clock
* @return {Clock} the currently active clock;
*/
clock(keyOrClock, offsets) {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.clock(...arguments);
}
if (arguments.length === 2) {
let clock;
if (typeof keyOrClock === 'string') {
clock = this.globalTimeContext.clocks.get(keyOrClock);
if (clock === undefined) {
throw "Unknown clock '" + keyOrClock + "'. Has it been registered with 'addClock'?";
}
} else if (typeof keyOrClock === 'object') {
clock = keyOrClock;
if (!this.globalTimeContext.clocks.has(clock.key)) {
throw "Unknown clock '" + keyOrClock.key + "'. Has it been registered with 'addClock'?";
}
}
const previousClock = this.activeClock;
if (previousClock !== undefined) {
previousClock.off("tick", this.tick);
}
this.activeClock = clock;
/**
* The active clock has changed. Clock can be unset by calling {@link stopClock}
* @event clock
* @memberof module:openmct.TimeAPI~
* @property {Clock} clock The newly activated clock, or undefined
* if the system is no longer following a clock source
*/
this.emit("clock", this.activeClock);
if (this.activeClock !== undefined) {
this.clockOffsets(offsets);
this.activeClock.on("tick", this.tick);
}
} else if (arguments.length === 1) {
throw "When setting the clock, clock offsets must also be provided";
}
return this.activeClock;
}
/**
* Causes this time context to follow another time context (either the global context, or another upstream time context)
* This allows views to have their own time context which points to the appropriate upstream context as necessary, achieving nesting.
*/
followTimeContext() {
this.stopFollowingTimeContext();
if (this.upstreamTimeContext) {
TIME_CONTEXT_EVENTS.forEach((eventName) => {
const thisTimeContext = this;
this.upstreamTimeContext.on(eventName, passthrough);
this.unlisteners.push(() => {
thisTimeContext.upstreamTimeContext.off(eventName, passthrough);
});
function passthrough() {
thisTimeContext.emit(eventName, ...arguments);
}
});
}
}
/**
* Stops following any upstream time context
*/
stopFollowingTimeContext() {
this.unlisteners.forEach(unlisten => unlisten());
this.unlisteners = [];
}
resetContext() {
if (this.upstreamTimeContext) {
this.stopFollowingTimeContext();
this.upstreamTimeContext = undefined;
}
}
/**
* Refresh the time context, following any upstream time contexts as necessary
*/
refreshContext(viewKey) {
const key = this.openmct.objects.makeKeyString(this.objectPath[0].identifier);
if (viewKey && key === viewKey) {
return;
}
//this is necessary as the upstream context gets reassigned after this
this.stopFollowingTimeContext();
this.upstreamTimeContext = this.getUpstreamContext();
this.followTimeContext();
// Emit bounds so that views that are changing context get the upstream bounds
this.emit('bounds', this.bounds());
}
hasOwnContext() {
return this.upstreamTimeContext === undefined;
}
getUpstreamContext() {
// If a view has an independent context, don't return an upstream context
// Be aware that when a new independent time context is created, we assign the global context as default
if (this.hasOwnContext()) {
return undefined;
}
let timeContext = this.globalTimeContext;
this.objectPath.some((item, index) => {
const key = this.openmct.objects.makeKeyString(item.identifier);
// we're only interested in parents, not self, so index > 0
const itemContext = this.globalTimeContext.independentContexts.get(key);
if (index > 0 && itemContext && itemContext.hasOwnContext()) {
//upstream time context
timeContext = itemContext;
return true;
}
return false;
/**
* Causes this time context to follow another time context (either the global context, or another upstream time context)
* This allows views to have their own time context which points to the appropriate upstream context as necessary, achieving nesting.
*/
followTimeContext() {
this.stopFollowingTimeContext();
if (this.upstreamTimeContext) {
TIME_CONTEXT_EVENTS.forEach((eventName) => {
const thisTimeContext = this;
this.upstreamTimeContext.on(eventName, passthrough);
this.unlisteners.push(() => {
thisTimeContext.upstreamTimeContext.off(eventName, passthrough);
});
return timeContext;
}
/**
* Set the time context of a view to follow any upstream time contexts as necessary (defaulting to the global context)
* This needs to be separate from refreshContext
*/
removeIndependentContext(viewKey) {
const key = this.openmct.objects.makeKeyString(this.objectPath[0].identifier);
if (viewKey && key === viewKey) {
//this is necessary as the upstream context gets reassigned after this
this.stopFollowingTimeContext();
let timeContext = this.globalTimeContext;
this.objectPath.some((item, index) => {
const objectKey = this.openmct.objects.makeKeyString(item.identifier);
// we're only interested in any parents, not self, so index > 0
const itemContext = this.globalTimeContext.independentContexts.get(objectKey);
if (index > 0 && itemContext && itemContext.hasOwnContext()) {
//upstream time context
timeContext = itemContext;
return true;
}
return false;
});
this.upstreamTimeContext = timeContext;
this.followTimeContext();
// Emit bounds so that views that are changing context get the upstream bounds
this.emit('bounds', this.bounds());
// now that the view's context is set, tell others to check theirs in case they were following this view's context.
this.globalTimeContext.emit('refreshContext', viewKey);
function passthrough() {
thisTimeContext.emit(eventName, ...arguments);
}
});
}
}
/**
* Stops following any upstream time context
*/
stopFollowingTimeContext() {
this.unlisteners.forEach((unlisten) => unlisten());
this.unlisteners = [];
}
resetContext() {
if (this.upstreamTimeContext) {
this.stopFollowingTimeContext();
this.upstreamTimeContext = undefined;
}
}
/**
* Refresh the time context, following any upstream time contexts as necessary
*/
refreshContext(viewKey) {
const key = this.openmct.objects.makeKeyString(this.objectPath[0].identifier);
if (viewKey && key === viewKey) {
return;
}
//this is necessary as the upstream context gets reassigned after this
this.stopFollowingTimeContext();
this.upstreamTimeContext = this.getUpstreamContext();
this.followTimeContext();
// Emit bounds so that views that are changing context get the upstream bounds
this.emit('bounds', this.bounds());
}
hasOwnContext() {
return this.upstreamTimeContext === undefined;
}
getUpstreamContext() {
// If a view has an independent context, don't return an upstream context
// Be aware that when a new independent time context is created, we assign the global context as default
if (this.hasOwnContext()) {
return undefined;
}
let timeContext = this.globalTimeContext;
this.objectPath.some((item, index) => {
const key = this.openmct.objects.makeKeyString(item.identifier);
// we're only interested in parents, not self, so index > 0
const itemContext = this.globalTimeContext.independentContexts.get(key);
if (index > 0 && itemContext && itemContext.hasOwnContext()) {
//upstream time context
timeContext = itemContext;
return true;
}
return false;
});
return timeContext;
}
/**
* Set the time context of a view to follow any upstream time contexts as necessary (defaulting to the global context)
* This needs to be separate from refreshContext
*/
removeIndependentContext(viewKey) {
const key = this.openmct.objects.makeKeyString(this.objectPath[0].identifier);
if (viewKey && key === viewKey) {
//this is necessary as the upstream context gets reassigned after this
this.stopFollowingTimeContext();
let timeContext = this.globalTimeContext;
this.objectPath.some((item, index) => {
const objectKey = this.openmct.objects.makeKeyString(item.identifier);
// we're only interested in any parents, not self, so index > 0
const itemContext = this.globalTimeContext.independentContexts.get(objectKey);
if (index > 0 && itemContext && itemContext.hasOwnContext()) {
//upstream time context
timeContext = itemContext;
return true;
}
return false;
});
this.upstreamTimeContext = timeContext;
this.followTimeContext();
// Emit bounds so that views that are changing context get the upstream bounds
this.emit('bounds', this.bounds());
// now that the view's context is set, tell others to check theirs in case they were following this view's context.
this.globalTimeContext.emit('refreshContext', viewKey);
}
}
}
export default IndependentTimeContext;

View File

@@ -20,189 +20,190 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import GlobalTimeContext from "./GlobalTimeContext";
import IndependentTimeContext from "@/api/time/IndependentTimeContext";
import GlobalTimeContext from './GlobalTimeContext';
import IndependentTimeContext from '@/api/time/IndependentTimeContext';
/**
* The public API for setting and querying the temporal state of the
* application. The concept of time is integral to Open MCT, and at least
* one {@link TimeSystem}, as well as some default time bounds must be
* registered and enabled via {@link TimeAPI.addTimeSystem} and
* {@link TimeAPI.timeSystem} respectively for Open MCT to work.
*
* Time-sensitive views will typically respond to changes to bounds or other
* properties of the time conductor and update the data displayed based on
* the temporal state of the application. The current time bounds are also
* used in queries for historical data.
*
* The TimeAPI extends the GlobalTimeContext which in turn extends the TimeContext/EventEmitter class. A number of events are
* fired when properties of the time conductor change, which are documented
* below.
*
* @interface
* @memberof module:openmct
*/
* The public API for setting and querying the temporal state of the
* application. The concept of time is integral to Open MCT, and at least
* one {@link TimeSystem}, as well as some default time bounds must be
* registered and enabled via {@link TimeAPI.addTimeSystem} and
* {@link TimeAPI.timeSystem} respectively for Open MCT to work.
*
* Time-sensitive views will typically respond to changes to bounds or other
* properties of the time conductor and update the data displayed based on
* the temporal state of the application. The current time bounds are also
* used in queries for historical data.
*
* The TimeAPI extends the GlobalTimeContext which in turn extends the TimeContext/EventEmitter class. A number of events are
* fired when properties of the time conductor change, which are documented
* below.
*
* @interface
* @memberof module:openmct
*/
class TimeAPI extends GlobalTimeContext {
constructor(openmct) {
super();
this.openmct = openmct;
this.independentContexts = new Map();
constructor(openmct) {
super();
this.openmct = openmct;
this.independentContexts = new Map();
}
/**
* A TimeSystem provides meaning to the values returned by the TimeAPI. Open
* MCT supports multiple different types of time values, although all are
* intrinsically represented by numbers, the meaning of those numbers can
* differ depending on context.
*
* A default time system is provided by Open MCT in the form of the {@link UTCTimeSystem},
* which represents integer values as ms in the Unix epoch. An example of
* another time system might be "sols" for a Martian mission. TimeSystems do
* not address the issue of converting between time systems.
*
* @typedef {object} TimeSystem
* @property {string} key A unique identifier
* @property {string} name A human-readable descriptor
* @property {string} [cssClass] Specify a css class defining an icon for
* this time system. This will be visible next to the time system in the
* menu in the Time Conductor
* @property {string} timeFormat The key of a format to use when displaying
* discrete timestamps from this time system
* @property {string} [durationFormat] The key of a format to use when
* displaying a duration or relative span of time in this time system.
*/
/**
* Register a new time system. Once registered it can activated using
* {@link TimeAPI.timeSystem}, and can be referenced via its key in [Time Conductor configuration](@link https://github.com/nasa/openmct/blob/master/API.md#time-conductor).
* @memberof module:openmct.TimeAPI#
* @param {TimeSystem} timeSystem A time system object.
*/
addTimeSystem(timeSystem) {
this.timeSystems.set(timeSystem.key, timeSystem);
}
/**
* @returns {TimeSystem[]}
*/
getAllTimeSystems() {
return Array.from(this.timeSystems.values());
}
/**
* Clocks provide a timing source that is used to
* automatically update the time bounds of the data displayed in Open MCT.
*
* @typedef {object} Clock
* @memberof openmct.timeAPI
* @property {string} key A unique identifier
* @property {string} name A human-readable name. The name will be used to
* represent this clock in the Time Conductor UI
* @property {string} description A longer description, ideally identifying
* what the clock ticks on.
* @property {function} currentValue Returns the last value generated by a tick, or a default value
* if no ticking has yet occurred
* @see {LocalClock}
*/
/**
* Register a new Clock.
* @memberof module:openmct.TimeAPI#
* @param {Clock} clock
*/
addClock(clock) {
this.clocks.set(clock.key, clock);
}
/**
* @memberof module:openmct.TimeAPI#
* @returns {Clock[]}
* @memberof module:openmct.TimeAPI#
*/
getAllClocks() {
return Array.from(this.clocks.values());
}
/**
* Get or set an independent time context which follows the TimeAPI timeSystem,
* but with different offsets for a given domain object
* @param {key | string} key The identifier key of the domain object these offsets are set for
* @param {ClockOffsets | TimeBounds} value This maintains a sliding time window of a fixed width that automatically updates
* @param {key | string} clockKey the real time clock key currently in use
* @memberof module:openmct.TimeAPI#
* @method addIndependentTimeContext
*/
addIndependentContext(key, value, clockKey) {
let timeContext = this.getIndependentContext(key);
//stop following upstream time context since the view has it's own
timeContext.resetContext();
if (clockKey) {
timeContext.clock(clockKey, value);
} else {
timeContext.stopClock();
timeContext.bounds(value);
}
/**
* A TimeSystem provides meaning to the values returned by the TimeAPI. Open
* MCT supports multiple different types of time values, although all are
* intrinsically represented by numbers, the meaning of those numbers can
* differ depending on context.
*
* A default time system is provided by Open MCT in the form of the {@link UTCTimeSystem},
* which represents integer values as ms in the Unix epoch. An example of
* another time system might be "sols" for a Martian mission. TimeSystems do
* not address the issue of converting between time systems.
*
* @typedef {object} TimeSystem
* @property {string} key A unique identifier
* @property {string} name A human-readable descriptor
* @property {string} [cssClass] Specify a css class defining an icon for
* this time system. This will be visible next to the time system in the
* menu in the Time Conductor
* @property {string} timeFormat The key of a format to use when displaying
* discrete timestamps from this time system
* @property {string} [durationFormat] The key of a format to use when
* displaying a duration or relative span of time in this time system.
*/
// Notify any nested views to update, pass in the viewKey so that particular view can skip getting an upstream context
this.emit('refreshContext', key);
/**
* Register a new time system. Once registered it can activated using
* {@link TimeAPI.timeSystem}, and can be referenced via its key in [Time Conductor configuration](@link https://github.com/nasa/openmct/blob/master/API.md#time-conductor).
* @memberof module:openmct.TimeAPI#
* @param {TimeSystem} timeSystem A time system object.
*/
addTimeSystem(timeSystem) {
this.timeSystems.set(timeSystem.key, timeSystem);
return () => {
//follow any upstream time context
this.emit('removeOwnContext', key);
};
}
/**
* Get the independent time context which follows the TimeAPI timeSystem,
* but with different offsets.
* @param {key | string} key The identifier key of the domain object these offsets
* @memberof module:openmct.TimeAPI#
* @method getIndependentTimeContext
*/
getIndependentContext(key) {
return this.independentContexts.get(key);
}
/**
* Get the a timeContext for a view based on it's objectPath. If there is any object in the objectPath with an independent time context, it will be returned.
* Otherwise, the global time context will be returned.
* @param { Array } objectPath The view's objectPath
* @memberof module:openmct.TimeAPI#
* @method getContextForView
*/
getContextForView(objectPath) {
if (!objectPath || !Array.isArray(objectPath)) {
throw new Error('No objectPath provided');
}
/**
* @returns {TimeSystem[]}
*/
getAllTimeSystems() {
return Array.from(this.timeSystems.values());
const viewKey =
objectPath.length && this.openmct.objects.makeKeyString(objectPath[0].identifier);
if (!viewKey) {
// Return the global time context
return this;
}
/**
* Clocks provide a timing source that is used to
* automatically update the time bounds of the data displayed in Open MCT.
*
* @typedef {object} Clock
* @memberof openmct.timeAPI
* @property {string} key A unique identifier
* @property {string} name A human-readable name. The name will be used to
* represent this clock in the Time Conductor UI
* @property {string} description A longer description, ideally identifying
* what the clock ticks on.
* @property {function} currentValue Returns the last value generated by a tick, or a default value
* if no ticking has yet occurred
* @see {LocalClock}
*/
let viewTimeContext = this.getIndependentContext(viewKey);
if (!viewTimeContext) {
// If the context doesn't exist yet, create it.
viewTimeContext = new IndependentTimeContext(this.openmct, this, objectPath);
this.independentContexts.set(viewKey, viewTimeContext);
} else {
// If it already exists, compare the objectPath to see if it needs to be updated.
const currentPath = this.openmct.objects.getRelativePath(viewTimeContext.objectPath);
const newPath = this.openmct.objects.getRelativePath(objectPath);
/**
* Register a new Clock.
* @memberof module:openmct.TimeAPI#
* @param {Clock} clock
*/
addClock(clock) {
this.clocks.set(clock.key, clock);
if (currentPath !== newPath) {
// If the path has changed, update the context.
this.independentContexts.delete(viewKey);
viewTimeContext = new IndependentTimeContext(this.openmct, this, objectPath);
this.independentContexts.set(viewKey, viewTimeContext);
}
}
/**
* @memberof module:openmct.TimeAPI#
* @returns {Clock[]}
* @memberof module:openmct.TimeAPI#
*/
getAllClocks() {
return Array.from(this.clocks.values());
}
/**
* Get or set an independent time context which follows the TimeAPI timeSystem,
* but with different offsets for a given domain object
* @param {key | string} key The identifier key of the domain object these offsets are set for
* @param {ClockOffsets | TimeBounds} value This maintains a sliding time window of a fixed width that automatically updates
* @param {key | string} clockKey the real time clock key currently in use
* @memberof module:openmct.TimeAPI#
* @method addIndependentTimeContext
*/
addIndependentContext(key, value, clockKey) {
let timeContext = this.getIndependentContext(key);
//stop following upstream time context since the view has it's own
timeContext.resetContext();
if (clockKey) {
timeContext.clock(clockKey, value);
} else {
timeContext.stopClock();
timeContext.bounds(value);
}
// Notify any nested views to update, pass in the viewKey so that particular view can skip getting an upstream context
this.emit('refreshContext', key);
return () => {
//follow any upstream time context
this.emit('removeOwnContext', key);
};
}
/**
* Get the independent time context which follows the TimeAPI timeSystem,
* but with different offsets.
* @param {key | string} key The identifier key of the domain object these offsets
* @memberof module:openmct.TimeAPI#
* @method getIndependentTimeContext
*/
getIndependentContext(key) {
return this.independentContexts.get(key);
}
/**
* Get the a timeContext for a view based on it's objectPath. If there is any object in the objectPath with an independent time context, it will be returned.
* Otherwise, the global time context will be returned.
* @param { Array } objectPath The view's objectPath
* @memberof module:openmct.TimeAPI#
* @method getContextForView
*/
getContextForView(objectPath) {
if (!objectPath || !Array.isArray(objectPath)) {
throw new Error('No objectPath provided');
}
const viewKey = objectPath.length && this.openmct.objects.makeKeyString(objectPath[0].identifier);
if (!viewKey) {
// Return the global time context
return this;
}
let viewTimeContext = this.getIndependentContext(viewKey);
if (!viewTimeContext) {
// If the context doesn't exist yet, create it.
viewTimeContext = new IndependentTimeContext(this.openmct, this, objectPath);
this.independentContexts.set(viewKey, viewTimeContext);
} else {
// If it already exists, compare the objectPath to see if it needs to be updated.
const currentPath = this.openmct.objects.getRelativePath(viewTimeContext.objectPath);
const newPath = this.openmct.objects.getRelativePath(objectPath);
if (currentPath !== newPath) {
// If the path has changed, update the context.
this.independentContexts.delete(viewKey);
viewTimeContext = new IndependentTimeContext(this.openmct, this, objectPath);
this.independentContexts.set(viewKey, viewTimeContext);
}
}
return viewTimeContext;
}
return viewTimeContext;
}
}
export default TimeAPI;

View File

@@ -19,262 +19,241 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import TimeAPI from "./TimeAPI";
import {createOpenMct} from "utils/testing";
import TimeAPI from './TimeAPI';
import { createOpenMct } from 'utils/testing';
describe("The Time API", function () {
let api;
let timeSystemKey;
let timeSystem;
let clockKey;
let clock;
let bounds;
let eventListener;
let toi;
let openmct;
describe('The Time API', function () {
let api;
let timeSystemKey;
let timeSystem;
let clockKey;
let clock;
let bounds;
let eventListener;
let toi;
let openmct;
beforeEach(function () {
openmct = createOpenMct();
api = new TimeAPI(openmct);
timeSystemKey = 'timeSystemKey';
timeSystem = { key: timeSystemKey };
clockKey = 'someClockKey';
clock = jasmine.createSpyObj('clock', ['on', 'off', 'currentValue']);
clock.currentValue.and.returnValue(100);
clock.key = clockKey;
bounds = {
start: 0,
end: 1
};
eventListener = jasmine.createSpy('eventListener');
toi = 111;
});
it('Supports setting and querying of time of interest', function () {
expect(api.timeOfInterest()).not.toBe(toi);
api.timeOfInterest(toi);
expect(api.timeOfInterest()).toBe(toi);
});
it('Allows setting of valid bounds', function () {
bounds = {
start: 0,
end: 1
};
expect(api.bounds()).not.toBe(bounds);
expect(api.bounds.bind(api, bounds)).not.toThrow();
expect(api.bounds()).toEqual(bounds);
});
it('Disallows setting of invalid bounds', function () {
bounds = {
start: 1,
end: 0
};
expect(api.bounds()).not.toEqual(bounds);
expect(api.bounds.bind(api, bounds)).toThrow();
expect(api.bounds()).not.toEqual(bounds);
bounds = { start: 1 };
expect(api.bounds()).not.toEqual(bounds);
expect(api.bounds.bind(api, bounds)).toThrow();
expect(api.bounds()).not.toEqual(bounds);
});
it('Allows setting of previously registered time system with bounds', function () {
api.addTimeSystem(timeSystem);
expect(api.timeSystem()).not.toBe(timeSystem);
expect(function () {
api.timeSystem(timeSystem, bounds);
}).not.toThrow();
expect(api.timeSystem()).toBe(timeSystem);
});
it('Disallows setting of time system without bounds', function () {
api.addTimeSystem(timeSystem);
expect(api.timeSystem()).not.toBe(timeSystem);
expect(function () {
api.timeSystem(timeSystemKey);
}).toThrow();
expect(api.timeSystem()).not.toBe(timeSystem);
});
it('allows setting of timesystem without bounds with clock', function () {
api.addTimeSystem(timeSystem);
api.addClock(clock);
api.clock(clockKey, {
start: 0,
end: 1
});
expect(api.timeSystem()).not.toBe(timeSystem);
expect(function () {
api.timeSystem(timeSystemKey);
}).not.toThrow();
expect(api.timeSystem()).toBe(timeSystem);
});
it('Emits an event when time system changes', function () {
api.addTimeSystem(timeSystem);
expect(eventListener).not.toHaveBeenCalled();
api.on('timeSystem', eventListener);
api.timeSystem(timeSystemKey, bounds);
expect(eventListener).toHaveBeenCalledWith(timeSystem);
});
it('Emits an event when time of interest changes', function () {
expect(eventListener).not.toHaveBeenCalled();
api.on('timeOfInterest', eventListener);
api.timeOfInterest(toi);
expect(eventListener).toHaveBeenCalledWith(toi);
});
it('Emits an event when bounds change', function () {
expect(eventListener).not.toHaveBeenCalled();
api.on('bounds', eventListener);
api.bounds(bounds);
expect(eventListener).toHaveBeenCalledWith(bounds, false);
});
it('If bounds are set and TOI lies inside them, do not change TOI', function () {
api.timeOfInterest(6);
api.bounds({
start: 1,
end: 10
});
expect(api.timeOfInterest()).toEqual(6);
});
it('If bounds are set and TOI lies outside them, reset TOI', function () {
api.timeOfInterest(11);
api.bounds({
start: 1,
end: 10
});
expect(api.timeOfInterest()).toBeUndefined();
});
it('Maintains delta during tick', function () {});
it('Allows registered time system to be activated', function () {});
it('Allows a registered tick source to be activated', function () {
const mockTickSource = jasmine.createSpyObj('mockTickSource', ['on', 'off', 'currentValue']);
mockTickSource.key = 'mockTickSource';
});
describe(' when enabling a tick source', function () {
let mockTickSource;
let anotherMockTickSource;
const mockOffsets = {
start: 0,
end: 1
};
beforeEach(function () {
openmct = createOpenMct();
api = new TimeAPI(openmct);
timeSystemKey = "timeSystemKey";
timeSystem = {key: timeSystemKey};
clockKey = "someClockKey";
clock = jasmine.createSpyObj("clock", [
"on",
"off",
"currentValue"
]);
clock.currentValue.and.returnValue(100);
clock.key = clockKey;
bounds = {
start: 0,
end: 1
};
eventListener = jasmine.createSpy("eventListener");
toi = 111;
mockTickSource = jasmine.createSpyObj('clock', ['on', 'off', 'currentValue']);
mockTickSource.currentValue.and.returnValue(10);
mockTickSource.key = 'mts';
anotherMockTickSource = jasmine.createSpyObj('clock', ['on', 'off', 'currentValue']);
anotherMockTickSource.key = 'amts';
anotherMockTickSource.currentValue.and.returnValue(10);
api.addClock(mockTickSource);
api.addClock(anotherMockTickSource);
});
it("Supports setting and querying of time of interest", function () {
expect(api.timeOfInterest()).not.toBe(toi);
api.timeOfInterest(toi);
expect(api.timeOfInterest()).toBe(toi);
it('sets bounds based on current value', function () {
api.clock('mts', mockOffsets);
expect(api.bounds()).toEqual({
start: 10,
end: 11
});
});
it("Allows setting of valid bounds", function () {
bounds = {
start: 0,
end: 1
};
expect(api.bounds()).not.toBe(bounds);
expect(api.bounds.bind(api, bounds)).not.toThrow();
expect(api.bounds()).toEqual(bounds);
it('a new tick listener is registered', function () {
api.clock('mts', mockOffsets);
expect(mockTickSource.on).toHaveBeenCalledWith('tick', jasmine.any(Function));
});
it("Disallows setting of invalid bounds", function () {
bounds = {
start: 1,
end: 0
};
expect(api.bounds()).not.toEqual(bounds);
expect(api.bounds.bind(api, bounds)).toThrow();
expect(api.bounds()).not.toEqual(bounds);
bounds = {start: 1};
expect(api.bounds()).not.toEqual(bounds);
expect(api.bounds.bind(api, bounds)).toThrow();
expect(api.bounds()).not.toEqual(bounds);
it('listener of existing tick source is reregistered', function () {
api.clock('mts', mockOffsets);
api.clock('amts', mockOffsets);
expect(mockTickSource.off).toHaveBeenCalledWith('tick', jasmine.any(Function));
});
it("Allows setting of previously registered time system with bounds", function () {
api.addTimeSystem(timeSystem);
expect(api.timeSystem()).not.toBe(timeSystem);
expect(function () {
api.timeSystem(timeSystem, bounds);
}).not.toThrow();
expect(api.timeSystem()).toBe(timeSystem);
it('Allows the active clock to be set and unset', function () {
expect(api.clock()).toBeUndefined();
api.clock('mts', mockOffsets);
expect(api.clock()).toBeDefined();
api.stopClock();
expect(api.clock()).toBeUndefined();
});
it("Disallows setting of time system without bounds", function () {
api.addTimeSystem(timeSystem);
expect(api.timeSystem()).not.toBe(timeSystem);
expect(function () {
api.timeSystem(timeSystemKey);
}).toThrow();
expect(api.timeSystem()).not.toBe(timeSystem);
it('Provides a default time context', () => {
const timeContext = api.getContextForView([]);
expect(timeContext).not.toBe(null);
});
it("allows setting of timesystem without bounds with clock", function () {
api.addTimeSystem(timeSystem);
api.addClock(clock);
api.clock(clockKey, {
start: 0,
end: 1
});
expect(api.timeSystem()).not.toBe(timeSystem);
expect(function () {
api.timeSystem(timeSystemKey);
}).not.toThrow();
expect(api.timeSystem()).toBe(timeSystem);
it('Without a clock, is in fixed time mode', () => {
const timeContext = api.getContextForView([]);
expect(timeContext.isRealTime()).toBe(false);
});
it("Emits an event when time system changes", function () {
api.addTimeSystem(timeSystem);
expect(eventListener).not.toHaveBeenCalled();
api.on("timeSystem", eventListener);
api.timeSystem(timeSystemKey, bounds);
expect(eventListener).toHaveBeenCalledWith(timeSystem);
it('Provided a clock, is in real-time mode', () => {
const timeContext = api.getContextForView([]);
timeContext.clock('mts', {
start: 0,
end: 1
});
expect(timeContext.isRealTime()).toBe(true);
});
});
it("Emits an event when time of interest changes", function () {
expect(eventListener).not.toHaveBeenCalled();
api.on("timeOfInterest", eventListener);
api.timeOfInterest(toi);
expect(eventListener).toHaveBeenCalledWith(toi);
});
it('on tick, observes offsets, and indicates tick in bounds callback', function () {
const mockTickSource = jasmine.createSpyObj('clock', ['on', 'off', 'currentValue']);
mockTickSource.currentValue.and.returnValue(100);
let tickCallback;
const boundsCallback = jasmine.createSpy('boundsCallback');
const clockOffsets = {
start: -100,
end: 100
};
mockTickSource.key = 'mts';
it("Emits an event when bounds change", function () {
expect(eventListener).not.toHaveBeenCalled();
api.on("bounds", eventListener);
api.bounds(bounds);
expect(eventListener).toHaveBeenCalledWith(bounds, false);
});
api.addClock(mockTickSource);
api.clock('mts', clockOffsets);
it("If bounds are set and TOI lies inside them, do not change TOI", function () {
api.timeOfInterest(6);
api.bounds({
start: 1,
end: 10
});
expect(api.timeOfInterest()).toEqual(6);
});
api.on('bounds', boundsCallback);
it("If bounds are set and TOI lies outside them, reset TOI", function () {
api.timeOfInterest(11);
api.bounds({
start: 1,
end: 10
});
expect(api.timeOfInterest()).toBeUndefined();
});
it("Maintains delta during tick", function () {
});
it("Allows registered time system to be activated", function () {
});
it("Allows a registered tick source to be activated", function () {
const mockTickSource = jasmine.createSpyObj("mockTickSource", [
"on",
"off",
"currentValue"
]);
mockTickSource.key = 'mockTickSource';
});
describe(" when enabling a tick source", function () {
let mockTickSource;
let anotherMockTickSource;
const mockOffsets = {
start: 0,
end: 1
};
beforeEach(function () {
mockTickSource = jasmine.createSpyObj("clock", [
"on",
"off",
"currentValue"
]);
mockTickSource.currentValue.and.returnValue(10);
mockTickSource.key = "mts";
anotherMockTickSource = jasmine.createSpyObj("clock", [
"on",
"off",
"currentValue"
]);
anotherMockTickSource.key = "amts";
anotherMockTickSource.currentValue.and.returnValue(10);
api.addClock(mockTickSource);
api.addClock(anotherMockTickSource);
});
it("sets bounds based on current value", function () {
api.clock("mts", mockOffsets);
expect(api.bounds()).toEqual({
start: 10,
end: 11
});
});
it("a new tick listener is registered", function () {
api.clock("mts", mockOffsets);
expect(mockTickSource.on).toHaveBeenCalledWith("tick", jasmine.any(Function));
});
it("listener of existing tick source is reregistered", function () {
api.clock("mts", mockOffsets);
api.clock("amts", mockOffsets);
expect(mockTickSource.off).toHaveBeenCalledWith("tick", jasmine.any(Function));
});
it("Allows the active clock to be set and unset", function () {
expect(api.clock()).toBeUndefined();
api.clock("mts", mockOffsets);
expect(api.clock()).toBeDefined();
api.stopClock();
expect(api.clock()).toBeUndefined();
});
it('Provides a default time context', () => {
const timeContext = api.getContextForView([]);
expect(timeContext).not.toBe(null);
});
it("Without a clock, is in fixed time mode", () => {
const timeContext = api.getContextForView([]);
expect(timeContext.isRealTime()).toBe(false);
});
it("Provided a clock, is in real-time mode", () => {
const timeContext = api.getContextForView([]);
timeContext.clock('mts', {
start: 0,
end: 1
});
expect(timeContext.isRealTime()).toBe(true);
});
});
it("on tick, observes offsets, and indicates tick in bounds callback", function () {
const mockTickSource = jasmine.createSpyObj("clock", [
"on",
"off",
"currentValue"
]);
mockTickSource.currentValue.and.returnValue(100);
let tickCallback;
const boundsCallback = jasmine.createSpy("boundsCallback");
const clockOffsets = {
start: -100,
end: 100
};
mockTickSource.key = "mts";
api.addClock(mockTickSource);
api.clock("mts", clockOffsets);
api.on("bounds", boundsCallback);
tickCallback = mockTickSource.on.calls.mostRecent().args[1];
tickCallback(1000);
expect(boundsCallback).toHaveBeenCalledWith({
start: 900,
end: 1100
}, true);
});
tickCallback = mockTickSource.on.calls.mostRecent().args[1];
tickCallback(1000);
expect(boundsCallback).toHaveBeenCalledWith(
{
start: 900,
end: 1100
},
true
);
});
});

View File

@@ -22,357 +22,357 @@
import EventEmitter from 'EventEmitter';
export const TIME_CONTEXT_EVENTS = [
'bounds',
'clock',
'timeSystem',
'clockOffsets'
];
export const TIME_CONTEXT_EVENTS = ['bounds', 'clock', 'timeSystem', 'clockOffsets'];
class TimeContext extends EventEmitter {
constructor() {
super();
constructor() {
super();
//The Time System
this.timeSystems = new Map();
//The Time System
this.timeSystems = new Map();
this.system = undefined;
this.system = undefined;
this.clocks = new Map();
this.clocks = new Map();
this.boundsVal = {
start: undefined,
end: undefined
};
this.boundsVal = {
start: undefined,
end: undefined
};
this.activeClock = undefined;
this.offsets = undefined;
this.activeClock = undefined;
this.offsets = undefined;
this.tick = this.tick.bind(this);
}
this.tick = this.tick.bind(this);
}
/**
* Get or set the time system of the TimeAPI.
* @param {TimeSystem | string} timeSystemOrKey
* @param {module:openmct.TimeAPI~TimeConductorBounds} bounds
* @fires module:openmct.TimeAPI~timeSystem
* @returns {TimeSystem} The currently applied time system
* @memberof module:openmct.TimeAPI#
* @method timeSystem
*/
timeSystem(timeSystemOrKey, bounds) {
if (arguments.length >= 1) {
if (arguments.length === 1 && !this.activeClock) {
throw new Error(
"Must specify bounds when changing time system without an active clock."
);
}
/**
* Get or set the time system of the TimeAPI.
* @param {TimeSystem | string} timeSystemOrKey
* @param {module:openmct.TimeAPI~TimeConductorBounds} bounds
* @fires module:openmct.TimeAPI~timeSystem
* @returns {TimeSystem} The currently applied time system
* @memberof module:openmct.TimeAPI#
* @method timeSystem
*/
timeSystem(timeSystemOrKey, bounds) {
if (arguments.length >= 1) {
if (arguments.length === 1 && !this.activeClock) {
throw new Error('Must specify bounds when changing time system without an active clock.');
}
let timeSystem;
let timeSystem;
if (timeSystemOrKey === undefined) {
throw "Please provide a time system";
}
if (timeSystemOrKey === undefined) {
throw 'Please provide a time system';
}
if (typeof timeSystemOrKey === 'string') {
timeSystem = this.timeSystems.get(timeSystemOrKey);
if (timeSystem === undefined) {
throw "Unknown time system " + timeSystemOrKey + ". Has it been registered with 'addTimeSystem'?";
}
} else if (typeof timeSystemOrKey === 'object') {
timeSystem = timeSystemOrKey;
if (!this.timeSystems.has(timeSystem.key)) {
throw "Unknown time system " + timeSystem.key + ". Has it been registered with 'addTimeSystem'?";
}
} else {
throw "Attempt to set invalid time system in Time API. Please provide a previously registered time system object or key";
}
this.system = timeSystem;
/**
* The time system used by the time
* conductor has changed. A change in Time System will always be
* followed by a bounds event specifying new query bounds.
*
* @event module:openmct.TimeAPI~timeSystem
* @property {TimeSystem} The value of the currently applied
* Time System
* */
this.emit('timeSystem', this.system);
if (bounds) {
this.bounds(bounds);
}
if (typeof timeSystemOrKey === 'string') {
timeSystem = this.timeSystems.get(timeSystemOrKey);
if (timeSystem === undefined) {
throw (
'Unknown time system ' +
timeSystemOrKey +
". Has it been registered with 'addTimeSystem'?"
);
}
} else if (typeof timeSystemOrKey === 'object') {
timeSystem = timeSystemOrKey;
return this.system;
}
/**
* Clock offsets are used to calculate temporal bounds when the system is
* ticking on a clock source.
*
* @typedef {object} ValidationResult
* @property {boolean} valid Result of the validation - true or false.
* @property {string} message An error message if valid is false.
*/
/**
* Validate the given bounds. This can be used for pre-validation of bounds,
* for example by views validating user inputs.
* @param {TimeBounds} bounds The start and end time of the conductor.
* @returns {ValidationResult} A validation error, or true if valid
* @memberof module:openmct.TimeAPI#
* @method validateBounds
*/
validateBounds(bounds) {
if ((bounds.start === undefined)
|| (bounds.end === undefined)
|| isNaN(bounds.start)
|| isNaN(bounds.end)
) {
return {
valid: false,
message: "Start and end must be specified as integer values"
};
} else if (bounds.start > bounds.end) {
return {
valid: false,
message: "Specified start date exceeds end bound"
};
if (!this.timeSystems.has(timeSystem.key)) {
throw (
'Unknown time system ' +
timeSystem.key +
". Has it been registered with 'addTimeSystem'?"
);
}
} else {
throw 'Attempt to set invalid time system in Time API. Please provide a previously registered time system object or key';
}
return {
valid: true,
message: ''
};
this.system = timeSystem;
/**
* The time system used by the time
* conductor has changed. A change in Time System will always be
* followed by a bounds event specifying new query bounds.
*
* @event module:openmct.TimeAPI~timeSystem
* @property {TimeSystem} The value of the currently applied
* Time System
* */
this.emit('timeSystem', this.system);
if (bounds) {
this.bounds(bounds);
}
}
/**
* Get or set the start and end time of the time conductor. Basic validation
* of bounds is performed.
*
* @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds
* @throws {Error} Validation error
* @fires module:openmct.TimeAPI~bounds
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
* @memberof module:openmct.TimeAPI#
* @method bounds
*/
bounds(newBounds) {
if (arguments.length > 0) {
const validationResult = this.validateBounds(newBounds);
if (validationResult.valid !== true) {
throw new Error(validationResult.message);
}
return this.system;
}
//Create a copy to avoid direct mutation of conductor bounds
this.boundsVal = JSON.parse(JSON.stringify(newBounds));
/**
* The start time, end time, or both have been updated.
* @event bounds
* @memberof module:openmct.TimeAPI~
* @property {TimeConductorBounds} bounds The newly updated bounds
* @property {boolean} [tick] `true` if the bounds update was due to
* a "tick" event (ie. was an automatic update), false otherwise.
*/
this.emit('bounds', this.boundsVal, false);
/**
* Clock offsets are used to calculate temporal bounds when the system is
* ticking on a clock source.
*
* @typedef {object} ValidationResult
* @property {boolean} valid Result of the validation - true or false.
* @property {string} message An error message if valid is false.
*/
/**
* Validate the given bounds. This can be used for pre-validation of bounds,
* for example by views validating user inputs.
* @param {TimeBounds} bounds The start and end time of the conductor.
* @returns {ValidationResult} A validation error, or true if valid
* @memberof module:openmct.TimeAPI#
* @method validateBounds
*/
validateBounds(bounds) {
if (
bounds.start === undefined ||
bounds.end === undefined ||
isNaN(bounds.start) ||
isNaN(bounds.end)
) {
return {
valid: false,
message: 'Start and end must be specified as integer values'
};
} else if (bounds.start > bounds.end) {
return {
valid: false,
message: 'Specified start date exceeds end bound'
};
}
return {
valid: true,
message: ''
};
}
/**
* Get or set the start and end time of the time conductor. Basic validation
* of bounds is performed.
*
* @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds
* @throws {Error} Validation error
* @fires module:openmct.TimeAPI~bounds
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
* @memberof module:openmct.TimeAPI#
* @method bounds
*/
bounds(newBounds) {
if (arguments.length > 0) {
const validationResult = this.validateBounds(newBounds);
if (validationResult.valid !== true) {
throw new Error(validationResult.message);
}
//Create a copy to avoid direct mutation of conductor bounds
this.boundsVal = JSON.parse(JSON.stringify(newBounds));
/**
* The start time, end time, or both have been updated.
* @event bounds
* @memberof module:openmct.TimeAPI~
* @property {TimeConductorBounds} bounds The newly updated bounds
* @property {boolean} [tick] `true` if the bounds update was due to
* a "tick" event (ie. was an automatic update), false otherwise.
*/
this.emit('bounds', this.boundsVal, false);
}
//Return a copy to prevent direct mutation of time conductor bounds.
return JSON.parse(JSON.stringify(this.boundsVal));
}
/**
* Validate the given offsets. This can be used for pre-validation of
* offsets, for example by views validating user inputs.
* @param {ClockOffsets} offsets The start and end offsets from a 'now' value.
* @returns { ValidationResult } A validation error, and true/false if valid or not
* @memberof module:openmct.TimeAPI#
* @method validateOffsets
*/
validateOffsets(offsets) {
if (
offsets.start === undefined ||
offsets.end === undefined ||
isNaN(offsets.start) ||
isNaN(offsets.end)
) {
return {
valid: false,
message: 'Start and end offsets must be specified as integer values'
};
} else if (offsets.start >= offsets.end) {
return {
valid: false,
message: 'Specified start offset must be < end offset'
};
}
return {
valid: true,
message: ''
};
}
/**
* @typedef {Object} TimeBounds
* @property {number} start The start time displayed by the time conductor
* in ms since epoch. Epoch determined by currently active time system
* @property {number} end The end time displayed by the time conductor in ms
* since epoch.
* @memberof module:openmct.TimeAPI~
*/
/**
* Clock offsets are used to calculate temporal bounds when the system is
* ticking on a clock source.
*
* @typedef {object} ClockOffsets
* @property {number} start A time span relative to the current value of the
* ticking clock, from which start bounds will be calculated. This value must
* be < 0. When a clock is active, bounds will be calculated automatically
* based on the value provided by the clock, and the defined clock offsets.
* @property {number} end A time span relative to the current value of the
* ticking clock, from which end bounds will be calculated. This value must
* be >= 0.
*/
/**
* Get or set the currently applied clock offsets. If no parameter is provided,
* the current value will be returned. If provided, the new value will be
* used as the new clock offsets.
* @param {ClockOffsets} offsets
* @returns {ClockOffsets}
*/
clockOffsets(offsets) {
if (arguments.length > 0) {
const validationResult = this.validateOffsets(offsets);
if (validationResult.valid !== true) {
throw new Error(validationResult.message);
}
this.offsets = offsets;
const currentValue = this.activeClock.currentValue();
const newBounds = {
start: currentValue + offsets.start,
end: currentValue + offsets.end
};
this.bounds(newBounds);
/**
* Event that is triggered when clock offsets change.
* @event clockOffsets
* @memberof module:openmct.TimeAPI~
* @property {ClockOffsets} clockOffsets The newly activated clock
* offsets.
*/
this.emit('clockOffsets', offsets);
}
return this.offsets;
}
/**
* Stop the currently active clock from ticking, and unset it. This will
* revert all views to showing a static time frame defined by the current
* bounds.
*/
stopClock() {
if (this.activeClock) {
this.clock(undefined, undefined);
}
}
/**
* Set the active clock. Tick source will be immediately subscribed to
* and ticking will begin. Offsets from 'now' must also be provided. A clock
* can be unset by calling {@link stopClock}.
*
* @param {Clock || string} keyOrClock The clock to activate, or its key
* @param {ClockOffsets} offsets on each tick these will be used to calculate
* the start and end bounds. This maintains a sliding time window of a fixed
* width that automatically updates.
* @fires module:openmct.TimeAPI~clock
* @return {Clock} the currently active clock;
*/
clock(keyOrClock, offsets) {
if (arguments.length === 2) {
let clock;
if (typeof keyOrClock === 'string') {
clock = this.clocks.get(keyOrClock);
if (clock === undefined) {
throw "Unknown clock '" + keyOrClock + "'. Has it been registered with 'addClock'?";
}
//Return a copy to prevent direct mutation of time conductor bounds.
return JSON.parse(JSON.stringify(this.boundsVal));
}
/**
* Validate the given offsets. This can be used for pre-validation of
* offsets, for example by views validating user inputs.
* @param {ClockOffsets} offsets The start and end offsets from a 'now' value.
* @returns { ValidationResult } A validation error, and true/false if valid or not
* @memberof module:openmct.TimeAPI#
* @method validateOffsets
*/
validateOffsets(offsets) {
if ((offsets.start === undefined)
|| (offsets.end === undefined)
|| isNaN(offsets.start)
|| isNaN(offsets.end)
) {
return {
valid: false,
message: "Start and end offsets must be specified as integer values"
};
} else if (offsets.start >= offsets.end) {
return {
valid: false,
message: "Specified start offset must be < end offset"
};
} else if (typeof keyOrClock === 'object') {
clock = keyOrClock;
if (!this.clocks.has(clock.key)) {
throw "Unknown clock '" + keyOrClock.key + "'. Has it been registered with 'addClock'?";
}
}
return {
valid: true,
message: ''
};
const previousClock = this.activeClock;
if (previousClock !== undefined) {
previousClock.off('tick', this.tick);
}
this.activeClock = clock;
/**
* The active clock has changed. Clock can be unset by calling {@link stopClock}
* @event clock
* @memberof module:openmct.TimeAPI~
* @property {Clock} clock The newly activated clock, or undefined
* if the system is no longer following a clock source
*/
this.emit('clock', this.activeClock);
if (this.activeClock !== undefined) {
this.clockOffsets(offsets);
this.activeClock.on('tick', this.tick);
}
} else if (arguments.length === 1) {
throw 'When setting the clock, clock offsets must also be provided';
}
/**
* @typedef {Object} TimeBounds
* @property {number} start The start time displayed by the time conductor
* in ms since epoch. Epoch determined by currently active time system
* @property {number} end The end time displayed by the time conductor in ms
* since epoch.
* @memberof module:openmct.TimeAPI~
*/
return this.activeClock;
}
/**
* Clock offsets are used to calculate temporal bounds when the system is
* ticking on a clock source.
*
* @typedef {object} ClockOffsets
* @property {number} start A time span relative to the current value of the
* ticking clock, from which start bounds will be calculated. This value must
* be < 0. When a clock is active, bounds will be calculated automatically
* based on the value provided by the clock, and the defined clock offsets.
* @property {number} end A time span relative to the current value of the
* ticking clock, from which end bounds will be calculated. This value must
* be >= 0.
*/
/**
* Get or set the currently applied clock offsets. If no parameter is provided,
* the current value will be returned. If provided, the new value will be
* used as the new clock offsets.
* @param {ClockOffsets} offsets
* @returns {ClockOffsets}
*/
clockOffsets(offsets) {
if (arguments.length > 0) {
const validationResult = this.validateOffsets(offsets);
if (validationResult.valid !== true) {
throw new Error(validationResult.message);
}
this.offsets = offsets;
const currentValue = this.activeClock.currentValue();
const newBounds = {
start: currentValue + offsets.start,
end: currentValue + offsets.end
};
this.bounds(newBounds);
/**
* Event that is triggered when clock offsets change.
* @event clockOffsets
* @memberof module:openmct.TimeAPI~
* @property {ClockOffsets} clockOffsets The newly activated clock
* offsets.
*/
this.emit("clockOffsets", offsets);
}
return this.offsets;
/**
* Update bounds based on provided time and current offsets
* @param {number} timestamp A time from which bounds will be calculated
* using current offsets.
*/
tick(timestamp) {
if (!this.activeClock) {
return;
}
/**
* Stop the currently active clock from ticking, and unset it. This will
* revert all views to showing a static time frame defined by the current
* bounds.
*/
stopClock() {
if (this.activeClock) {
this.clock(undefined, undefined);
}
const newBounds = {
start: timestamp + this.offsets.start,
end: timestamp + this.offsets.end
};
this.boundsVal = newBounds;
this.emit('bounds', this.boundsVal, true);
}
/**
* Checks if this time context is in real-time mode or not.
* @returns {boolean} true if this context is in real-time mode, false if not
*/
isRealTime() {
if (this.clock()) {
return true;
}
/**
* Set the active clock. Tick source will be immediately subscribed to
* and ticking will begin. Offsets from 'now' must also be provided. A clock
* can be unset by calling {@link stopClock}.
*
* @param {Clock || string} keyOrClock The clock to activate, or its key
* @param {ClockOffsets} offsets on each tick these will be used to calculate
* the start and end bounds. This maintains a sliding time window of a fixed
* width that automatically updates.
* @fires module:openmct.TimeAPI~clock
* @return {Clock} the currently active clock;
*/
clock(keyOrClock, offsets) {
if (arguments.length === 2) {
let clock;
if (typeof keyOrClock === 'string') {
clock = this.clocks.get(keyOrClock);
if (clock === undefined) {
throw "Unknown clock '" + keyOrClock + "'. Has it been registered with 'addClock'?";
}
} else if (typeof keyOrClock === 'object') {
clock = keyOrClock;
if (!this.clocks.has(clock.key)) {
throw "Unknown clock '" + keyOrClock.key + "'. Has it been registered with 'addClock'?";
}
}
const previousClock = this.activeClock;
if (previousClock !== undefined) {
previousClock.off("tick", this.tick);
}
this.activeClock = clock;
/**
* The active clock has changed. Clock can be unset by calling {@link stopClock}
* @event clock
* @memberof module:openmct.TimeAPI~
* @property {Clock} clock The newly activated clock, or undefined
* if the system is no longer following a clock source
*/
this.emit("clock", this.activeClock);
if (this.activeClock !== undefined) {
this.clockOffsets(offsets);
this.activeClock.on("tick", this.tick);
}
} else if (arguments.length === 1) {
throw "When setting the clock, clock offsets must also be provided";
}
return this.activeClock;
}
/**
* Update bounds based on provided time and current offsets
* @param {number} timestamp A time from which bounds will be calculated
* using current offsets.
*/
tick(timestamp) {
if (!this.activeClock) {
return;
}
const newBounds = {
start: timestamp + this.offsets.start,
end: timestamp + this.offsets.end
};
this.boundsVal = newBounds;
this.emit('bounds', this.boundsVal, true);
}
/**
* Checks if this time context is in real-time mode or not.
* @returns {boolean} true if this context is in real-time mode, false if not
*/
isRealTime() {
if (this.clock()) {
return true;
}
return false;
}
return false;
}
}
export default TimeContext;

View File

@@ -20,227 +20,242 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import TimeAPI from "./TimeAPI";
import {createOpenMct} from "utils/testing";
describe("The Independent Time API", function () {
let api;
let domainObjectKey;
let clockKey;
let clock;
let bounds;
let independentBounds;
let eventListener;
let openmct;
beforeEach(function () {
openmct = createOpenMct();
api = new TimeAPI(openmct);
clockKey = "someClockKey";
clock = jasmine.createSpyObj("clock", [
"on",
"off",
"currentValue"
]);
clock.currentValue.and.returnValue(100);
clock.key = clockKey;
api.addClock(clock);
domainObjectKey = 'test-key';
bounds = {
start: 0,
end: 1
};
api.bounds(bounds);
independentBounds = {
start: 10,
end: 11
};
eventListener = jasmine.createSpy("eventListener");
});
it("Creates an independent time context", () => {
let timeContext = api.getContextForView([{
identifier: {
namespace: '',
key: domainObjectKey
}
}]);
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
expect(timeContext.bounds()).toEqual(independentBounds);
destroyTimeContext();
});
it("Gets an independent time context given the objectPath", () => {
let timeContext = api.getContextForView([{ identifier: domainObjectKey },
{
identifier: {
namespace: '',
key: 'blah'
}
}]);
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
expect(timeContext.bounds()).toEqual(independentBounds);
destroyTimeContext();
});
it("defaults to the global time context given the objectPath", () => {
let timeContext = api.getContextForView([{
identifier: {
namespace: '',
key: 'blah'
}
}]);
expect(timeContext.bounds()).toEqual(bounds);
});
it("follows a parent time context given the objectPath", () => {
api.getContextForView([{
identifier: {
namespace: '',
key: 'blah'
}
}]);
let destroyTimeContext = api.addIndependentContext('blah', independentBounds);
let timeContext = api.getContextForView([{
identifier: {
namespace: '',
key: domainObjectKey
}
}, {
identifier: {
namespace: '',
key: 'blah'
}
}]);
expect(timeContext.bounds()).toEqual(independentBounds);
destroyTimeContext();
expect(timeContext.bounds()).toEqual(bounds);
});
it("uses an object's independent time context if the parent doesn't have one", () => {
const domainObjectKey2 = `${domainObjectKey}-2`;
const domainObjectKey3 = `${domainObjectKey}-3`;
let timeContext = api.getContextForView([{
identifier: {
namespace: '',
key: domainObjectKey
}
}]);
let timeContext2 = api.getContextForView([{
identifier: {
namespace: '',
key: domainObjectKey2
}
}]);
let timeContext3 = api.getContextForView([{
identifier: {
namespace: '',
key: domainObjectKey3
}
}]);
// all bounds follow global time context
expect(timeContext.bounds()).toEqual(bounds);
expect(timeContext2.bounds()).toEqual(bounds);
expect(timeContext3.bounds()).toEqual(bounds);
// only first item has own context
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
expect(timeContext.bounds()).toEqual(independentBounds);
expect(timeContext2.bounds()).toEqual(bounds);
expect(timeContext3.bounds()).toEqual(bounds);
// first and second item have own context
let destroyTimeContext2 = api.addIndependentContext(domainObjectKey2, independentBounds);
expect(timeContext.bounds()).toEqual(independentBounds);
expect(timeContext2.bounds()).toEqual(independentBounds);
expect(timeContext3.bounds()).toEqual(bounds);
// all items have own time context
let destroyTimeContext3 = api.addIndependentContext(domainObjectKey3, independentBounds);
expect(timeContext.bounds()).toEqual(independentBounds);
expect(timeContext2.bounds()).toEqual(independentBounds);
expect(timeContext3.bounds()).toEqual(independentBounds);
//remove own contexts one at a time - should revert to global time context
destroyTimeContext();
expect(timeContext.bounds()).toEqual(bounds);
expect(timeContext2.bounds()).toEqual(independentBounds);
expect(timeContext3.bounds()).toEqual(independentBounds);
destroyTimeContext2();
expect(timeContext.bounds()).toEqual(bounds);
expect(timeContext2.bounds()).toEqual(bounds);
expect(timeContext3.bounds()).toEqual(independentBounds);
destroyTimeContext3();
expect(timeContext.bounds()).toEqual(bounds);
expect(timeContext2.bounds()).toEqual(bounds);
expect(timeContext3.bounds()).toEqual(bounds);
});
it("Allows setting of valid bounds", function () {
bounds = {
start: 0,
end: 1
};
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
expect(timeContext.bounds()).not.toEqual(bounds);
timeContext.bounds(bounds);
expect(timeContext.bounds()).toEqual(bounds);
destroyTimeContext();
});
it("Disallows setting of invalid bounds", function () {
bounds = {
start: 1,
end: 0
};
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
expect(timeContext.bounds()).not.toBe(bounds);
expect(timeContext.bounds.bind(timeContext, bounds)).toThrow();
expect(timeContext.bounds()).not.toEqual(bounds);
bounds = {start: 1};
expect(timeContext.bounds()).not.toEqual(bounds);
expect(timeContext.bounds.bind(timeContext, bounds)).toThrow();
expect(timeContext.bounds()).not.toEqual(bounds);
destroyTimeContext();
});
it("Emits an event when bounds change", function () {
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
expect(eventListener).not.toHaveBeenCalled();
timeContext.on('bounds', eventListener);
timeContext.bounds(bounds);
expect(eventListener).toHaveBeenCalledWith(bounds, false);
destroyTimeContext();
});
it("Emits an event when bounds change on the global context", function () {
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
expect(eventListener).not.toHaveBeenCalled();
timeContext.on('bounds', eventListener);
timeContext.bounds(bounds);
expect(eventListener).toHaveBeenCalledWith(bounds, false);
});
describe(" when using real time clock", function () {
const mockOffsets = {
start: 10,
end: 11
};
it("Emits an event when bounds change based on current value", function () {
let timeContext = api.getContextForView([{identifier: domainObjectKey}]);
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
expect(eventListener).not.toHaveBeenCalled();
timeContext.clock('someClockKey', mockOffsets);
timeContext.on('bounds', eventListener);
timeContext.tick(10);
expect(eventListener).toHaveBeenCalledWith({
start: 20,
end: 21
}, true);
destroyTimeContext();
});
import TimeAPI from './TimeAPI';
import { createOpenMct } from 'utils/testing';
describe('The Independent Time API', function () {
let api;
let domainObjectKey;
let clockKey;
let clock;
let bounds;
let independentBounds;
let eventListener;
let openmct;
beforeEach(function () {
openmct = createOpenMct();
api = new TimeAPI(openmct);
clockKey = 'someClockKey';
clock = jasmine.createSpyObj('clock', ['on', 'off', 'currentValue']);
clock.currentValue.and.returnValue(100);
clock.key = clockKey;
api.addClock(clock);
domainObjectKey = 'test-key';
bounds = {
start: 0,
end: 1
};
api.bounds(bounds);
independentBounds = {
start: 10,
end: 11
};
eventListener = jasmine.createSpy('eventListener');
});
it('Creates an independent time context', () => {
let timeContext = api.getContextForView([
{
identifier: {
namespace: '',
key: domainObjectKey
}
}
]);
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
expect(timeContext.bounds()).toEqual(independentBounds);
destroyTimeContext();
});
it('Gets an independent time context given the objectPath', () => {
let timeContext = api.getContextForView([
{ identifier: domainObjectKey },
{
identifier: {
namespace: '',
key: 'blah'
}
}
]);
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
expect(timeContext.bounds()).toEqual(independentBounds);
destroyTimeContext();
});
it('defaults to the global time context given the objectPath', () => {
let timeContext = api.getContextForView([
{
identifier: {
namespace: '',
key: 'blah'
}
}
]);
expect(timeContext.bounds()).toEqual(bounds);
});
it('follows a parent time context given the objectPath', () => {
api.getContextForView([
{
identifier: {
namespace: '',
key: 'blah'
}
}
]);
let destroyTimeContext = api.addIndependentContext('blah', independentBounds);
let timeContext = api.getContextForView([
{
identifier: {
namespace: '',
key: domainObjectKey
}
},
{
identifier: {
namespace: '',
key: 'blah'
}
}
]);
expect(timeContext.bounds()).toEqual(independentBounds);
destroyTimeContext();
expect(timeContext.bounds()).toEqual(bounds);
});
it("uses an object's independent time context if the parent doesn't have one", () => {
const domainObjectKey2 = `${domainObjectKey}-2`;
const domainObjectKey3 = `${domainObjectKey}-3`;
let timeContext = api.getContextForView([
{
identifier: {
namespace: '',
key: domainObjectKey
}
}
]);
let timeContext2 = api.getContextForView([
{
identifier: {
namespace: '',
key: domainObjectKey2
}
}
]);
let timeContext3 = api.getContextForView([
{
identifier: {
namespace: '',
key: domainObjectKey3
}
}
]);
// all bounds follow global time context
expect(timeContext.bounds()).toEqual(bounds);
expect(timeContext2.bounds()).toEqual(bounds);
expect(timeContext3.bounds()).toEqual(bounds);
// only first item has own context
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
expect(timeContext.bounds()).toEqual(independentBounds);
expect(timeContext2.bounds()).toEqual(bounds);
expect(timeContext3.bounds()).toEqual(bounds);
// first and second item have own context
let destroyTimeContext2 = api.addIndependentContext(domainObjectKey2, independentBounds);
expect(timeContext.bounds()).toEqual(independentBounds);
expect(timeContext2.bounds()).toEqual(independentBounds);
expect(timeContext3.bounds()).toEqual(bounds);
// all items have own time context
let destroyTimeContext3 = api.addIndependentContext(domainObjectKey3, independentBounds);
expect(timeContext.bounds()).toEqual(independentBounds);
expect(timeContext2.bounds()).toEqual(independentBounds);
expect(timeContext3.bounds()).toEqual(independentBounds);
//remove own contexts one at a time - should revert to global time context
destroyTimeContext();
expect(timeContext.bounds()).toEqual(bounds);
expect(timeContext2.bounds()).toEqual(independentBounds);
expect(timeContext3.bounds()).toEqual(independentBounds);
destroyTimeContext2();
expect(timeContext.bounds()).toEqual(bounds);
expect(timeContext2.bounds()).toEqual(bounds);
expect(timeContext3.bounds()).toEqual(independentBounds);
destroyTimeContext3();
expect(timeContext.bounds()).toEqual(bounds);
expect(timeContext2.bounds()).toEqual(bounds);
expect(timeContext3.bounds()).toEqual(bounds);
});
it('Allows setting of valid bounds', function () {
bounds = {
start: 0,
end: 1
};
let timeContext = api.getContextForView([{ identifier: domainObjectKey }]);
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
expect(timeContext.bounds()).not.toEqual(bounds);
timeContext.bounds(bounds);
expect(timeContext.bounds()).toEqual(bounds);
destroyTimeContext();
});
it('Disallows setting of invalid bounds', function () {
bounds = {
start: 1,
end: 0
};
let timeContext = api.getContextForView([{ identifier: domainObjectKey }]);
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
expect(timeContext.bounds()).not.toBe(bounds);
expect(timeContext.bounds.bind(timeContext, bounds)).toThrow();
expect(timeContext.bounds()).not.toEqual(bounds);
bounds = { start: 1 };
expect(timeContext.bounds()).not.toEqual(bounds);
expect(timeContext.bounds.bind(timeContext, bounds)).toThrow();
expect(timeContext.bounds()).not.toEqual(bounds);
destroyTimeContext();
});
it('Emits an event when bounds change', function () {
let timeContext = api.getContextForView([{ identifier: domainObjectKey }]);
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
expect(eventListener).not.toHaveBeenCalled();
timeContext.on('bounds', eventListener);
timeContext.bounds(bounds);
expect(eventListener).toHaveBeenCalledWith(bounds, false);
destroyTimeContext();
});
it('Emits an event when bounds change on the global context', function () {
let timeContext = api.getContextForView([{ identifier: domainObjectKey }]);
expect(eventListener).not.toHaveBeenCalled();
timeContext.on('bounds', eventListener);
timeContext.bounds(bounds);
expect(eventListener).toHaveBeenCalledWith(bounds, false);
});
describe(' when using real time clock', function () {
const mockOffsets = {
start: 10,
end: 11
};
it('Emits an event when bounds change based on current value', function () {
let timeContext = api.getContextForView([{ identifier: domainObjectKey }]);
let destroyTimeContext = api.addIndependentContext(domainObjectKey, independentBounds);
expect(eventListener).not.toHaveBeenCalled();
timeContext.clock('someClockKey', mockOffsets);
timeContext.on('bounds', eventListener);
timeContext.tick(10);
expect(eventListener).toHaveBeenCalledWith(
{
start: 20,
end: 21
},
true
);
destroyTimeContext();
});
});
});

View File

@@ -29,93 +29,93 @@
* @memberof module:openmct
*/
export default class Type {
constructor(definition) {
this.definition = definition;
if (definition.key) {
this.key = definition.key;
}
constructor(definition) {
this.definition = definition;
if (definition.key) {
this.key = definition.key;
}
/**
* Create a type definition from a legacy definition.
*/
static definitionFromLegacyDefinition(legacyDefinition) {
let definition = {};
definition.name = legacyDefinition.name;
definition.cssClass = legacyDefinition.cssClass;
definition.description = legacyDefinition.description;
definition.form = legacyDefinition.properties;
if (legacyDefinition.telemetry !== undefined) {
let telemetry = {
values: []
};
}
/**
* Create a type definition from a legacy definition.
*/
static definitionFromLegacyDefinition(legacyDefinition) {
let definition = {};
definition.name = legacyDefinition.name;
definition.cssClass = legacyDefinition.cssClass;
definition.description = legacyDefinition.description;
definition.form = legacyDefinition.properties;
if (legacyDefinition.telemetry !== undefined) {
let telemetry = {
values: []
};
if (legacyDefinition.telemetry.domains !== undefined) {
legacyDefinition.telemetry.domains.forEach((domain, index) => {
domain.hints = {
domain: index
};
telemetry.values.push(domain);
});
}
if (legacyDefinition.telemetry.domains !== undefined) {
legacyDefinition.telemetry.domains.forEach((domain, index) => {
domain.hints = {
domain: index
};
telemetry.values.push(domain);
});
}
if (legacyDefinition.telemetry.ranges !== undefined) {
legacyDefinition.telemetry.ranges.forEach((range, index) => {
range.hints = {
range: index
};
telemetry.values.push(range);
});
}
if (legacyDefinition.telemetry.ranges !== undefined) {
legacyDefinition.telemetry.ranges.forEach((range, index) => {
range.hints = {
range: index
};
telemetry.values.push(range);
});
}
definition.telemetry = telemetry;
}
if (legacyDefinition.model) {
definition.initialize = function (model) {
for (let [k, v] of Object.entries(legacyDefinition.model)) {
model[k] = JSON.parse(JSON.stringify(v));
}
};
}
if (legacyDefinition.features && legacyDefinition.features.includes("creation")) {
definition.creatable = true;
}
return definition;
definition.telemetry = telemetry;
}
/**
* Check if a domain object is an instance of this type.
* @param domainObject
* @returns {boolean} true if the domain object is of this type
* @memberof module:openmct.Type#
* @method check
*/
check(domainObject) {
// Depends on assignment from MCT.
return domainObject.type === this.key;
}
/**
* Get a definition for this type that can be registered using the
* legacy bundle format.
* @private
*/
toLegacyDefinition() {
const def = {};
def.name = this.definition.name;
def.cssClass = this.definition.cssClass;
def.description = this.definition.description;
def.properties = this.definition.form;
if (this.definition.initialize) {
def.model = {};
this.definition.initialize(def.model);
if (legacyDefinition.model) {
definition.initialize = function (model) {
for (let [k, v] of Object.entries(legacyDefinition.model)) {
model[k] = JSON.parse(JSON.stringify(v));
}
if (this.definition.creatable) {
def.features = ['creation'];
}
return def;
};
}
if (legacyDefinition.features && legacyDefinition.features.includes('creation')) {
definition.creatable = true;
}
return definition;
}
/**
* Check if a domain object is an instance of this type.
* @param domainObject
* @returns {boolean} true if the domain object is of this type
* @memberof module:openmct.Type#
* @method check
*/
check(domainObject) {
// Depends on assignment from MCT.
return domainObject.type === this.key;
}
/**
* Get a definition for this type that can be registered using the
* legacy bundle format.
* @private
*/
toLegacyDefinition() {
const def = {};
def.name = this.definition.name;
def.cssClass = this.definition.cssClass;
def.description = this.definition.description;
def.properties = this.definition.form;
if (this.definition.initialize) {
def.model = {};
this.definition.initialize(def.model);
}
if (this.definition.creatable) {
def.features = ['creation'];
}
return def;
}
}

View File

@@ -22,9 +22,9 @@
import Type from './Type';
const UNKNOWN_TYPE = new Type({
key: "unknown",
name: "Unknown Type",
cssClass: "icon-object-unknown"
key: 'unknown',
name: 'Unknown Type',
cssClass: 'icon-object-unknown'
});
/**
@@ -46,60 +46,60 @@ const UNKNOWN_TYPE = new Type({
* @memberof module:openmct
*/
export default class TypeRegistry {
constructor() {
this.types = {};
}
/**
* Register a new object type.
*
* @param {string} typeKey a string identifier for this type
* @param {module:openmct.Type} type the type to add
* @method addType
* @memberof module:openmct.TypeRegistry#
*/
addType(typeKey, typeDef) {
this.standardizeType(typeDef);
this.types[typeKey] = new Type(typeDef);
}
/**
* Takes a typeDef, standardizes it, and logs warnings about unsupported
* usage.
* @private
*/
standardizeType(typeDef) {
if (Object.prototype.hasOwnProperty.call(typeDef, 'label')) {
if (!typeDef.name) {
typeDef.name = typeDef.label;
}
constructor() {
this.types = {};
}
/**
* Register a new object type.
*
* @param {string} typeKey a string identifier for this type
* @param {module:openmct.Type} type the type to add
* @method addType
* @memberof module:openmct.TypeRegistry#
*/
addType(typeKey, typeDef) {
this.standardizeType(typeDef);
this.types[typeKey] = new Type(typeDef);
}
/**
* Takes a typeDef, standardizes it, and logs warnings about unsupported
* usage.
* @private
*/
standardizeType(typeDef) {
if (Object.prototype.hasOwnProperty.call(typeDef, 'label')) {
if (!typeDef.name) {
typeDef.name = typeDef.label;
}
delete typeDef.label;
}
}
/**
* List keys for all registered types.
* @method listKeys
* @memberof module:openmct.TypeRegistry#
* @returns {string[]} all registered type keys
*/
listKeys() {
return Object.keys(this.types);
}
/**
* Retrieve a registered type by its key.
* @method get
* @param {string} typeKey the key for this type
* @memberof module:openmct.TypeRegistry#
* @returns {module:openmct.Type} the registered type
*/
get(typeKey) {
return this.types[typeKey] || UNKNOWN_TYPE;
}
importLegacyTypes(types) {
types.filter((t) => this.get(t.key) === UNKNOWN_TYPE)
.forEach((type) => {
let def = Type.definitionFromLegacyDefinition(type);
this.addType(type.key, def);
});
delete typeDef.label;
}
}
/**
* List keys for all registered types.
* @method listKeys
* @memberof module:openmct.TypeRegistry#
* @returns {string[]} all registered type keys
*/
listKeys() {
return Object.keys(this.types);
}
/**
* Retrieve a registered type by its key.
* @method get
* @param {string} typeKey the key for this type
* @memberof module:openmct.TypeRegistry#
* @returns {module:openmct.Type} the registered type
*/
get(typeKey) {
return this.types[typeKey] || UNKNOWN_TYPE;
}
importLegacyTypes(types) {
types
.filter((t) => this.get(t.key) === UNKNOWN_TYPE)
.forEach((type) => {
let def = Type.definitionFromLegacyDefinition(type);
this.addType(type.key, def);
});
}
}

View File

@@ -23,33 +23,33 @@
import TypeRegistry from './TypeRegistry';
describe('The Type API', function () {
let typeRegistryInstance;
let typeRegistryInstance;
beforeEach(function () {
typeRegistryInstance = new TypeRegistry ();
typeRegistryInstance.addType('testType', {
name: 'Test Type',
description: 'This is a test type.',
creatable: true
});
beforeEach(function () {
typeRegistryInstance = new TypeRegistry();
typeRegistryInstance.addType('testType', {
name: 'Test Type',
description: 'This is a test type.',
creatable: true
});
});
it('types can be standardized', function () {
typeRegistryInstance.addType('standardizationTestType', {
label: 'Test Type',
description: 'This is a test type.',
creatable: true
});
typeRegistryInstance.standardizeType(typeRegistryInstance.types.standardizationTestType);
expect(typeRegistryInstance.get('standardizationTestType').definition.label).toBeUndefined();
expect(typeRegistryInstance.get('standardizationTestType').definition.name).toBe('Test Type');
it('types can be standardized', function () {
typeRegistryInstance.addType('standardizationTestType', {
label: 'Test Type',
description: 'This is a test type.',
creatable: true
});
typeRegistryInstance.standardizeType(typeRegistryInstance.types.standardizationTestType);
expect(typeRegistryInstance.get('standardizationTestType').definition.label).toBeUndefined();
expect(typeRegistryInstance.get('standardizationTestType').definition.name).toBe('Test Type');
});
it('new types are registered successfully and can be retrieved', function () {
expect(typeRegistryInstance.get('testType').definition.name).toBe('Test Type');
});
it('new types are registered successfully and can be retrieved', function () {
expect(typeRegistryInstance.get('testType').definition.name).toBe('Test Type');
});
it('type registry contains new keys', function () {
expect(typeRegistryInstance.listKeys ()).toContain('testType');
});
it('type registry contains new keys', function () {
expect(typeRegistryInstance.listKeys()).toContain('testType');
});
});

View File

@@ -19,259 +19,259 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import EventEmitter from "EventEmitter";
import EventEmitter from 'EventEmitter';
export default class StatusAPI extends EventEmitter {
#userAPI;
#openmct;
#userAPI;
#openmct;
constructor(userAPI, openmct) {
super();
this.#userAPI = userAPI;
this.#openmct = openmct;
constructor(userAPI, openmct) {
super();
this.#userAPI = userAPI;
this.#openmct = openmct;
this.onProviderStatusChange = this.onProviderStatusChange.bind(this);
this.onProviderPollQuestionChange = this.onProviderPollQuestionChange.bind(this);
this.listenToStatusEvents = this.listenToStatusEvents.bind(this);
this.onProviderStatusChange = this.onProviderStatusChange.bind(this);
this.onProviderPollQuestionChange = this.onProviderPollQuestionChange.bind(this);
this.listenToStatusEvents = this.listenToStatusEvents.bind(this);
this.#openmct.once('destroy', () => {
const provider = this.#userAPI.getProvider();
this.#openmct.once('destroy', () => {
const provider = this.#userAPI.getProvider();
if (typeof provider?.off === 'function') {
provider.off('statusChange', this.onProviderStatusChange);
provider.off('pollQuestionChange', this.onProviderPollQuestionChange);
}
});
if (typeof provider?.off === 'function') {
provider.off('statusChange', this.onProviderStatusChange);
provider.off('pollQuestionChange', this.onProviderPollQuestionChange);
}
});
this.#userAPI.on('providerAdded', this.listenToStatusEvents);
this.#userAPI.on('providerAdded', this.listenToStatusEvents);
}
/**
* Fetch the currently defined operator status poll question. When presented with a status poll question, all operators will reply with their current status.
* @returns {Promise<PollQuestion>}
*/
getPollQuestion() {
const provider = this.#userAPI.getProvider();
if (provider.getPollQuestion) {
return provider.getPollQuestion();
} else {
this.#userAPI.error('User provider does not support polling questions');
}
}
/**
* Fetch the currently defined operator status poll question. When presented with a status poll question, all operators will reply with their current status.
* @returns {Promise<PollQuestion>}
*/
getPollQuestion() {
const provider = this.#userAPI.getProvider();
/**
* Set a poll question for operators to respond to. When presented with a status poll question, all operators will reply with their current status.
* @param {String} questionText - The text of the question
* @returns {Promise<Boolean>} true if operation was successful, otherwise false.
*/
async setPollQuestion(questionText) {
const canSetPollQuestion = await this.canSetPollQuestion();
if (provider.getPollQuestion) {
return provider.getPollQuestion();
} else {
this.#userAPI.error("User provider does not support polling questions");
}
if (canSetPollQuestion) {
const provider = this.#userAPI.getProvider();
const result = await provider.setPollQuestion(questionText);
try {
await this.resetAllStatuses();
} catch (error) {
console.warn('Poll question set but unable to clear operator statuses.');
console.error(error);
}
return result;
} else {
this.#userAPI.error('User provider does not support setting polling question');
}
}
/**
* Set a poll question for operators to respond to. When presented with a status poll question, all operators will reply with their current status.
* @param {String} questionText - The text of the question
* @returns {Promise<Boolean>} true if operation was successful, otherwise false.
*/
async setPollQuestion(questionText) {
const canSetPollQuestion = await this.canSetPollQuestion();
/**
* Can the currently logged in user set the operator status poll question.
* @returns {Promise<Boolean>}
*/
canSetPollQuestion() {
const provider = this.#userAPI.getProvider();
if (canSetPollQuestion) {
const provider = this.#userAPI.getProvider();
const result = await provider.setPollQuestion(questionText);
try {
await this.resetAllStatuses();
} catch (error) {
console.warn("Poll question set but unable to clear operator statuses.");
console.error(error);
}
return result;
} else {
this.#userAPI.error("User provider does not support setting polling question");
}
if (provider.canSetPollQuestion) {
return provider.canSetPollQuestion();
} else {
return Promise.resolve(false);
}
}
/**
* Can the currently logged in user set the operator status poll question.
* @returns {Promise<Boolean>}
*/
canSetPollQuestion() {
const provider = this.#userAPI.getProvider();
/**
* @returns {Promise<Array<Status>>} the complete list of possible states that an operator can reply to a poll question with.
*/
async getPossibleStatuses() {
const provider = this.#userAPI.getProvider();
if (provider.canSetPollQuestion) {
return provider.canSetPollQuestion();
} else {
return Promise.resolve(false);
}
if (provider.getPossibleStatuses) {
const possibleStatuses = (await provider.getPossibleStatuses()) || [];
return possibleStatuses.map((status) => status);
} else {
this.#userAPI.error('User provider cannot provide statuses');
}
}
/**
* @returns {Promise<Array<Status>>} the complete list of possible states that an operator can reply to a poll question with.
*/
async getPossibleStatuses() {
const provider = this.#userAPI.getProvider();
/**
* @param {import("./UserAPI").Role} role The role to fetch the current status for.
* @returns {Promise<Status>} the current status of the provided role
*/
async getStatusForRole(role) {
const provider = this.#userAPI.getProvider();
if (provider.getPossibleStatuses) {
const possibleStatuses = await provider.getPossibleStatuses() || [];
if (provider.getStatusForRole) {
const status = await provider.getStatusForRole(role);
return possibleStatuses.map(status => status);
} else {
this.#userAPI.error("User provider cannot provide statuses");
}
return status;
} else {
this.#userAPI.error('User provider does not support role status');
}
}
/**
* @param {import("./UserAPI").Role} role The role to fetch the current status for.
* @returns {Promise<Status>} the current status of the provided role
*/
async getStatusForRole(role) {
const provider = this.#userAPI.getProvider();
/**
* @param {import("./UserAPI").Role} role
* @returns {Promise<Boolean>} true if the configured UserProvider can provide status for the given role
* @see StatusUserProvider
*/
canProvideStatusForRole(role) {
const provider = this.#userAPI.getProvider();
if (provider.getStatusForRole) {
const status = await provider.getStatusForRole(role);
return status;
} else {
this.#userAPI.error("User provider does not support role status");
}
if (provider.canProvideStatusForRole) {
return provider.canProvideStatusForRole(role);
} else {
return false;
}
}
/**
* @param {import("./UserAPI").Role} role
* @returns {Promise<Boolean>} true if the configured UserProvider can provide status for the given role
* @see StatusUserProvider
*/
canProvideStatusForRole(role) {
const provider = this.#userAPI.getProvider();
/**
* @param {import("./UserAPI").Role} role The role to set the status for.
* @param {Status} status The status to set for the provided role
* @returns {Promise<Boolean>} true if operation was successful, otherwise false.
*/
setStatusForRole(role, status) {
const provider = this.#userAPI.getProvider();
if (provider.canProvideStatusForRole) {
return provider.canProvideStatusForRole(role);
} else {
return false;
}
if (provider.setStatusForRole) {
return provider.setStatusForRole(role, status);
} else {
this.#userAPI.error('User provider does not support setting role status');
}
}
/**
* @param {import("./UserAPI").Role} role The role to set the status for.
* @param {Status} status The status to set for the provided role
* @returns {Promise<Boolean>} true if operation was successful, otherwise false.
*/
setStatusForRole(role, status) {
const provider = this.#userAPI.getProvider();
/**
* Resets the status of the provided role back to its default status.
* @param {import("./UserAPI").Role} role The role to set the status for.
* @returns {Promise<Boolean>} true if operation was successful, otherwise false.
*/
async resetStatusForRole(role) {
const provider = this.#userAPI.getProvider();
const defaultStatus = await this.getDefaultStatusForRole(role);
if (provider.setStatusForRole) {
return provider.setStatusForRole(role, status);
} else {
this.#userAPI.error("User provider does not support setting role status");
}
if (provider.setStatusForRole) {
return provider.setStatusForRole(role, defaultStatus);
} else {
this.#userAPI.error('User provider does not support resetting role status');
}
}
/**
* Resets the status of the provided role back to its default status.
* @param {import("./UserAPI").Role} role The role to set the status for.
* @returns {Promise<Boolean>} true if operation was successful, otherwise false.
*/
async resetStatusForRole(role) {
const provider = this.#userAPI.getProvider();
const defaultStatus = await this.getDefaultStatusForRole(role);
/**
* Resets the status of all operators to their default status
* @returns {Promise<Boolean>} true if operation was successful, otherwise false.
*/
async resetAllStatuses() {
const allStatusRoles = await this.getAllStatusRoles();
if (provider.setStatusForRole) {
return provider.setStatusForRole(role, defaultStatus);
} else {
this.#userAPI.error("User provider does not support resetting role status");
}
return Promise.all(allStatusRoles.map((role) => this.resetStatusForRole(role)));
}
/**
* The default status. This is the status that will be used before the user has selected any status.
* @param {import("./UserAPI").Role} role
* @returns {Promise<Status>} the default operator status if no other has been set.
*/
async getDefaultStatusForRole(role) {
const provider = this.#userAPI.getProvider();
const defaultStatus = await provider.getDefaultStatusForRole(role);
return defaultStatus;
}
/**
* All possible status roles. A status role is a user role that can provide status. In some systems
* this may be all user roles, but there may be cases where some users are not are not polled
* for status if they do not have a real-time operational role.
*
* @returns {Promise<Array<import("./UserAPI").Role>>} the default operator status if no other has been set.
*/
getAllStatusRoles() {
const provider = this.#userAPI.getProvider();
if (provider.getAllStatusRoles) {
return provider.getAllStatusRoles();
} else {
this.#userAPI.error('User provider cannot provide all status roles');
}
}
/**
* Resets the status of all operators to their default status
* @returns {Promise<Boolean>} true if operation was successful, otherwise false.
*/
async resetAllStatuses() {
const allStatusRoles = await this.getAllStatusRoles();
/**
* The status role of the current user. A user may have multiple roles, but will only have one role
* that provides status at any time.
* @returns {Promise<import("./UserAPI").Role>} the role for which the current user can provide status.
*/
getStatusRoleForCurrentUser() {
const provider = this.#userAPI.getProvider();
return Promise.all(allStatusRoles.map(role => this.resetStatusForRole(role)));
if (provider.getStatusRoleForCurrentUser) {
return provider.getStatusRoleForCurrentUser();
} else {
this.#userAPI.error('User provider cannot provide role status for this user');
}
}
/**
* The default status. This is the status that will be used before the user has selected any status.
* @param {import("./UserAPI").Role} role
* @returns {Promise<Status>} the default operator status if no other has been set.
*/
async getDefaultStatusForRole(role) {
const provider = this.#userAPI.getProvider();
const defaultStatus = await provider.getDefaultStatusForRole(role);
/**
* @returns {Promise<Boolean>} true if the configured UserProvider can provide status for the currently logged in user, false otherwise.
* @see StatusUserProvider
*/
async canProvideStatusForCurrentUser() {
const provider = this.#userAPI.getProvider();
return defaultStatus;
if (provider.getStatusRoleForCurrentUser) {
const activeStatusRole = await this.#userAPI.getProvider().getStatusRoleForCurrentUser();
const canProvideStatus = await this.canProvideStatusForRole(activeStatusRole);
return canProvideStatus;
} else {
return false;
}
}
/**
* All possible status roles. A status role is a user role that can provide status. In some systems
* this may be all user roles, but there may be cases where some users are not are not polled
* for status if they do not have a real-time operational role.
*
* @returns {Promise<Array<import("./UserAPI").Role>>} the default operator status if no other has been set.
*/
getAllStatusRoles() {
const provider = this.#userAPI.getProvider();
if (provider.getAllStatusRoles) {
return provider.getAllStatusRoles();
} else {
this.#userAPI.error("User provider cannot provide all status roles");
}
/**
* Private internal function that cannot be made #private because it needs to be registered as a callback to the user provider
* @private
*/
listenToStatusEvents(provider) {
if (typeof provider.on === 'function') {
provider.on('statusChange', this.onProviderStatusChange);
provider.on('pollQuestionChange', this.onProviderPollQuestionChange);
}
}
/**
* The status role of the current user. A user may have multiple roles, but will only have one role
* that provides status at any time.
* @returns {Promise<import("./UserAPI").Role>} the role for which the current user can provide status.
*/
getStatusRoleForCurrentUser() {
const provider = this.#userAPI.getProvider();
/**
* @private
*/
onProviderStatusChange(newStatus) {
this.emit('statusChange', newStatus);
}
if (provider.getStatusRoleForCurrentUser) {
return provider.getStatusRoleForCurrentUser();
} else {
this.#userAPI.error("User provider cannot provide role status for this user");
}
}
/**
* @returns {Promise<Boolean>} true if the configured UserProvider can provide status for the currently logged in user, false otherwise.
* @see StatusUserProvider
*/
async canProvideStatusForCurrentUser() {
const provider = this.#userAPI.getProvider();
if (provider.getStatusRoleForCurrentUser) {
const activeStatusRole = await this.#userAPI.getProvider().getStatusRoleForCurrentUser();
const canProvideStatus = await this.canProvideStatusForRole(activeStatusRole);
return canProvideStatus;
} else {
return false;
}
}
/**
* Private internal function that cannot be made #private because it needs to be registered as a callback to the user provider
* @private
*/
listenToStatusEvents(provider) {
if (typeof provider.on === 'function') {
provider.on('statusChange', this.onProviderStatusChange);
provider.on('pollQuestionChange', this.onProviderPollQuestionChange);
}
}
/**
* @private
*/
onProviderStatusChange(newStatus) {
this.emit('statusChange', newStatus);
}
/**
* @private
*/
onProviderPollQuestionChange(pollQuestion) {
this.emit('pollQuestionChange', pollQuestion);
}
/**
* @private
*/
onProviderPollQuestionChange(pollQuestion) {
this.emit('pollQuestionChange', pollQuestion);
}
}
/**

View File

@@ -19,63 +19,63 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import UserProvider from "./UserProvider";
import UserProvider from './UserProvider';
export default class StatusUserProvider extends UserProvider {
/**
* @param {('statusChange'|'pollQuestionChange')} event the name of the event to listen to
* @param {Function} callback a function to invoke when this event occurs
*/
on(event, callback) {}
/**
* @param {('statusChange'|'pollQuestionChange')} event the name of the event to stop listen to
* @param {Function} callback the callback function used to register the listener
*/
off(event, callback) {}
/**
* @returns {import("./StatusAPI").PollQuestion} the current status poll question
*/
async getPollQuestion() {}
/**
* @param {import("./StatusAPI").PollQuestion} pollQuestion a new poll question to set
* @returns {Promise<Boolean>} true if operation was successful, otherwise false
*/
async setPollQuestion(pollQuestion) {}
/**
* @returns {Promise<Boolean>} true if the current user can set the poll question, otherwise false
*/
async canSetPollQuestion() {}
/**
* @returns {Promise<Array<import("./StatusAPI").Status>>} a list of the possible statuses that an operator can be in
*/
async getPossibleStatuses() {}
/**
* @param {import("./UserAPI").Role} role
* @returns {Promise<import("./StatusAPI").Status}
*/
async getStatusForRole(role) {}
/**
* @param {import("./UserAPI").Role} role
* @returns {Promise<import("./StatusAPI").Status}
*/
async getDefaultStatusForRole(role) {}
/**
* @param {import("./UserAPI").Role} role
* @param {*} status
* @returns {Promise<Boolean>} true if operation was successful, otherwise false.
*/
async setStatusForRole(role, status) {}
/**
* @param {import("./UserAPI").Role} role
* @returns {Promise<Boolean} true if the user provider can provide status for the given role
*/
async canProvideStatusForRole(role) {}
/**
* @returns {Promise<Array<import("./UserAPI").Role>>} a list of all available status roles, if user permissions allow it.
*/
async getAllStatusRoles() {}
/**
* @returns {Promise<import("./UserAPI").Role>} the active status role for the currently logged in user
*/
async getStatusRoleForCurrentUser() {}
/**
* @param {('statusChange'|'pollQuestionChange')} event the name of the event to listen to
* @param {Function} callback a function to invoke when this event occurs
*/
on(event, callback) {}
/**
* @param {('statusChange'|'pollQuestionChange')} event the name of the event to stop listen to
* @param {Function} callback the callback function used to register the listener
*/
off(event, callback) {}
/**
* @returns {import("./StatusAPI").PollQuestion} the current status poll question
*/
async getPollQuestion() {}
/**
* @param {import("./StatusAPI").PollQuestion} pollQuestion a new poll question to set
* @returns {Promise<Boolean>} true if operation was successful, otherwise false
*/
async setPollQuestion(pollQuestion) {}
/**
* @returns {Promise<Boolean>} true if the current user can set the poll question, otherwise false
*/
async canSetPollQuestion() {}
/**
* @returns {Promise<Array<import("./StatusAPI").Status>>} a list of the possible statuses that an operator can be in
*/
async getPossibleStatuses() {}
/**
* @param {import("./UserAPI").Role} role
* @returns {Promise<import("./StatusAPI").Status}
*/
async getStatusForRole(role) {}
/**
* @param {import("./UserAPI").Role} role
* @returns {Promise<import("./StatusAPI").Status}
*/
async getDefaultStatusForRole(role) {}
/**
* @param {import("./UserAPI").Role} role
* @param {*} status
* @returns {Promise<Boolean>} true if operation was successful, otherwise false.
*/
async setStatusForRole(role, status) {}
/**
* @param {import("./UserAPI").Role} role
* @returns {Promise<Boolean} true if the user provider can provide status for the given role
*/
async canProvideStatusForRole(role) {}
/**
* @returns {Promise<Array<import("./UserAPI").Role>>} a list of all available status roles, if user permissions allow it.
*/
async getAllStatusRoles() {}
/**
* @returns {Promise<import("./UserAPI").Role>} the active status role for the currently logged in user
*/
async getStatusRoleForCurrentUser() {}
}

View File

@@ -21,19 +21,19 @@
*****************************************************************************/
export default class User {
constructor(id, name) {
this.id = id;
this.name = name;
constructor(id, name) {
this.id = id;
this.name = name;
this.getId = this.getId.bind(this);
this.getName = this.getName.bind(this);
}
this.getId = this.getId.bind(this);
this.getName = this.getName.bind(this);
}
getId() {
return this.id;
}
getId() {
return this.id;
}
getName() {
return this.name;
}
getName() {
return this.name;
}
}

View File

@@ -21,128 +21,125 @@
*****************************************************************************/
import EventEmitter from 'EventEmitter';
import {
MULTIPLE_PROVIDER_ERROR,
NO_PROVIDER_ERROR
} from './constants';
import { MULTIPLE_PROVIDER_ERROR, NO_PROVIDER_ERROR } from './constants';
import StatusAPI from './StatusAPI';
import User from './User';
class UserAPI extends EventEmitter {
/**
* @param {OpenMCT} openmct
* @param {UserAPIConfiguration} config
*/
constructor(openmct, config) {
super();
/**
* @param {OpenMCT} openmct
* @param {UserAPIConfiguration} config
*/
constructor(openmct, config) {
super();
this._openmct = openmct;
this._provider = undefined;
this._openmct = openmct;
this._provider = undefined;
this.User = User;
this.status = new StatusAPI(this, openmct, config);
this.User = User;
this.status = new StatusAPI(this, openmct, config);
}
/**
* Set the user provider for the user API. This allows you
* to specifiy ONE user provider to be used with Open MCT.
* @method setProvider
* @memberof module:openmct.UserAPI#
* @param {module:openmct.UserAPI~UserProvider} provider the new
* user provider
*/
setProvider(provider) {
if (this.hasProvider()) {
this.error(MULTIPLE_PROVIDER_ERROR);
}
/**
* Set the user provider for the user API. This allows you
* to specifiy ONE user provider to be used with Open MCT.
* @method setProvider
* @memberof module:openmct.UserAPI#
* @param {module:openmct.UserAPI~UserProvider} provider the new
* user provider
*/
setProvider(provider) {
if (this.hasProvider()) {
this.error(MULTIPLE_PROVIDER_ERROR);
}
this._provider = provider;
this.emit('providerAdded', this._provider);
}
this._provider = provider;
this.emit('providerAdded', this._provider);
getProvider() {
return this._provider;
}
/**
* Return true if the user provider has been set.
*
* @memberof module:openmct.UserAPI#
* @returns {boolean} true if the user provider exists
*/
hasProvider() {
return this._provider !== undefined;
}
/**
* If a user provider is set, it will return a copy of a user object from
* the provider. If the user is not logged in, it will return undefined;
*
* @memberof module:openmct.UserAPI#
* @returns {Function|Promise} user provider 'getCurrentUser' method
* @throws Will throw an error if no user provider is set
*/
getCurrentUser() {
if (!this.hasProvider()) {
return Promise.resolve(undefined);
} else {
return this._provider.getCurrentUser();
}
}
/**
* If a user provider is set, it will return the user provider's
* 'isLoggedIn' method
*
* @memberof module:openmct.UserAPI#
* @returns {Function|Boolean} user provider 'isLoggedIn' method
* @throws Will throw an error if no user provider is set
*/
isLoggedIn() {
if (!this.hasProvider()) {
return false;
}
getProvider() {
return this._provider;
return this._provider.isLoggedIn();
}
/**
* If a user provider is set, it will return a call to it's
* 'hasRole' method
*
* @memberof module:openmct.UserAPI#
* @returns {Function|Boolean} user provider 'isLoggedIn' method
* @param {string} roleId id of role to check for
* @throws Will throw an error if no user provider is set
*/
hasRole(roleId) {
this.noProviderCheck();
return this._provider.hasRole(roleId);
}
/**
* Checks if a provider is set and if not, will throw error
*
* @private
* @throws Will throw an error if no user provider is set
*/
noProviderCheck() {
if (!this.hasProvider()) {
this.error(NO_PROVIDER_ERROR);
}
}
/**
* Return true if the user provider has been set.
*
* @memberof module:openmct.UserAPI#
* @returns {boolean} true if the user provider exists
*/
hasProvider() {
return this._provider !== undefined;
}
/**
* If a user provider is set, it will return a copy of a user object from
* the provider. If the user is not logged in, it will return undefined;
*
* @memberof module:openmct.UserAPI#
* @returns {Function|Promise} user provider 'getCurrentUser' method
* @throws Will throw an error if no user provider is set
*/
getCurrentUser() {
if (!this.hasProvider()) {
return Promise.resolve(undefined);
} else {
return this._provider.getCurrentUser();
}
}
/**
* If a user provider is set, it will return the user provider's
* 'isLoggedIn' method
*
* @memberof module:openmct.UserAPI#
* @returns {Function|Boolean} user provider 'isLoggedIn' method
* @throws Will throw an error if no user provider is set
*/
isLoggedIn() {
if (!this.hasProvider()) {
return false;
}
return this._provider.isLoggedIn();
}
/**
* If a user provider is set, it will return a call to it's
* 'hasRole' method
*
* @memberof module:openmct.UserAPI#
* @returns {Function|Boolean} user provider 'isLoggedIn' method
* @param {string} roleId id of role to check for
* @throws Will throw an error if no user provider is set
*/
hasRole(roleId) {
this.noProviderCheck();
return this._provider.hasRole(roleId);
}
/**
* Checks if a provider is set and if not, will throw error
*
* @private
* @throws Will throw an error if no user provider is set
*/
noProviderCheck() {
if (!this.hasProvider()) {
this.error(NO_PROVIDER_ERROR);
}
}
/**
* Utility function for throwing errors
*
* @private
* @param {string} error description of error
* @throws Will throw error passed in
*/
error(error) {
throw new Error(error);
}
/**
* Utility function for throwing errors
*
* @private
* @param {string} error description of error
* @throws Will throw error passed in
*/
error(error) {
throw new Error(error);
}
}
export default UserAPI;

View File

@@ -20,95 +20,93 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import {
createOpenMct,
resetApplicationState
} from '../../utils/testing';
import {
MULTIPLE_PROVIDER_ERROR
} from './constants';
import { createOpenMct, resetApplicationState } from '../../utils/testing';
import { MULTIPLE_PROVIDER_ERROR } from './constants';
import ExampleUserProvider from '../../../example/exampleUser/ExampleUserProvider';
const USERNAME = 'Test User';
const EXAMPLE_ROLE = 'example-role';
describe("The User API", () => {
let openmct;
describe('The User API', () => {
let openmct;
beforeEach(() => {
openmct = createOpenMct();
});
afterEach(() => {
const activeOverlays = openmct.overlays.activeOverlays;
activeOverlays.forEach((overlay) => overlay.dismiss());
return resetApplicationState(openmct);
});
describe('with regard to user providers', () => {
it('allows you to specify a user provider', () => {
openmct.user.on('providerAdded', (provider) => {
expect(provider).toBeInstanceOf(ExampleUserProvider);
});
openmct.user.setProvider(new ExampleUserProvider(openmct));
});
it('prevents more than one user provider from being set', () => {
openmct.user.setProvider(new ExampleUserProvider(openmct));
expect(() => {
openmct.user.setProvider({});
}).toThrow(new Error(MULTIPLE_PROVIDER_ERROR));
});
it('provides a check for an existing user provider', () => {
expect(openmct.user.hasProvider()).toBeFalse();
openmct.user.setProvider(new ExampleUserProvider(openmct));
expect(openmct.user.hasProvider()).toBeTrue();
});
});
describe('provides the ability', () => {
let provider;
beforeEach(() => {
openmct = createOpenMct();
provider = new ExampleUserProvider(openmct);
provider.autoLogin(USERNAME);
});
afterEach(() => {
const activeOverlays = openmct.overlays.activeOverlays;
activeOverlays.forEach(overlay => overlay.dismiss());
it('to check if a user (not specific) is loged in', (done) => {
expect(openmct.user.isLoggedIn()).toBeFalse();
return resetApplicationState(openmct);
openmct.user.on('providerAdded', () => {
expect(openmct.user.isLoggedIn()).toBeTrue();
done();
});
// this will trigger the user indicator plugin,
// which will in turn login the user
openmct.user.setProvider(provider);
});
describe('with regard to user providers', () => {
it('allows you to specify a user provider', () => {
openmct.user.on('providerAdded', (provider) => {
expect(provider).toBeInstanceOf(ExampleUserProvider);
});
openmct.user.setProvider(new ExampleUserProvider(openmct));
});
it('prevents more than one user provider from being set', () => {
openmct.user.setProvider(new ExampleUserProvider(openmct));
expect(() => {
openmct.user.setProvider({});
}).toThrow(new Error(MULTIPLE_PROVIDER_ERROR));
});
it('provides a check for an existing user provider', () => {
expect(openmct.user.hasProvider()).toBeFalse();
openmct.user.setProvider(new ExampleUserProvider(openmct));
expect(openmct.user.hasProvider()).toBeTrue();
});
it('to get the current user', (done) => {
openmct.user.setProvider(provider);
openmct.user
.getCurrentUser()
.then((apiUser) => {
expect(apiUser.name).toEqual(USERNAME);
})
.finally(done);
});
describe('provides the ability', () => {
let provider;
it('to check if a user has a specific role (by id)', (done) => {
openmct.user.setProvider(provider);
let junkIdCheckPromise = openmct.user.hasRole('junk-id').then((hasRole) => {
expect(hasRole).toBeFalse();
});
let realIdCheckPromise = openmct.user.hasRole(EXAMPLE_ROLE).then((hasRole) => {
expect(hasRole).toBeTrue();
});
beforeEach(() => {
provider = new ExampleUserProvider(openmct);
provider.autoLogin(USERNAME);
});
it('to check if a user (not specific) is loged in', (done) => {
expect(openmct.user.isLoggedIn()).toBeFalse();
openmct.user.on('providerAdded', () => {
expect(openmct.user.isLoggedIn()).toBeTrue();
done();
});
// this will trigger the user indicator plugin,
// which will in turn login the user
openmct.user.setProvider(provider);
});
it('to get the current user', (done) => {
openmct.user.setProvider(provider);
openmct.user.getCurrentUser().then((apiUser) => {
expect(apiUser.name).toEqual(USERNAME);
}).finally(done);
});
it('to check if a user has a specific role (by id)', (done) => {
openmct.user.setProvider(provider);
let junkIdCheckPromise = openmct.user.hasRole('junk-id').then((hasRole) => {
expect(hasRole).toBeFalse();
});
let realIdCheckPromise = openmct.user.hasRole(EXAMPLE_ROLE).then((hasRole) => {
expect(hasRole).toBeTrue();
});
Promise.all([junkIdCheckPromise, realIdCheckPromise]).finally(done);
});
Promise.all([junkIdCheckPromise, realIdCheckPromise]).finally(done);
});
});
});

Some files were not shown because too many files have changed in this diff Show More