Compare commits

..

57 Commits

Author SHA1 Message Date
Jamie Vigliotta
6b719259e3 WIP: shelving for now, but may be useful in future 2021-01-04 11:59:08 -08:00
Jamie Vigliotta
9fc31809f6 Merge branch 'search-index-type' into tree-search-item
Merg'n search index type to use in tree
2020-12-23 13:19:33 -08:00
Jamie Vigliotta
ccd4bbd279 Merge branch 'master' into search-index-type
Merg'n master
2020-12-23 13:18:50 -08:00
Jamie V
351848ad56 only edit name in browse bar if obj creatable (#3616) 2020-12-22 11:31:30 -08:00
Jamie V
cbac495f93 [LocalTimeSystem] Plugin tests (#3478) 2020-12-18 10:38:02 -08:00
Nikhil
15ef5b7623 [Testing] [Notebook] fix failing notebook tests from three-dot-menu-proto (#3577) 2020-12-18 10:26:20 -08:00
Jamie V
46c7ac944f [Testing] Imagery Plugin Tests (#3606)
* imagery tests initial

* imagery plugin tests

* removing unused code

* added in key events, as well as a utility function for key events

* PR updates
2020-12-18 09:55:51 -08:00
Nikhil
aa4bfab462 Problem copying unitless telemetry values to notebook #3574 (#3589)
Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
2020-12-18 09:49:08 -08:00
Charles Hacskaylo
f5cbb37e5a Fix Inspector-based font size and style controls and menus (#3497)
- Moved CSS rule that was pushing the font style control to the right
side of the Inspector to `l-shell__toolbar` rule definition;
- Fixed `menus-to-left` CSS rule and applied to font size and style
menu controls;
- Added a new `menus-no-icon` style for menus that don't have icons,
applied to font size and style menu controls;
2020-12-17 18:43:58 -08:00
Jamie V
8d9079984a [Verve Imagery] Missing JS imagery class (#3603)
* replacing class that was accidentally deleted

* moved to correct div... aye aye ayeee
2020-12-14 15:42:45 -08:00
Jamie Vigliotta
72848849dd WIP working with imagery for testing 2020-12-14 14:14:47 -08:00
Andrew Henry
41783d8939 Fixed minor issues in Code Guidelines (#3596)
There was a missing semi-colon in a code example (oops!) and incorrect capitalization of the `const` keyword (overzealous word processor).
2020-12-11 15:13:51 -08:00
Shefali Joshi
441ad58fe7 Prepare for sprint 1.5.0 (#3594) 2020-12-11 14:12:52 -08:00
Jamie Vigliotta
66130ba542 Merge branch 'master' into telemetry-collection
Merg'n master
2020-12-11 10:49:12 -08:00
Nikhil
06a6a3f773 [Notebook] Link to snapshot should not be a fully qualified url #3445 (#3576)
Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2020-12-11 09:41:35 -08:00
Nikhil
52fab78625 [Testing] Fixes console errors while running npm test (#3593)
* ERROR: Error: [$injector:unpr] Unknown provider: exportImageServiceProvider <- exportImageService
* [Vue warn]: Injection "stylesManager" not found
* [Vue warn]: Error in mounted hook: "TypeError: identifier is undefined"
2020-12-10 19:44:51 -08:00
Jamie V
5eb6c15959 [Duplicate Action] Fix Display Layout unwanted duplication issue (#3591)
* WIP: refactoring legacy dulicate action

* WIP: debugging duplicate duplicates...

* WIP: fixed duplicate duplicates issue

* added unit tests

* removing old legacy copyaction and renaming duplicate action

* removing fdescribe

* trying to see if a done callback fixes testing issues

* fixed tests

* testing autoflow tests on server

* tweaked autoflow tests to stop failing

* minor updates for new 3 dot menu

* WIP bug fixing

* WIP debugging

* WIP more debuggin

* WIP using parent namespace for all duped objs

* WIP

* WIP
;
;

* cleaning up debugging code

* fixed linting issues

* fixed layout configuration items issue

Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
2020-12-10 09:09:25 -08:00
Jamie Vigliotta
895bdc164f WIP 2020-12-09 11:57:11 -08:00
Deep Tailor
ce8c31cfa4 [Stacked plots] fixes scroll issue and removed redundant calls to backend (#3585)
* fixes scroll issue and removed redundant calls to backend

* change scroll to auto
2020-12-08 10:10:50 -08:00
Jamie Vigliotta
c9728144a5 Merge branch 'master' into telemetry-collection
Merg'n master
2020-12-07 12:12:21 -08:00
Jamie V
d80c0eef8e [Actions] Duplicate Action bug fixes (#3578)
* WIP: refactoring legacy dulicate action

* WIP: debugging duplicate duplicates...

* WIP: fixed duplicate duplicates issue

* added unit tests

* removing old legacy copyaction and renaming duplicate action

* removing fdescribe

* trying to see if a done callback fixes testing issues

* fixed tests

* testing autoflow tests on server

* tweaked autoflow tests to stop failing

* minor updates for new 3 dot menu

* WIP bug fixing

* WIP debugging

* WIP more debuggin

* WIP using parent namespace for all duped objs

* WIP

* WIP
;
;

* cleaning up debugging code

* fixed linting issues

Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
2020-12-07 10:15:41 -08:00
Nikhil
55829dcf05 [Conductor] Remove 24 hour default limit in time conductor (#3512) 2020-12-04 10:34:19 -08:00
Deep Tailor
d78956327c Action API unit tests (#3527)
* add tests

* add tests for ActionCollection

* add tests for getVisibleActions and getStatusBarActions

Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2020-12-02 12:53:34 -08:00
Nikhil
4a0728a55b Navigating to a Notebook snapshot not working #3538 (#3569) 2020-12-02 12:40:35 -08:00
Deep Tailor
2e1d57aa8c only update selection if selectable has changed (#3573) 2020-12-02 12:00:30 -08:00
Nikhil
1c2b0678be [Notebook] snapshots on plots are empty #3566 (#3567) 2020-12-01 14:14:59 -08:00
Jamie Vigliotta
76fec7f3bc WIP 2020-12-01 12:04:14 -08:00
Deep Tailor
6f810add43 replace dots with underscores in save as filenames (#3565) 2020-12-01 11:18:13 -08:00
Deep Tailor
12727adb16 fix export marked data as csv (#3563) 2020-12-01 09:44:17 -08:00
Jamie Vigliotta
1b4717065a WIP 2020-11-30 14:35:02 -08:00
Shefali Joshi
9da750c3bb Add object interceptor API to allow missing model and missing my-items handling (#3522)
* Extends Object API to allow adding interceptors

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2020-11-30 10:50:24 -08:00
Shefali Joshi
176226ddef Don't allow Move and Duplicate actions on non creatable objects (#3518)
* Prevent copy and move actions for non creatable objects

* Remove unneeded code

* Remove prototype typo

* Allow duplicating an object only if it is creatable

Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
2020-11-25 14:23:12 -08:00
Jamie V
acea18fa70 Move action (#3356)
* WIP: added new move action plugin, added to default plugins in mct.js

* WIP: removed old move action and references, added new root action, working, needs tess

* added tests for move action

* removing focused tests

* WIP

* using composition collection now, optimized some calls

* removed test for removed function

* minor spec change, format only

* updated for new action registration and 3 dot

* removing comments

Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2020-11-24 14:37:28 -08:00
Nikhil
d1656f8561 Notebook localstorage issue (#3545)
* Unable to edit Notebooks (Firefox) #3534
Unable to take a snapshot - snapshot dropdown not working #3533

* Navigating to a Notebook snapshot not working #3538

Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
2020-11-24 14:21:36 -08:00
Charles Hacskaylo
87751e882c Fixed problem preventing alphanumerics from being font styled (#3550)
- Applied missing `u-style-receiver` to markup;
2020-11-24 11:26:20 -08:00
Jamie Vigliotta
e24542c1a6 Merge branch 'master' into telemetry-collection
Merg'n master
2020-11-24 11:22:04 -08:00
Jamie Vigliotta
b08f3106ed WIP 2020-11-24 10:34:08 -08:00
Jamie V
dff393a714 [Actions] New Duplicate Action (#3410)
* WIP: refactoring legacy dulicate action

* WIP: debugging duplicate duplicates...

* WIP: fixed duplicate duplicates issue

* added unit tests

* removing old legacy copyaction and renaming duplicate action

* removing fdescribe

* trying to see if a done callback fixes testing issues

* fixed tests

* testing autoflow tests on server

* tweaked autoflow tests to stop failing

* minor updates for new 3 dot menu

Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
2020-11-23 12:55:27 -08:00
Charles Hacskaylo
fd9c9aee03 Mod classes to fix default Notebook indicator (#3541)
- Simplified `is-status--*` mixins;
- Cleaned up CSS;
- Removed unused grid-item.scss file;
- Added specific styling for default Notebook grid item;

Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
2020-11-23 12:46:56 -08:00
Charles Hacskaylo
59bf981fb0 Sanding and polishing CSS related to 3 Dot Menu (#3542)
- Increased opacity of `c-icon-button` labels;
- Fixed CSS selector targeting no-icon menu items;
- Hide overflow in `c-so-view` header element to prevent icons extending
 outside very small layout frames;
2020-11-23 12:41:04 -08:00
Deep Tailor
4bbdac759f check if domainobejct before listening for status (#3539) 2020-11-20 15:27:57 -08:00
Nikhil
13fe7509de [Notebook] can not add snapshots to default notebook #3530 (#3531) 2020-11-20 13:40:59 -08:00
Nikhil
6fd8f6cd43 [VISTA] custom format tokens (#3469)
* [VISTA] custom format tokens #3468

Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
2020-11-20 12:17:18 -08:00
Jamie Vigliotta
700bc7616d WIP 2020-11-20 11:13:41 -08:00
Charles Hacskaylo
6375ecda34 Three Dot Menu Prototype (#3325)
* Three dot menu implementation

Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
Co-authored-by: Nikhil <nikhil.k.mandlik@nasa.gov>
2020-11-19 09:53:06 -08:00
Nikhil
d232dacc65 [Notebook] new entries on brand new notebook not rendered (#3496)
* [Notebook] new entries on brand new notebook not rendered #3488

* Refactored code to 'mutateObject'  from one place only, add page to newly created section immediately,  update entries copy then call mutate to update it inside domainObject.

Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2020-11-19 08:38:14 -08:00
Shefali Joshi
59946e89ef Unset the displayRange when updating yAxis (#3504)
Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
2020-11-16 11:39:20 -08:00
Shefali Joshi
d75c4b4049 Render grouped activities correctly and redraw without browser refresh when a new file is uploaded. (#3516)
* Ensure that overlap checking only looks at activities within it's own group. This is done by assuming that any rows less than a group's starting row belong to another group.
Observe for changes to a plan and update the plan view accordingly.

* Address review comments

Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
2020-11-16 06:20:40 -08:00
Jamie V
3436e976cf Merge branch 'master' into search-index-type 2020-11-13 17:03:45 -08:00
Charles Hacskaylo
30ca4b707d Fix Imagery-related issue for VERVE #301 (#3520)
- Add 'display: flex' where needed to get main image to display in
preview overlay;
2020-11-13 16:09:01 -08:00
Jamie V
27704c9a48 [Testing] Test UTC Time System at plugin level (#3517) 2020-11-13 14:25:58 -08:00
Jamie V
b8e232831e Merge branch 'master' into search-index-type 2020-10-15 09:33:58 -07:00
Jamie Vigliotta
f6bc49fc82 using type from model, instead of passing in separately 2020-10-15 09:33:09 -07:00
Jamie V
7018c217c4 Merge branch 'master' into search-index-type 2020-10-02 15:46:53 -07:00
Jamie Vigliotta
18c230c0f7 reverting 2020-10-02 15:27:22 -07:00
Jamie Vigliotta
b8c2f3f49a Reverting some files 2020-10-02 15:22:19 -07:00
Jamie Vigliotta
e5e27ea498 WIP 2020-10-02 10:26:29 -07:00
103 changed files with 3936 additions and 2356 deletions

View File

@@ -182,7 +182,7 @@ The following guidelines are provided for anyone contributing source code to the
1. Avoid the use of "magic" values.
eg.
```JavaScript
Const UNAUTHORIZED = 401
const UNAUTHORIZED = 401;
if (responseCode === UNAUTHORIZED)
```
is preferable to

View File

@@ -131,10 +131,10 @@
}
],
// maximum recent bounds to retain in conductor history
records: 10,
records: 10
// maximum duration between start and end bounds
// for utc-based time systems this is in milliseconds
limit: ONE_DAY
// limit: ONE_DAY
},
{
name: "Realtime",

View File

@@ -1,6 +1,6 @@
{
"name": "openmct",
"version": "1.4.1-SNAPSHOT",
"version": "1.5.0-SNAPSHOT",
"description": "The Open MCT core platform",
"dependencies": {},
"devDependencies": {

View File

@@ -21,32 +21,24 @@
*****************************************************************************/
define([
"./src/actions/MoveAction",
"./src/actions/CopyAction",
"./src/actions/LinkAction",
"./src/actions/SetPrimaryLocationAction",
"./src/services/LocatingCreationDecorator",
"./src/services/LocatingObjectDecorator",
"./src/policies/CopyPolicy",
"./src/policies/CrossSpacePolicy",
"./src/policies/MovePolicy",
"./src/capabilities/LocationCapability",
"./src/services/MoveService",
"./src/services/LinkService",
"./src/services/CopyService",
"./src/services/LocationService"
], function (
MoveAction,
CopyAction,
LinkAction,
SetPrimaryLocationAction,
LocatingCreationDecorator,
LocatingObjectDecorator,
CopyPolicy,
CrossSpacePolicy,
MovePolicy,
LocationCapability,
MoveService,
LinkService,
CopyService,
LocationService
@@ -60,39 +52,6 @@ define([
"configuration": {},
"extensions": {
"actions": [
{
"key": "move",
"name": "Move",
"description": "Move object to another location.",
"cssClass": "icon-move",
"category": "contextual",
"group": "action",
"priority": 9,
"implementation": MoveAction,
"depends": [
"policyService",
"locationService",
"moveService"
]
},
{
"key": "copy",
"name": "Duplicate",
"description": "Duplicate object to another location.",
"cssClass": "icon-duplicate",
"category": "contextual",
"group": "action",
"priority": 8,
"implementation": CopyAction,
"depends": [
"$log",
"policyService",
"locationService",
"copyService",
"dialogService",
"notificationService"
]
},
{
"key": "link",
"name": "Create Link",
@@ -141,10 +100,6 @@ define([
{
"category": "action",
"implementation": CopyPolicy
},
{
"category": "action",
"implementation": MovePolicy
}
],
"capabilities": [
@@ -160,17 +115,6 @@ define([
}
],
"services": [
{
"key": "moveService",
"name": "Move Service",
"description": "Provides a service for moving objects",
"implementation": MoveService,
"depends": [
"openmct",
"linkService",
"$q"
]
},
{
"key": "linkService",
"name": "Link Service",

View File

@@ -1,168 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
['./AbstractComposeAction', './CancelError'],
function (AbstractComposeAction, CancelError) {
/**
* The CopyAction is available from context menus and allows a user to
* deep copy an object to another location of their choosing.
*
* @implements {Action}
* @constructor
* @memberof platform/entanglement
*/
function CopyAction(
$log,
policyService,
locationService,
copyService,
dialogService,
notificationService,
context
) {
this.dialog = undefined;
this.notification = undefined;
this.dialogService = dialogService;
this.notificationService = notificationService;
this.$log = $log;
//Extend the behaviour of the Abstract Compose Action
AbstractComposeAction.call(
this,
policyService,
locationService,
copyService,
context,
"Duplicate",
"To a Location"
);
}
CopyAction.prototype = Object.create(AbstractComposeAction.prototype);
/**
* Updates user about progress of copy. Should not be invoked by
* client code under any circumstances.
*
* @private
* @param phase
* @param totalObjects
* @param processed
*/
CopyAction.prototype.progress = function (phase, totalObjects, processed) {
/*
Copy has two distinct phases. In the first phase a copy plan is
made in memory. During this phase of execution, the user is
shown a blocking 'modal' dialog.
In the second phase, the copying is taking place, and the user
is shown non-invasive banner notifications at the bottom of the screen.
*/
if (phase.toLowerCase() === 'preparing' && !this.dialog) {
this.dialog = this.dialogService.showBlockingMessage({
title: "Preparing to copy objects",
hint: "Do not navigate away from this page or close this browser tab while this message is displayed.",
unknownProgress: true,
severity: "info"
});
} else if (phase.toLowerCase() === "copying") {
if (this.dialog) {
this.dialog.dismiss();
}
if (!this.notification) {
this.notification = this.notificationService
.notify({
title: "Copying objects",
unknownProgress: false,
severity: "info"
});
}
this.notification.model.progress = (processed / totalObjects) * 100;
this.notification.model.title = ["Copied ", processed, "of ",
totalObjects, "objects"].join(" ");
}
};
/**
* Executes the CopyAction. The CopyAction uses the default behaviour of
* the AbstractComposeAction, but extends it to support notification
* updates of progress on copy.
*/
CopyAction.prototype.perform = function () {
var self = this;
function success(domainObject) {
var domainObjectName = domainObject.model.name;
self.notification.dismiss();
self.notificationService.info(domainObjectName + " copied successfully.");
}
function error(errorDetails) {
// No need to notify user of their own cancellation
if (errorDetails instanceof CancelError) {
return;
}
var errorDialog,
errorMessage = {
title: "Error copying objects.",
severity: "error",
hint: errorDetails.message,
minimized: true, // want the notification to be minimized initially (don't show banner)
options: [{
label: "OK",
callback: function () {
errorDialog.dismiss();
}
}]
};
self.dialog.dismiss();
if (self.notification) {
self.notification.dismiss(); // Clear the progress notification
}
self.$log.error("Error copying objects. ", errorDetails);
//Show a minimized notification of error for posterity
self.notificationService.notify(errorMessage);
//Display a blocking message
errorDialog = self.dialogService.showBlockingMessage(errorMessage);
}
function notification(details) {
self.progress(details.phase, details.totalObjects, details.processed);
}
return AbstractComposeAction.prototype.perform.call(this)
.then(success, error, notification);
};
CopyAction.appliesTo = AbstractComposeAction.appliesTo;
return CopyAction;
}
);

View File

@@ -1,104 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
function () {
/**
* MoveService provides an interface for moving objects from one
* location to another. It also provides a method for determining if
* an object can be copied to a specific location.
* @constructor
* @memberof platform/entanglement
* @implements {platform/entanglement.AbstractComposeService}
*/
function MoveService(openmct, linkService) {
this.openmct = openmct;
this.linkService = linkService;
}
MoveService.prototype.validate = function (object, parentCandidate) {
var currentParent = object
.getCapability('context')
.getParent();
if (!parentCandidate || !parentCandidate.getId) {
return false;
}
if (parentCandidate.getId() === currentParent.getId()) {
return false;
}
if (parentCandidate.getId() === object.getId()) {
return false;
}
if (parentCandidate.getModel().composition.indexOf(object.getId()) !== -1) {
return false;
}
return this.openmct.composition.checkPolicy(
parentCandidate.useCapability('adapter'),
object.useCapability('adapter')
);
};
MoveService.prototype.perform = function (object, parentObject) {
function relocate(objectInNewContext) {
var newLocationCapability = objectInNewContext
.getCapability('location'),
oldLocationCapability = object
.getCapability('location');
if (!newLocationCapability
|| !oldLocationCapability) {
return;
}
if (oldLocationCapability.isOriginal()) {
return newLocationCapability.setPrimaryLocation(
newLocationCapability
.getContextualLocation()
);
}
}
if (!this.validate(object, parentObject)) {
throw new Error(
"Tried to move objects without validating first."
);
}
return this.linkService
.perform(object, parentObject)
.then(relocate)
.then(function () {
return object
.getCapability('action')
.perform('remove', true);
});
};
return MoveService;
}
);

View File

@@ -1,243 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
[
'../../src/actions/CopyAction',
'../services/MockCopyService',
'../DomainObjectFactory'
],
function (CopyAction, MockCopyService, domainObjectFactory) {
describe("Copy Action", function () {
var copyAction,
policyService,
locationService,
locationServicePromise,
copyService,
context,
selectedObject,
selectedObjectContextCapability,
currentParent,
newParent,
notificationService,
notification,
dialogService,
mockDialog,
mockLog,
abstractComposePromise,
domainObject = {model: {name: "mockObject"}},
progress = {
phase: "copying",
totalObjects: 10,
processed: 1
};
beforeEach(function () {
policyService = jasmine.createSpyObj(
'policyService',
['allow']
);
policyService.allow.and.returnValue(true);
selectedObjectContextCapability = jasmine.createSpyObj(
'selectedObjectContextCapability',
[
'getParent'
]
);
selectedObject = domainObjectFactory({
name: 'selectedObject',
model: {
name: 'selectedObject'
},
capabilities: {
context: selectedObjectContextCapability
}
});
currentParent = domainObjectFactory({
name: 'currentParent'
});
selectedObjectContextCapability
.getParent
.and.returnValue(currentParent);
newParent = domainObjectFactory({
name: 'newParent'
});
locationService = jasmine.createSpyObj(
'locationService',
[
'getLocationFromUser'
]
);
locationServicePromise = jasmine.createSpyObj(
'locationServicePromise',
[
'then'
]
);
abstractComposePromise = jasmine.createSpyObj(
'abstractComposePromise',
[
'then'
]
);
abstractComposePromise.then.and.callFake(function (success, error, notify) {
notify(progress);
success(domainObject);
});
locationServicePromise.then.and.callFake(function (callback) {
callback(newParent);
return abstractComposePromise;
});
locationService
.getLocationFromUser
.and.returnValue(locationServicePromise);
dialogService = jasmine.createSpyObj('dialogService',
['showBlockingMessage']
);
mockDialog = jasmine.createSpyObj("dialog", ["dismiss"]);
dialogService.showBlockingMessage.and.returnValue(mockDialog);
notification = jasmine.createSpyObj('notification',
['dismiss', 'model']
);
notificationService = jasmine.createSpyObj('notificationService',
['notify', 'info']
);
notificationService.notify.and.returnValue(notification);
mockLog = jasmine.createSpyObj('log', ['error']);
copyService = new MockCopyService();
});
describe("with context from context-action", function () {
beforeEach(function () {
context = {
domainObject: selectedObject
};
copyAction = new CopyAction(
mockLog,
policyService,
locationService,
copyService,
dialogService,
notificationService,
context
);
});
it("initializes happily", function () {
expect(copyAction).toBeDefined();
});
describe("when performed it", function () {
beforeEach(function () {
spyOn(copyAction, 'progress').and.callThrough();
copyAction.perform();
});
it("prompts for location", function () {
expect(locationService.getLocationFromUser)
.toHaveBeenCalledWith(
"Duplicate selectedObject To a Location",
"Duplicate To",
jasmine.any(Function),
currentParent
);
});
it("waits for location and handles cancellation by user", function () {
expect(locationServicePromise.then)
.toHaveBeenCalledWith(jasmine.any(Function), jasmine.any(Function));
});
it("copies object to selected location", function () {
locationServicePromise
.then
.calls.mostRecent()
.args[0](newParent);
expect(copyService.perform)
.toHaveBeenCalledWith(selectedObject, newParent);
});
it("notifies the user of progress", function () {
expect(notificationService.info).toHaveBeenCalled();
});
it("notifies the user with name of object copied", function () {
expect(notificationService.info)
.toHaveBeenCalledWith("mockObject copied successfully.");
});
});
});
describe("with context from drag-drop", function () {
beforeEach(function () {
context = {
selectedObject: selectedObject,
domainObject: newParent
};
copyAction = new CopyAction(
mockLog,
policyService,
locationService,
copyService,
dialogService,
notificationService,
context
);
});
it("initializes happily", function () {
expect(copyAction).toBeDefined();
});
it("performs copy immediately", function () {
copyAction.perform();
expect(copyService.perform)
.toHaveBeenCalledWith(selectedObject, newParent);
});
});
});
}
);

View File

@@ -1,178 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
[
'../../src/actions/MoveAction',
'../services/MockMoveService',
'../DomainObjectFactory'
],
function (MoveAction, MockMoveService, domainObjectFactory) {
describe("Move Action", function () {
var moveAction,
policyService,
locationService,
locationServicePromise,
moveService,
context,
selectedObject,
selectedObjectContextCapability,
currentParent,
newParent;
beforeEach(function () {
policyService = jasmine.createSpyObj(
'policyService',
['allow']
);
policyService.allow.and.returnValue(true);
selectedObjectContextCapability = jasmine.createSpyObj(
'selectedObjectContextCapability',
[
'getParent'
]
);
selectedObject = domainObjectFactory({
name: 'selectedObject',
model: {
name: 'selectedObject'
},
capabilities: {
context: selectedObjectContextCapability
}
});
currentParent = domainObjectFactory({
name: 'currentParent'
});
selectedObjectContextCapability
.getParent
.and.returnValue(currentParent);
newParent = domainObjectFactory({
name: 'newParent'
});
locationService = jasmine.createSpyObj(
'locationService',
[
'getLocationFromUser'
]
);
locationServicePromise = jasmine.createSpyObj(
'locationServicePromise',
[
'then'
]
);
locationService
.getLocationFromUser
.and.returnValue(locationServicePromise);
moveService = new MockMoveService();
});
describe("with context from context-action", function () {
beforeEach(function () {
context = {
domainObject: selectedObject
};
moveAction = new MoveAction(
policyService,
locationService,
moveService,
context
);
});
it("initializes happily", function () {
expect(moveAction).toBeDefined();
});
describe("when performed it", function () {
beforeEach(function () {
moveAction.perform();
});
it("prompts for location", function () {
expect(locationService.getLocationFromUser)
.toHaveBeenCalledWith(
"Move selectedObject To a New Location",
"Move To",
jasmine.any(Function),
currentParent
);
});
it("waits for location and handles cancellation by user", function () {
expect(locationServicePromise.then)
.toHaveBeenCalledWith(jasmine.any(Function), jasmine.any(Function));
});
it("moves object to selected location", function () {
locationServicePromise
.then
.calls.mostRecent()
.args[0](newParent);
expect(moveService.perform)
.toHaveBeenCalledWith(selectedObject, newParent);
});
});
});
describe("with context from drag-drop", function () {
beforeEach(function () {
context = {
selectedObject: selectedObject,
domainObject: newParent
};
moveAction = new MoveAction(
policyService,
locationService,
moveService,
context
);
});
it("initializes happily", function () {
expect(moveAction).toBeDefined();
});
it("performs move immediately", function () {
moveAction.perform();
expect(moveService.perform)
.toHaveBeenCalledWith(selectedObject, newParent);
});
});
});
}
);

View File

@@ -1,124 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'../../src/policies/MovePolicy',
'../DomainObjectFactory'
], function (MovePolicy, domainObjectFactory) {
describe("MovePolicy", function () {
var testMetadata,
testContext,
mockDomainObject,
mockParent,
mockParentType,
mockType,
mockAction,
policy;
beforeEach(function () {
var mockContextCapability =
jasmine.createSpyObj('context', ['getParent']);
mockType =
jasmine.createSpyObj('type', ['hasFeature']);
mockParentType =
jasmine.createSpyObj('parent-type', ['hasFeature']);
testMetadata = {};
mockDomainObject = domainObjectFactory({
capabilities: {
context: mockContextCapability,
type: mockType
}
});
mockParent = domainObjectFactory({
capabilities: {
type: mockParentType
}
});
mockContextCapability.getParent.and.returnValue(mockParent);
mockType.hasFeature.and.callFake(function (feature) {
return feature === 'creation';
});
mockParentType.hasFeature.and.callFake(function (feature) {
return feature === 'creation';
});
mockAction = jasmine.createSpyObj('action', ['getMetadata']);
mockAction.getMetadata.and.returnValue(testMetadata);
testContext = { domainObject: mockDomainObject };
policy = new MovePolicy();
});
describe("for move actions", function () {
beforeEach(function () {
testMetadata.key = 'move';
});
describe("when an object is non-modifiable", function () {
beforeEach(function () {
mockType.hasFeature.and.returnValue(false);
});
it("disallows the action", function () {
expect(policy.allow(mockAction, testContext)).toBe(false);
});
});
describe("when a parent is non-modifiable", function () {
beforeEach(function () {
mockParentType.hasFeature.and.returnValue(false);
});
it("disallows the action", function () {
expect(policy.allow(mockAction, testContext)).toBe(false);
});
});
describe("when an object and its parent are modifiable", function () {
it("allows the action", function () {
expect(policy.allow(mockAction, testContext)).toBe(true);
});
});
});
describe("for other actions", function () {
beforeEach(function () {
testMetadata.key = 'foo';
});
it("simply allows the action", function () {
expect(policy.allow(mockAction, testContext)).toBe(true);
mockType.hasFeature.and.returnValue(false);
expect(policy.allow(mockAction, testContext)).toBe(true);
mockParentType.hasFeature.and.returnValue(false);
expect(policy.allow(mockAction, testContext)).toBe(true);
});
});
});
});

View File

@@ -1,96 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
function () {
/**
* MockMoveService provides the same interface as the moveService,
* returning promises where it would normally do so. At it's core,
* it is a jasmine spy object, but it also tracks the promises it
* returns and provides shortcut methods for resolving those promises
* synchronously.
*
* Usage:
*
* ```javascript
* var moveService = new MockMoveService();
*
* // validate is a standard jasmine spy.
* moveService.validate.and.returnValue(true);
* var isValid = moveService.validate(object, parentCandidate);
* expect(isValid).toBe(true);
*
* // perform returns promises and tracks them.
* var whenCopied = jasmine.createSpy('whenCopied');
* moveService.perform(object, parentObject).then(whenCopied);
* expect(whenCopied).not.toHaveBeenCalled();
* moveService.perform.calls.mostRecent().resolve('someArg');
* expect(whenCopied).toHaveBeenCalledWith('someArg');
* ```
*/
function MockMoveService() {
// track most recent call of a function,
// perform automatically returns
var mockMoveService = jasmine.createSpyObj(
'MockMoveService',
[
'validate',
'perform'
]
);
mockMoveService.perform.and.callFake(() => {
var performPromise,
callExtensions,
spy;
performPromise = jasmine.createSpyObj(
'performPromise',
['then']
);
callExtensions = {
promise: performPromise,
resolve: function (resolveWith) {
performPromise.then.calls.all().forEach(function (call) {
call.args[0](resolveWith);
});
}
};
spy = mockMoveService.perform;
Object.keys(callExtensions).forEach(function (key) {
spy.calls.mostRecent()[key] = callExtensions[key];
spy.calls.all()[spy.calls.count() - 1][key] = callExtensions[key];
});
return performPromise;
});
return mockMoveService;
}
return MockMoveService;
}
);

View File

@@ -1,260 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
[
'../../src/services/MoveService',
'../services/MockLinkService',
'../DomainObjectFactory',
'../ControlledPromise'
],
function (
MoveService,
MockLinkService,
domainObjectFactory,
ControlledPromise
) {
xdescribe("MoveService", function () {
var moveService,
policyService,
object,
objectContextCapability,
currentParent,
parentCandidate,
linkService;
beforeEach(function () {
objectContextCapability = jasmine.createSpyObj(
'objectContextCapability',
[
'getParent'
]
);
object = domainObjectFactory({
name: 'object',
id: 'a',
capabilities: {
context: objectContextCapability,
type: { type: 'object' }
}
});
currentParent = domainObjectFactory({
name: 'currentParent',
id: 'b'
});
objectContextCapability.getParent.and.returnValue(currentParent);
parentCandidate = domainObjectFactory({
name: 'parentCandidate',
model: { composition: [] },
id: 'c',
capabilities: {
type: { type: 'parentCandidate' }
}
});
policyService = jasmine.createSpyObj(
'policyService',
['allow']
);
linkService = new MockLinkService();
policyService.allow.and.returnValue(true);
moveService = new MoveService(policyService, linkService);
});
describe("validate", function () {
var validate;
beforeEach(function () {
validate = function () {
return moveService.validate(object, parentCandidate);
};
});
it("does not allow an invalid parent", function () {
parentCandidate = undefined;
expect(validate()).toBe(false);
parentCandidate = {};
expect(validate()).toBe(false);
});
it("does not allow moving to current parent", function () {
parentCandidate.id = currentParent.id = 'xyz';
expect(validate()).toBe(false);
});
it("does not allow moving to self", function () {
object.id = parentCandidate.id = 'xyz';
expect(validate()).toBe(false);
});
it("does not allow moving to the same location", function () {
object.id = 'abc';
parentCandidate.model.composition = ['abc'];
expect(validate()).toBe(false);
});
describe("defers to policyService", function () {
it("calls policy service with correct args", function () {
validate();
expect(policyService.allow).toHaveBeenCalledWith(
"composition",
parentCandidate,
object
);
});
it("and returns false", function () {
policyService.allow.and.returnValue(false);
expect(validate()).toBe(false);
});
it("and returns true", function () {
policyService.allow.and.returnValue(true);
expect(validate()).toBe(true);
});
});
});
describe("perform", function () {
var actionCapability,
locationCapability,
locationPromise,
newParent,
moveResult;
beforeEach(function () {
newParent = parentCandidate;
actionCapability = jasmine.createSpyObj(
'actionCapability',
['perform']
);
locationCapability = jasmine.createSpyObj(
'locationCapability',
[
'isOriginal',
'setPrimaryLocation',
'getContextualLocation'
]
);
locationPromise = new ControlledPromise();
locationCapability.setPrimaryLocation
.and.returnValue(locationPromise);
object = domainObjectFactory({
name: 'object',
capabilities: {
action: actionCapability,
location: locationCapability,
context: objectContextCapability,
type: { type: 'object' }
}
});
moveResult = moveService.perform(object, newParent);
});
it("links object to newParent", function () {
expect(linkService.perform).toHaveBeenCalledWith(
object,
newParent
);
});
it("returns a promise", function () {
expect(moveResult.then).toEqual(jasmine.any(Function));
});
it("waits for result of link", function () {
expect(linkService.perform.calls.mostRecent().promise.then)
.toHaveBeenCalledWith(jasmine.any(Function));
});
it("throws an error when performed on invalid inputs", function () {
function perform() {
moveService.perform(object, newParent);
}
spyOn(moveService, "validate");
moveService.validate.and.returnValue(true);
expect(perform).not.toThrow();
moveService.validate.and.returnValue(false);
expect(perform).toThrow();
});
describe("when moving an original", function () {
beforeEach(function () {
locationCapability.getContextualLocation
.and.returnValue('new-location');
locationCapability.isOriginal.and.returnValue(true);
linkService.perform.calls.mostRecent().promise.resolve();
});
it("updates location", function () {
expect(locationCapability.setPrimaryLocation)
.toHaveBeenCalledWith('new-location');
});
describe("after location update", function () {
beforeEach(function () {
locationPromise.resolve();
});
it("removes object from parent without user warning dialog", function () {
expect(actionCapability.perform)
.toHaveBeenCalledWith('remove', true);
});
});
});
describe("when moving a link", function () {
beforeEach(function () {
locationCapability.isOriginal.and.returnValue(false);
linkService.perform.calls.mostRecent().promise.resolve();
});
it("does not update location", function () {
expect(locationCapability.setPrimaryLocation)
.not
.toHaveBeenCalled();
});
it("removes object from parent without user warning dialog", function () {
expect(actionCapability.perform)
.toHaveBeenCalledWith('remove', true);
});
});
});
});
}
);

View File

@@ -32,7 +32,8 @@
function indexItem(id, model) {
indexedItems.push({
id: id,
name: model.name.toLowerCase()
name: model.name.toLowerCase(),
type: model.type
});
}

View File

@@ -125,15 +125,17 @@ define([
* @param topic the topicService.
*/
GenericSearchProvider.prototype.indexOnMutation = function (topic) {
var mutationTopic = topic('mutation'),
provider = this;
let mutationTopic = topic('mutation');
mutationTopic.listen(function (mutatedObject) {
var editor = mutatedObject.getCapability('editor');
mutationTopic.listen(mutatedObject => {
let editor = mutatedObject.getCapability('editor');
if (!editor || !editor.inEditContext()) {
provider.index(
let mutatedObjectModel = mutatedObject.getModel();
this.index(
mutatedObject.getId(),
mutatedObject.getModel()
mutatedObjectModel,
mutatedObjectModel.type
);
}
});
@@ -178,14 +180,15 @@ define([
* @param id a model id
* @param model a model
*/
GenericSearchProvider.prototype.index = function (id, model) {
GenericSearchProvider.prototype.index = function (id, model, type) {
var provider = this;
if (id !== 'ROOT') {
this.worker.postMessage({
request: 'index',
model: model,
id: id
id: id,
type: type
});
}
@@ -223,7 +226,7 @@ define([
.then(function (objects) {
delete provider.pendingIndex[idToIndex];
if (objects[idToIndex]) {
provider.index(idToIndex, objects[idToIndex].model);
provider.index(idToIndex, objects[idToIndex].model, objects[idToIndex].model.type);
}
}, function () {
provider
@@ -262,6 +265,7 @@ define([
return {
id: hit.item.id,
model: hit.item.model,
type: hit.item.type,
score: hit.matchCount
};
});
@@ -273,7 +277,9 @@ define([
modelResults.hits = event.data.results.map(function (hit) {
return {
id: hit.id
id: hit.id,
name: hit.name,
type: hit.type
};
});
}

View File

@@ -41,7 +41,8 @@
indexedItems.push({
id: id,
vector: vector,
model: model
model: model,
type: model.type
});
}

View File

@@ -85,7 +85,8 @@ define([
SearchAggregator.prototype.query = function (
inputText,
maxResults,
filter
filter,
indexOnly
) {
var aggregator = this,
@@ -120,10 +121,24 @@ define([
modelResults = aggregator.applyFilter(modelResults, filter);
modelResults = aggregator.removeDuplicates(modelResults);
if (indexOnly) {
return Promise.resolve(modelResults);
}
return aggregator.asObjectResults(modelResults);
});
};
SearchAggregator.prototype.queryLite = function (
inputText,
maxResults,
filter
) {
const INDEX_ONLY = true;
return this.query(inputText, maxResults, filter, INDEX_ONLY);
};
/**
* Order model results by score descending and return them.
*/

View File

@@ -46,6 +46,8 @@ define([
'./api/Branding',
'./plugins/licenses/plugin',
'./plugins/remove/plugin',
'./plugins/move/plugin',
'./plugins/duplicate/plugin',
'vue'
], function (
EventEmitter,
@@ -73,6 +75,8 @@ define([
BrandingAPI,
LicensesPlugin,
RemoveActionPlugin,
MoveActionPlugin,
DuplicateActionPlugin,
Vue
) {
/**
@@ -263,6 +267,8 @@ define([
this.install(LegacyIndicatorsPlugin());
this.install(LicensesPlugin.default());
this.install(RemoveActionPlugin.default());
this.install(MoveActionPlugin.default());
this.install(DuplicateActionPlugin.default());
this.install(this.plugins.FolderView());
this.install(this.plugins.Tabs());
this.install(ImageryPlugin.default());
@@ -276,6 +282,7 @@ define([
this.install(this.plugins.NotificationIndicator());
this.install(this.plugins.NewFolderAction());
this.install(this.plugins.ViewDatumAction());
this.install(this.plugins.ObjectInterceptors());
}
MCT.prototype = Object.create(EventEmitter.prototype);

View File

@@ -24,13 +24,14 @@ import EventEmitter from 'EventEmitter';
import _ from 'lodash';
class ActionCollection extends EventEmitter {
constructor(applicableActions, objectPath, view, openmct) {
constructor(applicableActions, objectPath, view, openmct, skipEnvironmentObservers) {
super();
this.applicableActions = applicableActions;
this.openmct = openmct;
this.objectPath = objectPath;
this.view = view;
this.skipEnvironmentObservers = skipEnvironmentObservers;
this.objectUnsubscribes = [];
let debounceOptions = {
@@ -41,10 +42,12 @@ class ActionCollection extends EventEmitter {
this._updateActions = _.debounce(this._updateActions.bind(this), 150, debounceOptions);
this._update = _.debounce(this._update.bind(this), 150, debounceOptions);
this._observeObjectPath();
this._initializeActions();
if (!skipEnvironmentObservers) {
this._observeObjectPath();
this.openmct.editor.on('isEditing', this._updateActions);
}
this.openmct.editor.on('isEditing', this._updateActions);
this._initializeActions();
}
disable(actionKeys) {
@@ -84,11 +87,15 @@ class ActionCollection extends EventEmitter {
}
destroy() {
this.objectUnsubscribes.forEach(unsubscribe => {
unsubscribe();
});
super.removeAllListeners();
this.openmct.editor.off('isEditing', this._updateActions);
if (!this.skipEnvironmentObservers) {
this.objectUnsubscribes.forEach(unsubscribe => {
unsubscribe();
});
this.openmct.editor.off('isEditing', this._updateActions);
}
this.emit('destroy', this.view);
}
@@ -123,6 +130,10 @@ class ActionCollection extends EventEmitter {
return statusBarActions;
}
getActionsObject() {
return this.applicableActions;
}
_update() {
this.emit('update', this.applicableActions);
}

View File

@@ -0,0 +1,225 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import ActionCollection from './ActionCollection';
import { createOpenMct, resetApplicationState } from '../../utils/testing';
describe('The ActionCollection', () => {
let openmct;
let actionCollection;
let mockApplicableActions;
let mockObjectPath;
let mockView;
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: ''
}
}
];
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';
}
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 false;
},
invoke: () => {
}
}
};
actionCollection = new ActionCollection(mockApplicableActions, mockObjectPath, mockView, openmct);
});
afterEach(() => {
actionCollection.destroy();
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();
});
});
describe("enable method invoked with action keys", () => {
it("marks the isDisabled property as false", () => {
let actionKey = 'test-action-object-path';
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("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.isHidden).toBeFalsy();
actionCollection.hide([actionKey]);
actionsObject = actionCollection.getActionsObject();
action = actionsObject[actionKey];
expect(action.isHidden).toBeTrue();
});
});
describe("show method invoked with action keys", () => {
it("marks the isHidden property as false", () => {
let actionKey = 'test-action-object-path';
actionCollection.hide([actionKey]);
let actionsObject = actionCollection.getActionsObject();
let action = actionsObject[actionKey];
expect(action.isHidden).toBeTrue();
actionCollection.show([actionKey]);
actionsObject = actionCollection.getActionsObject();
action = actionsObject[actionKey];
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);
});
});
});

View File

@@ -44,34 +44,12 @@ class ActionsAPI extends EventEmitter {
}
get(objectPath, view) {
let viewContext = view && view.getViewContext && view.getViewContext() || {};
if (view) {
if (view && !viewContext.skipCache) {
let cachedActionCollection = this._actionCollections.get(view);
if (cachedActionCollection) {
return cachedActionCollection;
} else {
let applicableActions = this._applicableActions(objectPath, view);
let actionCollection = new ActionCollection(applicableActions, objectPath, view, this._openmct);
this._actionCollections.set(view, actionCollection);
actionCollection.on('destroy', this._updateCachedActionCollections);
return actionCollection;
}
return this._getCachedActionCollection(objectPath, view) || this._newActionCollection(objectPath, view, true);
} else {
let applicableActions = this._applicableActions(objectPath, view);
Object.keys(applicableActions).forEach(key => {
let action = applicableActions[key];
action.callBack = () => {
return action.invoke(objectPath, view);
};
});
return applicableActions;
return this._newActionCollection(objectPath, view, true);
}
}
@@ -79,6 +57,27 @@ class ActionsAPI extends EventEmitter {
this._groupOrder = groupArray;
}
_get(objectPath, view) {
let actionCollection = this._newActionCollection(objectPath, view);
this._actionCollections.set(view, actionCollection);
actionCollection.on('destroy', this._updateCachedActionCollections);
return actionCollection;
}
_getCachedActionCollection(objectPath, view) {
let cachedActionCollection = this._actionCollections.get(view);
return cachedActionCollection;
}
_newActionCollection(objectPath, view, skipEnvironmentObservers) {
let applicableActions = this._applicableActions(objectPath, view);
return new ActionCollection(applicableActions, objectPath, view, this._openmct, skipEnvironmentObservers);
}
_updateCachedActionCollections(key) {
if (this._actionCollections.has(key)) {
let actionCollection = this._actionCollections.get(key);

View File

@@ -22,22 +22,41 @@
import ActionsAPI from './ActionsAPI';
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;
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',
key: 'test-action',
cssClass: 'test-action',
description: 'This is a test action',
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 = {}) => {
@@ -45,8 +64,6 @@ describe('The Actions API', () => {
let viewContext = view.getViewContext();
return viewContext.onlyAppliesToTestCase;
} else if (objectPath.length) {
return objectPath[0].type === 'fake-folder';
}
return false;
@@ -75,8 +92,7 @@ describe('The Actions API', () => {
mockViewContext1 = {
getViewContext: () => {
return {
onlyAppliesToTestCase: true,
skipCache: true
onlyAppliesToTestCase: true
};
}
};
@@ -90,7 +106,8 @@ describe('The Actions API', () => {
it("adds action to ActionsAPI", () => {
actionsAPI.register(mockAction);
let action = actionsAPI.get(mockObjectPath, mockViewContext1)[mockAction.key];
let actionCollection = actionsAPI.get(mockObjectPath, mockViewContext1);
let action = actionCollection.getActionsObject()[mockAction.key];
expect(action.key).toEqual(mockAction.key);
expect(action.name).toEqual(mockAction.name);
@@ -100,17 +117,34 @@ describe('The Actions API', () => {
describe("get method", () => {
beforeEach(() => {
actionsAPI.register(mockAction);
actionsAPI.register(mockObjectPathAction);
});
it("returns an object with relevant actions when invoked with objectPath only", () => {
let action = actionsAPI.get(mockObjectPath, mockViewContext1)[mockAction.key];
it("returns an ActionCollection when invoked with an objectPath only", () => {
let actionCollection = actionsAPI.get(mockObjectPath);
let instanceOfActionCollection = actionCollection instanceof ActionCollection;
expect(action.key).toEqual(mockAction.key);
expect(action.name).toEqual(mockAction.name);
expect(instanceOfActionCollection).toBeTrue();
});
it("returns an object with relevant actions when invoked with viewContext and skipCache", () => {
let action = actionsAPI.get(mockObjectPath, mockViewContext1)[mockAction.key];
it("returns an ActionCollection when invoked with an objectPath and view", () => {
let actionCollection = actionsAPI.get(mockObjectPath, mockViewContext1);
let instanceOfActionCollection = actionCollection instanceof ActionCollection;
expect(instanceOfActionCollection).toBeTrue();
});
it("returns relevant actions when invoked with objectPath only", () => {
let actionCollection = actionsAPI.get(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.get(mockObjectPath, mockViewContext1);
let action = actionCollection.getActionsObject()[mockAction.key];
expect(action.key).toEqual(mockAction.key);
expect(action.name).toEqual(mockAction.name);

View File

@@ -0,0 +1,66 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
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) {
//TODO: sort by priority
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) {
return this.interceptors.filter(interceptor => {
return typeof interceptor.appliesTo === 'function'
&& interceptor.appliesTo(identifier, object);
});
}
}

View File

@@ -26,6 +26,7 @@ define([
'./MutableObject',
'./RootRegistry',
'./RootObjectProvider',
'./InterceptorRegistry',
'EventEmitter'
], function (
_,
@@ -33,6 +34,7 @@ define([
MutableObject,
RootRegistry,
RootObjectProvider,
InterceptorRegistry,
EventEmitter
) {
@@ -48,6 +50,7 @@ define([
this.rootRegistry = new RootRegistry();
this.rootProvider = new RootObjectProvider.default(this.rootRegistry);
this.cache = {};
this.interceptorRegistry = new InterceptorRegistry.default();
}
/**
@@ -177,6 +180,10 @@ define([
return objectPromise.then(result => {
delete this.cache[keystring];
const interceptors = this.listGetInterceptors(identifier, result);
interceptors.forEach(interceptor => {
result = interceptor.invoke(identifier, result);
});
return result;
});
@@ -312,6 +319,27 @@ define([
});
};
/**
* Register an object interceptor that transforms a domain object requested via module:openmct.ObjectAPI.get
* The domain object will be transformed after it is retrieved from the persistence store
* The domain object will be transformed only if the interceptor is applicable to that domain object as defined by the InterceptorDef
*
* @param {module:openmct.InterceptorDef} interceptorDef the interceptor definition to add
* @method addGetInterceptor
* @memberof module:openmct.InterceptorRegistry#
*/
ObjectAPI.prototype.addGetInterceptor = function (interceptorDef) {
this.interceptorRegistry.addInterceptor(interceptorDef);
};
/**
* Retrieve the interceptors for a given domain object.
* @private
*/
ObjectAPI.prototype.listGetInterceptors = function (identifier, object) {
return this.interceptorRegistry.getInterceptors(identifier, object);
};
/**
* Uniquely identifies a domain object.
*

View File

@@ -63,12 +63,51 @@ describe("The Object API", () => {
describe("The get function", () => {
describe("when a provider is available", () => {
let mockProvider;
let mockInterceptor;
let anotherMockInterceptor;
let notApplicableMockInterceptor;
beforeEach(() => {
mockProvider = jasmine.createSpyObj("mock provider", [
"get"
]);
mockProvider.get.and.returnValue(Promise.resolve(mockDomainObject));
mockInterceptor = jasmine.createSpyObj("mock interceptor", [
"appliesTo",
"invoke"
]);
mockInterceptor.appliesTo.and.returnValue(true);
mockInterceptor.invoke.and.callFake((identifier, object) => {
return Object.assign({
changed: true
}, object);
});
anotherMockInterceptor = jasmine.createSpyObj("another mock interceptor", [
"appliesTo",
"invoke"
]);
anotherMockInterceptor.appliesTo.and.returnValue(true);
anotherMockInterceptor.invoke.and.callFake((identifier, object) => {
return Object.assign({
alsoChanged: true
}, object);
});
notApplicableMockInterceptor = jasmine.createSpyObj("not applicable mock interceptor", [
"appliesTo",
"invoke"
]);
notApplicableMockInterceptor.appliesTo.and.returnValue(false);
notApplicableMockInterceptor.invoke.and.callFake((identifier, object) => {
return Object.assign({
shouldNotBeChanged: true
}, object);
});
objectAPI.addProvider(TEST_NAMESPACE, mockProvider);
objectAPI.addGetInterceptor(mockInterceptor);
objectAPI.addGetInterceptor(anotherMockInterceptor);
objectAPI.addGetInterceptor(notApplicableMockInterceptor);
});
it("Caches multiple requests for the same object", () => {
@@ -78,6 +117,15 @@ describe("The Object API", () => {
objectAPI.get(mockDomainObject.identifier);
expect(mockProvider.get.calls.count()).toBe(1);
});
it("applies any applicable interceptors", () => {
expect(mockDomainObject.changed).toBeUndefined();
objectAPI.get(mockDomainObject.identifier).then((object) => {
expect(object.changed).toBeTrue();
expect(object.alsoChanged).toBeTrue();
expect(object.shouldNotBeChanged).toBeUndefined();
});
});
});
});
});

View File

@@ -20,13 +20,17 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
const { TelemetryCollection } = require("./TelemetryCollection");
define([
'../../plugins/displayLayout/CustomStringFormatter',
'./TelemetryMetadataManager',
'./TelemetryValueFormatter',
'./DefaultMetadataProvider',
'objectUtils',
'lodash'
], function (
CustomStringFormatter,
TelemetryMetadataManager,
TelemetryValueFormatter,
DefaultMetadataProvider,
@@ -142,6 +146,17 @@ define([
this.valueFormatterCache = new WeakMap();
}
/**
* Return Custom String Formatter
*
* @param {Object} valueMetadata valueMetadata for given telemetry object
* @param {string} format custom formatter string (eg: %.4f, &lts etc.)
* @returns {CustomStringFormatter}
*/
TelemetryAPI.prototype.customStringFormatter = function (valueMetadata, format) {
return new CustomStringFormatter.default(this.openmct, valueMetadata, format);
};
/**
* Return true if the given domainObject is a telemetry object. A telemetry
* object is any object which has telemetry metadata-- regardless of whether
@@ -260,6 +275,48 @@ define([
}
};
/**
* Request telemetry collection for a domain object.
* The `options` argument allows you to specify filters
* (start, end, etc.), sort order, and strategies for retrieving
* telemetry (aggregation, latest available, etc.).
*
* @method request
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
* @param {module:openmct.DomainObject} domainObject the object
* which has associated telemetry
* @param {module:openmct.TelemetryAPI~TelemetryRequest} options
* options for this historical request
* @returns {Promise.<object[]>} a promise for an array of
* telemetry data
*/
TelemetryAPI.prototype.requestTelemetryCollection = function (domainObject) {
if (arguments.length === 1) {
arguments.length = 2;
arguments[1] = {};
}
// historical setup
this.standardizeRequestOptions(arguments[1]);
const historicalProvider = this.findRequestProvider(domainObject, arguments);
// subscription setup
const subscriptionProvider = this.findSubscriptionProvider(domainObject);
// check for no providers
if (!historicalProvider && !subscriptionProvider) {
return Promise.reject('No providers found');
}
let telemetryCollectionOptions = {
historicalProvider,
subscriptionProvider,
arguments: arguments
};
return Promise.resolve(new TelemetryCollection(this.openmct, domainObject, telemetryCollectionOptions));
};
/**
* Request historical telemetry for a domain object.
* The `options` argument allows you to specify filters
@@ -400,6 +457,17 @@ define([
return _.sortBy(options, sortKeys);
};
/**
* @private
*/
TelemetryAPI.prototype.getFormatService = function () {
if (!this.formatService) {
this.formatService = this.openmct.$injector.get('formatService');
}
return this.formatService;
};
/**
* Get a value formatter for a given valueMetadata.
*
@@ -407,19 +475,27 @@ define([
*/
TelemetryAPI.prototype.getValueFormatter = function (valueMetadata) {
if (!this.valueFormatterCache.has(valueMetadata)) {
if (!this.formatService) {
this.formatService = this.openmct.$injector.get('formatService');
}
this.valueFormatterCache.set(
valueMetadata,
new TelemetryValueFormatter(valueMetadata, this.formatService)
new TelemetryValueFormatter(valueMetadata, this.getFormatService())
);
}
return this.valueFormatterCache.get(valueMetadata);
};
/**
* Get a value formatter for a given key.
* @param {string} key
*
* @returns {Format}
*/
TelemetryAPI.prototype.getFormatter = function (key) {
const formatMap = this.getFormatService().formatMap;
return formatMap[key];
};
/**
* Get a format map of all value formatters for a given piece of telemetry
* metadata.

View File

@@ -0,0 +1,315 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
// import _ from 'lodash';
import TelemetrySubscriptionService from './TelemetrySubscriptionService';
function bindUs() {
return [
'trackHistoricalTelemetry',
'trackSubscriptionTelemetry',
'addPage',
'processNewTelemetry',
'hasMorePages',
'nextPage',
'bounds',
'timeSystem',
'on',
'off',
'emit',
'subscribeToBounds',
'unsubscribeFromBounds',
'subscribeToTimeSystem',
'unsubscribeFromTimeSystem',
'destroy'
];
}
export class TelemetryCollection {
constructor(openmct, domainObject, options) {
bindUs().forEach(method => this[method] = this[method].bind(this));
this.openmct = openmct;
this.domainObject = domainObject;
this.boundedTelemetry = [];
this.futureBuffer = [];
this.parseTime = undefined;
this.timeSystem(openmct.time.timeSystem());
this.lastBounds = openmct.time.bounds();
this.historicalProvider = options.historicalProvider;
this.subscriptionProvider = options.subscriptionProvider;
this.arguments = options.arguments;
this.listeners = {
add: [],
remove: []
};
this.trackHistoricalTelemetry();
this.trackSubscriptionTelemetry();
this.subscribeToBounds();
this.subscribeToTimeSystem();
}
// should we wait to track history until an 'add' listener is added?
async trackHistoricalTelemetry() {
if (!this.historicalProvider) {
return;
}
// remove for reset
if (this.boundedTelemetry.length !== 0) {
this.emit('remove', this.boundedTelemetry);
this.boundedTelemetry = [];
}
let historicalData = await this.historicalProvider.request.apply(this.domainObject, this.arguments).catch((rejected) => {
this.openmct.notifications.error('Error requesting telemetry data, see console for details');
console.error(rejected);
return Promise.reject(rejected);
});
// make sure it wasn't rejected
if (Array.isArray(historicalData)) {
// reset on requests, should only happen on initial load,
// bounds manually changed and time system changes
this.boundedTelemetry = historicalData;
this.emit('add', [...this.boundedTelemetry]);
}
}
trackSubscriptionTelemetry() {
if (!this.subscriptionProvider) {
return;
}
this.subscriptionService = new TelemetrySubscriptionService(this.openmct);
this.unsubscribe = this.subscriptionService.subscribe(
this.domainObject,
this.processNewTelemetry,
this.subscriptionProvider,
this.arguments
);
}
// utilized by telemetry provider to add more data
addPage(telemetryData) {
this.processNewTelemetry(telemetryData);
}
// used to sort any new telemetry (add/page, historical, subscription)
processNewTelemetry(telemetryData) {
let data = Array.isArray(telemetryData) ? telemetryData : [telemetryData];
let parsedValue;
let beforeStartOfBounds;
let afterEndOfBounds;
let added = [];
for (let datum of data) {
parsedValue = this.parseTime(datum);
beforeStartOfBounds = parsedValue < this.lastBounds.start;
afterEndOfBounds = parsedValue > this.lastBounds.end;
if (!afterEndOfBounds && !beforeStartOfBounds) {
if (!this.boundedTelemetry.includes(datum)) {
this.boundedTelemetry.push(datum);
added.push(datum);
}
} else if (afterEndOfBounds) {
this.futureBuffer.push(datum);
}
}
if (added.length) {
this.emit('add', added);
}
}
// returns a boolean if there is more telemetry within the time bounds
// if the provider supports it
hasMorePages() {
return this.historicalProvider
&& this.historicalProvider.supportsPaging()
&& this.historicalProvider.hasMorePages(this);
}
// will return the next "page" of telemetry if the provider supports it
nextPage() {
if (!this.historicalProvider || !this.historicalProvider.supportsPaging()) {
throw new Error('Provider does not support paging');
}
this.historicalProvider.nextPage(this.arguments, this);
}
// when user changes bounds, or when bounds increment from a tick
bounds(bounds, isTick) {
this.lastBounds = bounds;
if (isTick) {
// need to check futureBuffer and need to check
// if anything has fallen out of bounds
} else {
// TODO: also reset right?
// need to reset and request history again
// no need to mess with subscription
}
let startChanged = this.lastBounds.start !== bounds.start;
let endChanged = this.lastBounds.end !== bounds.end;
let startIndex = 0;
let endIndex = 0;
let discarded = [];
let added = [];
let testValue = {
datum: {}
};
this.lastBounds = bounds;
if (startChanged) {
testValue.datum[this.sortOptions.key] = bounds.start;
// Calculate the new index of the first item within the bounds
startIndex = this.sortedIndex(this.rows, testValue);
discarded = this.rows.splice(0, startIndex);
}
if (endChanged) {
testValue.datum[this.sortOptions.key] = bounds.end;
// Calculate the new index of the last item in bounds
endIndex = this.sortedLastIndex(this.futureBuffer.rows, testValue);
added = this.futureBuffer.rows.splice(0, endIndex);
added.forEach((datum) => this.rows.push(datum));
}
if (discarded.length > 0) {
/**
* A `discarded` event is emitted when telemetry data fall out of
* bounds due to a bounds change event
* @type {object[]} discarded the telemetry data
* discarded as a result of the bounds change
*/
this.emit('remove', discarded);
}
if (added.length > 0) {
/**
* An `added` event is emitted when a bounds change results in
* received telemetry falling within the new bounds.
* @type {object[]} added the telemetry data that is now within bounds
*/
this.emit('add', added);
}
}
timeSystem(timeSystem) {
let timeKey = timeSystem.key;
let formatter = this.openmct.telemetry.getValueFormatter({
key: timeKey,
source: timeKey,
format: timeKey
});
this.parseTime = formatter.parse;
// TODO: Reset right?
}
on(event, callback, context) {
if (!this.listeners[event]) {
throw new Error('Event not supported by Telemetry Collections: ' + event);
}
if (this.listeners[event].includes(callback)) {
throw new Error('Tried to add a listener that is already registered');
}
this.listeners[event].push({
callback: callback,
context: context
});
}
// Unregister TelemetryCollection events.
off(event, callback) {
if (!this.listeners[event]) {
throw new Error('Event not supported by Telemetry Collections: ' + event);
}
if (!this.listeners[event].includes(callback)) {
throw new Error('Tried to remove a listener that does not exist');
}
this.listeners[event].remove(callback);
}
emit(event, payload) {
if (!this.listeners[event].length) {
return;
}
payload = [...payload];
this.listeners[event].forEach((listener) => {
if (listener.context) {
listener.callback.apply(listener.context, payload);
} else {
listener.callback(payload);
}
});
}
subscribeToBounds() {
this.openmct.time.on('bounds', this.bounds);
}
unsubscribeFromBounds() {
this.openmct.time.off('bounds', this.bounds);
}
subscribeToTimeSystem() {
this.openmct.time.on('timeSystem', this.timeSystem);
}
unsubscribeFromTimeSystem() {
this.openmct.time.off('bounds', this.timeSystem);
}
destroy() {
this.unsubscribeFromBounds();
this.unsubscribeFromTimeSystem();
if (this.unsubscribe) {
this.unsubscribe();
}
}
}

View File

@@ -0,0 +1,72 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import objectUtils from 'objectUtils';
class TelemetrySubscriptionService {
constructor(openmct) {
if (!TelemetrySubscriptionService.instance) {
this.openmct = openmct;
this.subscriptionCache = {};
TelemetrySubscriptionService.instance = this;
}
return TelemetrySubscriptionService.instance; // eslint-disable-line no-constructor-return
}
subscribe(domainObject, callback, provider, options) {
const keyString = objectUtils.makeKeyString(domainObject.identifier);
let subscriber = this.subscriptionCache[keyString];
if (!subscriber) {
subscriber = this.subscriptionCache[keyString] = {
callbacks: [callback]
};
subscriber.unsubscribe = provider
.subscribe(domainObject, function (value) {
subscriber.callbacks.forEach(function (cb) {
cb(value);
});
}, options);
} else {
subscriber.callbacks.push(callback);
}
return () => {
subscriber.callbacks = subscriber.callbacks.filter((cb) => {
return cb !== callback;
});
if (subscriber.callbacks.length === 0) {
subscriber.unsubscribe();
delete this.subscriptionCache[keyString];
}
};
}
}
function instance(openmct) {
return new TelemetrySubscriptionService(openmct);
}
export default instance;

View File

@@ -44,6 +44,7 @@
<script>
const CONTEXT_MENU_ACTIONS = [
'viewDatumAction',
'viewHistoricalData',
'remove'
];
@@ -129,6 +130,7 @@ export default {
let limit;
if (this.shouldUpdate(newTimestamp)) {
this.datum = datum;
this.timestamp = newTimestamp;
this.value = this.formats[this.valueKey].format(datum);
limit = this.limitEvaluator.evaluate(datum, this.valueMetadata);
@@ -175,8 +177,22 @@ export default {
this.resetValues();
this.timestampKey = timeSystem.key;
},
getView() {
return {
getViewContext: () => {
return {
viewHistoricalData: true,
viewDatumAction: true,
getDatum: () => {
return this.datum;
}
};
}
};
},
showContextMenu(event) {
let allActions = this.openmct.actions.get(this.currentObjectPath, {}, {viewHistoricalData: true});
let actionCollection = this.openmct.actions.get(this.currentObjectPath, this.getView());
let allActions = actionCollection.getActionsObject();
let applicableActions = CONTEXT_MENU_ACTIONS.map(key => allActions[key]);
this.openmct.menus.showMenu(event.x, event.y, applicableActions);

View File

@@ -19,342 +19,352 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import AutoflowTabularPlugin from './AutoflowTabularPlugin';
import AutoflowTabularConstants from './AutoflowTabularConstants';
import $ from 'zepto';
import DOMObserver from './dom-observer';
import {
createOpenMct,
resetApplicationState,
spyOnBuiltins
} from 'utils/testing';
define([
'./AutoflowTabularPlugin',
'./AutoflowTabularConstants',
'../../MCT',
'zepto',
'./dom-observer'
], function (AutoflowTabularPlugin, AutoflowTabularConstants, MCT, $, DOMObserver) {
describe("AutoflowTabularPlugin", function () {
let testType;
let testObject;
let mockmct;
describe("AutoflowTabularPlugin", () => {
let testType;
let testObject;
let mockmct;
beforeEach(function () {
testType = "some-type";
testObject = { type: testType };
mockmct = new MCT();
spyOn(mockmct.composition, 'get');
spyOn(mockmct.objectViews, 'addProvider');
spyOn(mockmct.telemetry, 'getMetadata');
spyOn(mockmct.telemetry, 'getValueFormatter');
spyOn(mockmct.telemetry, 'limitEvaluator');
spyOn(mockmct.telemetry, 'request');
spyOn(mockmct.telemetry, 'subscribe');
beforeEach(() => {
testType = "some-type";
testObject = { type: testType };
mockmct = createOpenMct();
spyOn(mockmct.composition, 'get');
spyOn(mockmct.objectViews, 'addProvider');
spyOn(mockmct.telemetry, 'getMetadata');
spyOn(mockmct.telemetry, 'getValueFormatter');
spyOn(mockmct.telemetry, 'limitEvaluator');
spyOn(mockmct.telemetry, 'request');
spyOn(mockmct.telemetry, 'subscribe');
const plugin = new AutoflowTabularPlugin({ type: testType });
plugin(mockmct);
const plugin = new AutoflowTabularPlugin({ type: testType });
plugin(mockmct);
});
afterEach(() => {
resetApplicationState(mockmct);
});
it("installs a view provider", () => {
expect(mockmct.objectViews.addProvider).toHaveBeenCalled();
});
describe("installs a view provider which", () => {
let provider;
beforeEach(() => {
provider =
mockmct.objectViews.addProvider.calls.mostRecent().args[0];
});
it("installs a view provider", function () {
expect(mockmct.objectViews.addProvider).toHaveBeenCalled();
it("applies its view to the type from options", () => {
expect(provider.canView(testObject)).toBe(true);
});
describe("installs a view provider which", function () {
let provider;
it("does not apply to other types", () => {
expect(provider.canView({ type: 'foo' })).toBe(false);
});
beforeEach(function () {
provider =
mockmct.objectViews.addProvider.calls.mostRecent().args[0];
});
describe("provides a view which", () => {
let testKeys;
let testChildren;
let testContainer;
let testHistories;
let mockComposition;
let mockMetadata;
let mockEvaluator;
let mockUnsubscribes;
let callbacks;
let view;
let domObserver;
it("applies its view to the type from options", function () {
expect(provider.canView(testObject)).toBe(true);
});
function waitsForChange() {
return new Promise(function (resolve) {
window.requestAnimationFrame(resolve);
});
}
it("does not apply to other types", function () {
expect(provider.canView({ type: 'foo' })).toBe(false);
});
function emitEvent(mockEmitter, type, event) {
mockEmitter.on.calls.all().forEach((call) => {
if (call.args[0] === type) {
call.args[1](event);
}
});
}
describe("provides a view which", function () {
let testKeys;
let testChildren;
let testContainer;
let testHistories;
let mockComposition;
let mockMetadata;
let mockEvaluator;
let mockUnsubscribes;
let callbacks;
let view;
let domObserver;
beforeEach((done) => {
callbacks = {};
function waitsForChange() {
return new Promise(function (resolve) {
window.requestAnimationFrame(resolve);
spyOnBuiltins(['requestAnimationFrame']);
window.requestAnimationFrame.and.callFake((callBack) => {
callBack();
});
testObject = { type: 'some-type' };
testKeys = ['abc', 'def', 'xyz'];
testChildren = testKeys.map((key) => {
return {
identifier: {
namespace: "test",
key: key
},
name: "Object " + key
};
});
testContainer = $('<div>')[0];
domObserver = new DOMObserver(testContainer);
testHistories = testKeys.reduce((histories, key, index) => {
histories[key] = {
key: key,
range: index + 10,
domain: key + index
};
return histories;
}, {});
mockComposition =
jasmine.createSpyObj('composition', ['load', 'on', 'off']);
mockMetadata =
jasmine.createSpyObj('metadata', ['valuesForHints']);
mockEvaluator = jasmine.createSpyObj('evaluator', ['evaluate']);
mockUnsubscribes = testKeys.reduce((map, key) => {
map[key] = jasmine.createSpy('unsubscribe-' + key);
return map;
}, {});
mockmct.composition.get.and.returnValue(mockComposition);
mockComposition.load.and.callFake(() => {
testChildren.forEach(emitEvent.bind(null, mockComposition, 'add'));
return Promise.resolve(testChildren);
});
mockmct.telemetry.getMetadata.and.returnValue(mockMetadata);
mockmct.telemetry.getValueFormatter.and.callFake((metadatum) => {
const mockFormatter = jasmine.createSpyObj('formatter', ['format']);
mockFormatter.format.and.callFake((datum) => {
return datum[metadatum.hint];
});
return mockFormatter;
});
mockmct.telemetry.limitEvaluator.and.returnValue(mockEvaluator);
mockmct.telemetry.subscribe.and.callFake((obj, callback) => {
const key = obj.identifier.key;
callbacks[key] = callback;
return mockUnsubscribes[key];
});
mockmct.telemetry.request.and.callFake((obj, request) => {
const key = obj.identifier.key;
return Promise.resolve([testHistories[key]]);
});
mockMetadata.valuesForHints.and.callFake((hints) => {
return [{ hint: hints[0] }];
});
view = provider.view(testObject);
view.show(testContainer);
return done();
});
afterEach(() => {
domObserver.destroy();
});
it("populates its container", () => {
expect(testContainer.children.length > 0).toBe(true);
});
describe("when rows have been populated", () => {
function rowsMatch() {
const rows = $(testContainer).find(".l-autoflow-row").length;
return rows === testChildren.length;
}
function emitEvent(mockEmitter, type, event) {
mockEmitter.on.calls.all().forEach(function (call) {
if (call.args[0] === type) {
call.args[1](event);
}
});
it("shows one row per child object", () => {
return domObserver.when(rowsMatch);
});
// it("adds rows on composition change", () => {
// const child = {
// identifier: {
// namespace: "test",
// key: "123"
// },
// name: "Object 123"
// };
// testChildren.push(child);
// emitEvent(mockComposition, 'add', child);
// return domObserver.when(rowsMatch);
// });
it("removes rows on composition change", () => {
const child = testChildren.pop();
emitEvent(mockComposition, 'remove', child.identifier);
return domObserver.when(rowsMatch);
});
});
it("removes subscriptions when destroyed", () => {
testKeys.forEach((key) => {
expect(mockUnsubscribes[key]).not.toHaveBeenCalled();
});
view.destroy();
testKeys.forEach((key) => {
expect(mockUnsubscribes[key]).toHaveBeenCalled();
});
});
it("provides a button to change column width", () => {
const initialWidth = AutoflowTabularConstants.INITIAL_COLUMN_WIDTH;
const nextWidth =
initialWidth + AutoflowTabularConstants.COLUMN_WIDTH_STEP;
expect($(testContainer).find('.l-autoflow-col').css('width'))
.toEqual(initialWidth + 'px');
$(testContainer).find('.change-column-width').click();
function widthHasChanged() {
const width = $(testContainer).find('.l-autoflow-col').css('width');
return width !== initialWidth + 'px';
}
beforeEach(function () {
callbacks = {};
testObject = { type: 'some-type' };
testKeys = ['abc', 'def', 'xyz'];
testChildren = testKeys.map(function (key) {
return {
identifier: {
namespace: "test",
key: key
},
name: "Object " + key
};
return domObserver.when(widthHasChanged)
.then(() => {
expect($(testContainer).find('.l-autoflow-col').css('width'))
.toEqual(nextWidth + 'px');
});
testContainer = $('<div>')[0];
domObserver = new DOMObserver(testContainer);
});
testHistories = testKeys.reduce(function (histories, key, index) {
histories[key] = {
key: key,
range: index + 10,
domain: key + index
};
it("subscribes to all child objects", () => {
testKeys.forEach((key) => {
expect(callbacks[key]).toEqual(jasmine.any(Function));
});
});
return histories;
}, {});
it("displays historical telemetry", () => {
function rowTextDefined() {
return $(testContainer).find(".l-autoflow-item").filter(".r").text() !== "";
}
mockComposition =
jasmine.createSpyObj('composition', ['load', 'on', 'off']);
mockMetadata =
jasmine.createSpyObj('metadata', ['valuesForHints']);
mockEvaluator = jasmine.createSpyObj('evaluator', ['evaluate']);
mockUnsubscribes = testKeys.reduce(function (map, key) {
map[key] = jasmine.createSpy('unsubscribe-' + key);
return map;
}, {});
mockmct.composition.get.and.returnValue(mockComposition);
mockComposition.load.and.callFake(function () {
testChildren.forEach(emitEvent.bind(null, mockComposition, 'add'));
return Promise.resolve(testChildren);
return domObserver.when(rowTextDefined).then(() => {
testKeys.forEach((key, index) => {
const datum = testHistories[key];
const $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r");
expect($cell.text()).toEqual(String(datum.range));
});
});
});
mockmct.telemetry.getMetadata.and.returnValue(mockMetadata);
mockmct.telemetry.getValueFormatter.and.callFake(function (metadatum) {
const mockFormatter = jasmine.createSpyObj('formatter', ['format']);
mockFormatter.format.and.callFake(function (datum) {
return datum[metadatum.hint];
});
return mockFormatter;
});
mockmct.telemetry.limitEvaluator.and.returnValue(mockEvaluator);
mockmct.telemetry.subscribe.and.callFake(function (obj, callback) {
const key = obj.identifier.key;
callbacks[key] = callback;
return mockUnsubscribes[key];
});
mockmct.telemetry.request.and.callFake(function (obj, request) {
const key = obj.identifier.key;
return Promise.resolve([testHistories[key]]);
});
mockMetadata.valuesForHints.and.callFake(function (hints) {
return [{ hint: hints[0] }];
});
view = provider.view(testObject);
view.show(testContainer);
return waitsForChange();
it("displays incoming telemetry", () => {
const testData = testKeys.map((key, index) => {
return {
key: key,
range: index * 100,
domain: key + index
};
});
afterEach(function () {
domObserver.destroy();
testData.forEach((datum) => {
callbacks[datum.key](datum);
});
it("populates its container", function () {
expect(testContainer.children.length > 0).toBe(true);
return waitsForChange().then(() => {
testData.forEach((datum, index) => {
const $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r");
expect($cell.text()).toEqual(String(datum.range));
});
});
});
describe("when rows have been populated", function () {
function rowsMatch() {
const rows = $(testContainer).find(".l-autoflow-row").length;
return rows === testChildren.length;
}
it("shows one row per child object", function () {
return domObserver.when(rowsMatch);
});
it("adds rows on composition change", function () {
const child = {
identifier: {
namespace: "test",
key: "123"
},
name: "Object 123"
};
testChildren.push(child);
emitEvent(mockComposition, 'add', child);
return domObserver.when(rowsMatch);
});
it("removes rows on composition change", function () {
const child = testChildren.pop();
emitEvent(mockComposition, 'remove', child.identifier);
return domObserver.when(rowsMatch);
it("updates classes for limit violations", () => {
const testClass = "some-limit-violation";
mockEvaluator.evaluate.and.returnValue({ cssClass: testClass });
testKeys.forEach((key) => {
callbacks[key]({
range: 'foo',
domain: 'bar'
});
});
it("removes subscriptions when destroyed", function () {
testKeys.forEach(function (key) {
expect(mockUnsubscribes[key]).not.toHaveBeenCalled();
});
view.destroy();
testKeys.forEach(function (key) {
expect(mockUnsubscribes[key]).toHaveBeenCalled();
return waitsForChange().then(() => {
testKeys.forEach((datum, index) => {
const $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r");
expect($cell.hasClass(testClass)).toBe(true);
});
});
});
it("provides a button to change column width", function () {
const initialWidth = AutoflowTabularConstants.INITIAL_COLUMN_WIDTH;
const nextWidth =
initialWidth + AutoflowTabularConstants.COLUMN_WIDTH_STEP;
it("automatically flows to new columns", () => {
const rowHeight = AutoflowTabularConstants.ROW_HEIGHT;
const sliderHeight = AutoflowTabularConstants.SLIDER_HEIGHT;
const count = testKeys.length;
const $container = $(testContainer);
let promiseChain = Promise.resolve();
expect($(testContainer).find('.l-autoflow-col').css('width'))
.toEqual(initialWidth + 'px');
function columnsHaveAutoflowed() {
const itemsHeight = $container.find('.l-autoflow-items').height();
const availableHeight = itemsHeight - sliderHeight;
const availableRows = Math.max(Math.floor(availableHeight / rowHeight), 1);
const columns = Math.ceil(count / availableRows);
$(testContainer).find('.change-column-width').click();
return $container.find('.l-autoflow-col').length === columns;
}
function widthHasChanged() {
const width = $(testContainer).find('.l-autoflow-col').css('width');
return width !== initialWidth + 'px';
}
return domObserver.when(widthHasChanged)
.then(function () {
expect($(testContainer).find('.l-autoflow-col').css('width'))
.toEqual(nextWidth + 'px');
});
$container.find('.abs').css({
position: 'absolute',
left: '0px',
right: '0px',
top: '0px',
bottom: '0px'
});
$container.css({ position: 'absolute' });
it("subscribes to all child objects", function () {
testKeys.forEach(function (key) {
expect(callbacks[key]).toEqual(jasmine.any(Function));
});
$container.appendTo(document.body);
function setHeight(height) {
$container.css('height', height + 'px');
return domObserver.when(columnsHaveAutoflowed);
}
for (let height = 0; height < rowHeight * count * 2; height += rowHeight / 2) {
// eslint-disable-next-line no-invalid-this
promiseChain = promiseChain.then(setHeight.bind(this, height));
}
return promiseChain.then(() => {
$container.remove();
});
});
it("displays historical telemetry", function () {
function rowTextDefined() {
return $(testContainer).find(".l-autoflow-item").filter(".r").text() !== "";
}
return domObserver.when(rowTextDefined).then(function () {
testKeys.forEach(function (key, index) {
const datum = testHistories[key];
const $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r");
expect($cell.text()).toEqual(String(datum.range));
});
});
});
it("displays incoming telemetry", function () {
const testData = testKeys.map(function (key, index) {
return {
key: key,
range: index * 100,
domain: key + index
};
});
testData.forEach(function (datum) {
callbacks[datum.key](datum);
});
return waitsForChange().then(function () {
testData.forEach(function (datum, index) {
const $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r");
expect($cell.text()).toEqual(String(datum.range));
});
});
});
it("updates classes for limit violations", function () {
const testClass = "some-limit-violation";
mockEvaluator.evaluate.and.returnValue({ cssClass: testClass });
testKeys.forEach(function (key) {
callbacks[key]({
range: 'foo',
domain: 'bar'
});
});
return waitsForChange().then(function () {
testKeys.forEach(function (datum, index) {
const $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r");
expect($cell.hasClass(testClass)).toBe(true);
});
});
});
it("automatically flows to new columns", function () {
const rowHeight = AutoflowTabularConstants.ROW_HEIGHT;
const sliderHeight = AutoflowTabularConstants.SLIDER_HEIGHT;
const count = testKeys.length;
const $container = $(testContainer);
let promiseChain = Promise.resolve();
function columnsHaveAutoflowed() {
const itemsHeight = $container.find('.l-autoflow-items').height();
const availableHeight = itemsHeight - sliderHeight;
const availableRows = Math.max(Math.floor(availableHeight / rowHeight), 1);
const columns = Math.ceil(count / availableRows);
return $container.find('.l-autoflow-col').length === columns;
}
$container.find('.abs').css({
position: 'absolute',
left: '0px',
right: '0px',
top: '0px',
bottom: '0px'
});
$container.css({ position: 'absolute' });
$container.appendTo(document.body);
function setHeight(height) {
$container.css('height', height + 'px');
return domObserver.when(columnsHaveAutoflowed);
}
for (let height = 0; height < rowHeight * count * 2; height += rowHeight / 2) {
// eslint-disable-next-line no-invalid-this
promiseChain = promiseChain.then(setHeight.bind(this, height));
}
return promiseChain.then(function () {
$container.remove();
});
});
it("loads composition exactly once", function () {
const testObj = testChildren.pop();
emitEvent(mockComposition, 'remove', testObj.identifier);
testChildren.push(testObj);
emitEvent(mockComposition, 'add', testObj);
expect(mockComposition.load.calls.count()).toEqual(1);
});
it("loads composition exactly once", () => {
const testObj = testChildren.pop();
emitEvent(mockComposition, 'remove', testObj.identifier);
testChildren.push(testObj);
emitEvent(mockComposition, 'add', testObj);
expect(mockComposition.load.calls.count()).toEqual(1);
});
});
});

View File

@@ -22,6 +22,7 @@
import { createOpenMct, resetApplicationState } from "utils/testing";
import ConditionPlugin from "./plugin";
import stylesManager from '@/ui/inspector/styles/StylesManager';
import StylesView from "./components/inspector/StylesView.vue";
import Vue from 'vue';
import {getApplicableStylesForItem} from "./utils/styleUtils";
@@ -402,7 +403,8 @@ describe('the plugin', function () {
component = new Vue({
provide: {
openmct: openmct,
selection: selection
selection: selection,
stylesManager
},
el: viewContainer,
components: {

View File

@@ -0,0 +1,38 @@
import printj from 'printj';
export default class CustomStringFormatter {
constructor(openmct, valueMetadata, itemFormat) {
this.openmct = openmct;
this.itemFormat = itemFormat;
this.valueMetadata = valueMetadata;
}
format(datum) {
if (!this.itemFormat) {
return;
}
if (!this.itemFormat.startsWith('&')) {
return printj.sprintf(this.itemFormat, datum[this.valueMetadata.key]);
}
try {
const key = this.itemFormat.slice(1);
const customFormatter = this.openmct.telemetry.getFormatter(key);
if (!customFormatter) {
throw new Error('Custom Formatter not found');
}
return customFormatter.format(datum[this.valueMetadata.key]);
} catch (e) {
console.error(e);
return datum[this.valueMetadata.key];
}
}
setFormat(itemFormat) {
this.itemFormat = itemFormat;
}
}

View File

@@ -0,0 +1,82 @@
import CustomStringFormatter from './CustomStringFormatter';
import { createOpenMct, resetApplicationState } from 'utils/testing';
const CUSTOM_FORMATS = [
{
key: 'sclk',
format: (value) => 2 * value
},
{
key: 'lts',
format: (value) => 3 * value
}
];
const valueMetadata = {
key: "sin",
name: "Sine",
unit: "Hz",
formatString: "%0.2f",
hints: {
range: 1,
priority: 3
},
source: "sin"
};
const datum = {
name: "1 Sine Wave Generator",
utc: 1603930354000,
yesterday: 1603843954000,
sin: 0.587785209686822,
cos: -0.8090170253297632
};
describe('CustomStringFormatter', function () {
let element;
let child;
let openmct;
let customStringFormatter;
beforeEach((done) => {
openmct = createOpenMct();
element = document.createElement('div');
child = document.createElement('div');
element.appendChild(child);
CUSTOM_FORMATS.forEach(openmct.telemetry.addFormat.bind({openmct}));
openmct.on('start', done);
openmct.startHeadless();
spyOn(openmct.telemetry, 'getFormatter');
openmct.telemetry.getFormatter.and.callFake((key) => CUSTOM_FORMATS.find(d => d.key === key));
customStringFormatter = new CustomStringFormatter(openmct, valueMetadata);
});
afterEach(() => {
return resetApplicationState(openmct);
});
it('adds custom format sclk', () => {
const format = openmct.telemetry.getFormatter('sclk');
expect(format.key).toEqual('sclk');
});
it('adds custom format lts', () => {
const format = openmct.telemetry.getFormatter('lts');
expect(format.key).toEqual('lts');
});
it('returns correct value for custom format sclk', () => {
customStringFormatter.setFormat('&sclk');
const value = customStringFormatter.format(datum, valueMetadata);
expect(datum.sin * 2).toEqual(value);
});
it('returns correct value for custom format lts', () => {
customStringFormatter.setFormat('&lts');
const value = customStringFormatter.format(datum, valueMetadata);
expect(datum.sin * 3).toEqual(value);
});
});

View File

@@ -5,29 +5,30 @@ export default class CopyToClipboardAction {
this.openmct = openmct;
this.cssClass = 'icon-duplicate';
this.description = 'Copy to Clipboard action';
this.description = 'Copy value to clipboard';
this.group = "action";
this.key = 'copyToClipboard';
this.name = 'Copy to Clipboard';
this.priority = 9;
this.priority = 1;
}
invoke(objectPath, viewContext) {
invoke(objectPath, view = {}) {
const viewContext = view.getViewContext && view.getViewContext();
const formattedValue = viewContext.formattedValueForCopy();
clipboard.updateClipboard(formattedValue)
.then(() => {
this.openmct.notifications.info(`Success : copied to clipboard '${formattedValue}'`);
this.openmct.notifications.info(`Success : copied '${formattedValue}' to clipboard `);
})
.catch(() => {
this.openmct.notifications.error(`Failed : to copy to clipboard '${formattedValue}'`);
this.openmct.notifications.error(`Failed : to copy '${formattedValue}' to clipboard `);
});
}
appliesTo(objectPath, viewContext) {
if (viewContext && viewContext.getViewKey) {
return viewContext.getViewKey().includes('alphanumeric-format');
}
appliesTo(objectPath, view = {}) {
let viewContext = view.getViewContext && view.getViewContext();
return false;
return viewContext && viewContext.formattedValueForCopy
&& typeof viewContext.formattedValueForCopy === 'function';
}
}

View File

@@ -30,7 +30,7 @@
>
<div
v-if="domainObject"
class="c-telemetry-view"
class="c-telemetry-view u-style-receiver"
:class="[statusClass]"
:style="styleObject"
:data-font-size="item.fontSize"
@@ -38,7 +38,7 @@
@contextmenu.prevent="showContextMenu"
>
<div class="is-status__indicator"
:title="`This item is ${this.status}`"
:title="`This item is ${status}`"
></div>
<div
v-if="showLabel"
@@ -71,7 +71,6 @@
<script>
import LayoutFrame from './LayoutFrame.vue';
import printj from 'printj';
import conditionalStylesMixin from "../mixins/objectStyles-mixin";
import { getDefaultNotebook } from '@/plugins/notebook/utils/notebook-storage.js';
@@ -172,7 +171,11 @@ export default {
valueMetadata() {
return this.datum && this.metadata.value(this.item.value);
},
valueFormatter() {
formatter() {
if (this.item.format) {
return this.customStringformatter;
}
return this.formats[this.item.value];
},
telemetryValue() {
@@ -180,11 +183,7 @@ export default {
return;
}
if (this.item.format) {
return printj.sprintf(this.item.format, this.datum[this.valueMetadata.key]);
}
return this.valueFormatter && this.valueFormatter.format(this.datum);
return this.formatter && this.formatter.format(this.datum);
},
telemetryClass() {
if (!this.datum) {
@@ -231,17 +230,12 @@ export default {
this.openmct.time.off("bounds", this.refreshData);
},
methods: {
getViewContext() {
return {
getViewKey: () => this.viewKey,
formattedValueForCopy: this.formattedValueForCopy
};
},
formattedValueForCopy() {
const timeFormatterKey = this.openmct.time.timeSystem().key;
const timeFormatter = this.formats[timeFormatterKey];
const unit = this.unit ? ` ${this.unit}` : '';
return `At ${timeFormatter.format(this.datum)} ${this.domainObject.name} had a value of ${this.telemetryValue} ${this.unit}`;
return `At ${timeFormatter.format(this.datum)} ${this.domainObject.name} had a value of ${this.telemetryValue}${unit}`;
},
requestHistoricalData() {
let bounds = this.openmct.time.bounds();
@@ -282,10 +276,10 @@ export default {
},
getView() {
return {
getViewContext() {
getViewContext: () => {
return {
viewHistoricalData: true,
skipCache: true
formattedValueForCopy: this.formattedValueForCopy
};
}
};
@@ -296,6 +290,10 @@ export default {
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
this.limitEvaluator = this.openmct.telemetry.limitEvaluator(this.domainObject);
this.formats = this.openmct.telemetry.getFormatMap(this.metadata);
const valueMetadata = this.metadata.value(this.item.value);
this.customStringformatter = this.openmct.telemetry.customStringFormatter(valueMetadata, this.item.format);
this.requestHistoricalData();
this.subscribeToObject();
@@ -313,36 +311,35 @@ export default {
this.removeSelectable = this.openmct.selection.selectable(
this.$el, this.context, this.immediatelySelect || this.initSelect);
delete this.immediatelySelect;
let allActions = this.openmct.actions.get(this.currentObjectPath, this.getView());
this.applicableActions = CONTEXT_MENU_ACTIONS.map(actionKey => {
return allActions[actionKey];
});
},
updateTelemetryFormat(format) {
this.customStringformatter.setFormat(format);
this.$emit('formatChanged', this.item, format);
},
async getContextMenuActions() {
const defaultNotebook = getDefaultNotebook();
const domainObject = defaultNotebook && await this.openmct.objects.get(defaultNotebook.notebookMeta.identifier);
const actionCollection = this.openmct.actions.get(this.currentObjectPath, this.getView());
const actionsObject = actionCollection.getActionsObject();
const actionsObject = this.openmct.actions.get(this.currentObjectPath, this.getViewContext(), { viewHistoricalData: true }).applicableActions;
let applicableActionKeys = Object.keys(actionsObject)
.filter(key => {
const isCopyToNotebook = actionsObject[key].key === 'copyToNotebook';
if (defaultNotebook && isCopyToNotebook) {
const defaultPath = domainObject && `${domainObject.name} - ${defaultNotebook.section.name} - ${defaultNotebook.page.name}`;
actionsObject[key].name = `Copy to Notebook ${defaultPath}`;
}
let copyToNotebookAction = actionsObject.copyToNotebook;
return CONTEXT_MENU_ACTIONS.includes(actionsObject[key].key);
});
if (defaultNotebook) {
const defaultPath = domainObject && `${domainObject.name} - ${defaultNotebook.section.name} - ${defaultNotebook.page.name}`;
copyToNotebookAction.name = `Copy to Notebook ${defaultPath}`;
} else {
actionsObject.copyToNotebook = undefined;
delete actionsObject.copyToNotebook;
}
return applicableActionKeys.map(key => actionsObject[key]);
return CONTEXT_MENU_ACTIONS.map(actionKey => {
return actionsObject[actionKey];
}).filter(action => action !== undefined);
},
async showContextMenu(event) {
const contextMenuActions = await this.getContextMenuActions();
this.openmct.menus.showMenu(event.x, event.y, contextMenuActions);
},
setStatus(status) {

View File

@@ -7,7 +7,6 @@
flex: 1 1 auto;
display: flex;
flex-direction: row;
// justify-content: center;
align-items: center;
overflow: hidden;
padding: $interiorMargin;
@@ -28,19 +27,12 @@
}
.is-status__indicator {
position: absolute;
top: 0;
left: 0;
}
&.is-status--missing {
@include isMissing($absPos: true);
border: $borderMissing;
}
&.is-status--suspect {
@include isSuspect($absPos: true);
&[class*='is-status'] {
border: $borderMissing;
}
}

View File

@@ -0,0 +1,159 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import DuplicateTask from './DuplicateTask';
export default class DuplicateAction {
constructor(openmct) {
this.name = 'Duplicate';
this.key = 'duplicate';
this.description = 'Duplicate this object.';
this.cssClass = "icon-duplicate";
this.group = "action";
this.priority = 7;
this.openmct = openmct;
}
async invoke(objectPath) {
let duplicationTask = new DuplicateTask(this.openmct);
let originalObject = objectPath[0];
let parent = objectPath[1];
let userInput = await this.getUserInput(originalObject, parent);
let newParent = userInput.location;
let inNavigationPath = this.inNavigationPath(originalObject);
// legacy check
if (this.isLegacyDomainObject(newParent)) {
newParent = await this.convertFromLegacy(newParent);
}
// if editing, save
if (inNavigationPath && this.openmct.editor.isEditing()) {
this.openmct.editor.save();
}
// duplicate
let newObject = await duplicationTask.duplicate(originalObject, newParent);
this.updateNameCheck(newObject, userInput.name);
return;
}
async getUserInput(originalObject, parent) {
let dialogService = this.openmct.$injector.get('dialogService');
let dialogForm = this.getDialogForm(originalObject, parent);
let formState = {
name: originalObject.name
};
let userInput = await dialogService.getUserInput(dialogForm, formState);
return userInput;
}
updateNameCheck(object, name) {
if (object.name !== name) {
this.openmct.objects.mutate(object, 'name', name);
}
}
inNavigationPath(object) {
return this.openmct.router.path
.some(objectInPath => this.openmct.objects.areIdsEqual(objectInPath.identifier, object.identifier));
}
getDialogForm(object, parent) {
return {
name: "Duplicate Item",
sections: [
{
rows: [
{
key: "name",
control: "textfield",
name: "Name",
pattern: "\\S+",
required: true,
cssClass: "l-input-lg"
},
{
name: "location",
cssClass: "grows",
control: "locator",
validate: this.validate(object, parent),
key: 'location'
}
]
}
]
};
}
validate(object, currentParent) {
return (parentCandidate) => {
let currentParentKeystring = this.openmct.objects.makeKeyString(currentParent.identifier);
let parentCandidateKeystring = this.openmct.objects.makeKeyString(parentCandidate.getId());
let objectKeystring = this.openmct.objects.makeKeyString(object.identifier);
if (!parentCandidate || !currentParentKeystring) {
return false;
}
if (parentCandidateKeystring === objectKeystring) {
return false;
}
return this.openmct.composition.checkPolicy(
parentCandidate.useCapability('adapter'),
object
);
};
}
isLegacyDomainObject(domainObject) {
return domainObject.getCapability !== undefined;
}
async convertFromLegacy(legacyDomainObject) {
let objectContext = legacyDomainObject.getCapability('context');
let domainObject = await this.openmct.objects.get(objectContext.domainObject.id);
return domainObject;
}
appliesTo(objectPath) {
let parent = objectPath[1];
let parentType = parent && this.openmct.types.get(parent.type);
let child = objectPath[0];
let childType = child && this.openmct.types.get(child.type);
let locked = child.locked ? child.locked : parent && parent.locked;
if (locked) {
return false;
}
return childType
&& childType.definition.creatable
&& parentType
&& parentType.definition.creatable
&& Array.isArray(parent.composition);
}
}

View File

@@ -0,0 +1,273 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import uuid from 'uuid';
/**
* This class encapsulates the process of duplicating/copying a domain object
* and all of its children.
*
* @param {DomainObject} domainObject The object to duplicate
* @param {DomainObject} parent The new location of the cloned object tree
* @param {src/plugins/duplicate.DuplicateService~filter} filter
* a function used to filter out objects from
* the cloning process
* @constructor
*/
export default class DuplicateTask {
constructor(openmct) {
this.domainObject = undefined;
this.parent = undefined;
this.firstClone = undefined;
this.filter = undefined;
this.persisted = 0;
this.clones = [];
this.idMap = {};
this.openmct = openmct;
}
/**
* Execute the duplicate/copy task with the objects provided.
* @returns {promise} Which will resolve with a clone of the object
* once complete.
*/
async duplicate(domainObject, parent, filter) {
this.domainObject = domainObject;
this.parent = parent;
this.namespace = parent.identifier.namespace;
this.filter = filter || this.isCreatable;
await this.buildDuplicationPlan();
await this.persistObjects();
await this.addClonesToParent();
return this.firstClone;
}
/**
* Will build a graph of an object and all of its child objects in
* memory
* @private
* @param domainObject The original object to be copied
* @param parent The parent of the original object to be copied
* @returns {Promise} resolved with an array of clones of the models
* of the object tree being copied. Duplicating is done in a bottom-up
* fashion, so that the last member in the array is a clone of the model
* object being copied. The clones are all full composed with
* references to their own children.
*/
async buildDuplicationPlan() {
let domainObjectClone = await this.duplicateObject(this.domainObject);
if (domainObjectClone !== this.domainObject) {
domainObjectClone.location = this.getKeyString(this.parent);
}
this.firstClone = domainObjectClone;
return;
}
/**
* Will persist a list of {@link objectClones}. It will persist all
* simultaneously, irrespective of order in the list. This may
* result in automatic request batching by the browser.
*/
async persistObjects() {
let initialCount = this.clones.length;
let dialog = this.openmct.overlays.progressDialog({
progressPerc: 0,
message: `Duplicating ${initialCount} objects.`,
iconClass: 'info',
title: 'Duplicating'
});
let clonesDone = Promise.all(this.clones.map((clone) => {
let percentPersisted = Math.ceil(100 * (++this.persisted / initialCount));
let message = `Duplicating ${initialCount - this.persisted} objects.`;
dialog.updateProgress(percentPersisted, message);
return this.openmct.objects.save(clone);
}));
await clonesDone;
dialog.dismiss();
this.openmct.notifications.info(`Duplicated ${this.persisted} objects.`);
return;
}
/**
* Will add a list of clones to the specified parent's composition
*/
async addClonesToParent() {
let parentComposition = this.openmct.composition.get(this.parent);
await parentComposition.load();
parentComposition.add(this.firstClone);
return;
}
/**
* A recursive function that will perform a bottom-up duplicate of
* the object tree with originalObject at the root. Recurses to
* the farthest leaf, then works its way back up again,
* cloning objects, and composing them with their child clones
* as it goes
* @private
* @returns {DomainObject} If the type of the original object allows for
* duplication, then a duplicate of the object, otherwise the object
* itself (to allow linking to non duplicatable objects).
*/
async duplicateObject(originalObject) {
// Check if the creatable (or other passed in filter).
if (this.filter(originalObject)) {
let clone = this.cloneObjectModel(originalObject);
let composeesCollection = this.openmct.composition.get(originalObject);
let composees;
if (composeesCollection) {
composees = await composeesCollection.load();
}
return this.duplicateComposees(clone, composees);
}
// Not creatable, creating a link, no need to iterate children
return originalObject;
}
/**
* Given an array of objects composed by a parent, clone them, then
* add them to the parent.
* @private
* @returns {*}
*/
async duplicateComposees(clonedParent, composees = []) {
let idMappings = [];
let allComposeesDuplicated = composees.reduce(async (previousPromise, nextComposee) => {
await previousPromise;
let clonedComposee = await this.duplicateObject(nextComposee);
if (clonedComposee) {
idMappings.push({
newId: clonedComposee.identifier,
oldId: nextComposee.identifier
});
this.composeChild(clonedComposee, clonedParent, clonedComposee !== nextComposee);
}
return;
}, Promise.resolve());
await allComposeesDuplicated;
clonedParent = this.rewriteIdentifiers(clonedParent, idMappings);
this.clones.push(clonedParent);
return clonedParent;
}
/**
* Update identifiers in a cloned object model (or part of
* a cloned object model) to reflect new identifiers after
* duplicating.
* @private
*/
rewriteIdentifiers(clonedParent, childIdMappings) {
for (let { newId, oldId } of childIdMappings) {
let newIdKeyString = this.openmct.objects.makeKeyString(newId);
let oldIdKeyString = this.openmct.objects.makeKeyString(oldId);
// regex replace keystrings
clonedParent = JSON.stringify(clonedParent).replace(new RegExp(oldIdKeyString, 'g'), newIdKeyString);
// parse reviver to replace identifiers
clonedParent = JSON.parse(clonedParent, (key, value) => {
if (Object.prototype.hasOwnProperty.call(value, 'key')
&& Object.prototype.hasOwnProperty.call(value, 'namespace')
&& value.key === oldId.key
&& value.namespace === oldId.namespace) {
return newId;
} else {
return value;
}
});
}
return clonedParent;
}
composeChild(child, parent, setLocation) {
parent.composition.push(child.identifier);
//If a location is not specified, set it.
if (setLocation && child.location === undefined) {
let parentKeyString = this.getKeyString(parent);
child.location = parentKeyString;
}
}
getTypeDefinition(domainObject, definition) {
let typeDefinitions = this.openmct.types.get(domainObject.type).definition;
return typeDefinitions[definition] || false;
}
cloneObjectModel(domainObject) {
let clone = JSON.parse(JSON.stringify(domainObject));
let identifier = {
key: uuid(),
namespace: this.namespace // set to NEW parent's namespace
};
if (clone.modified || clone.persisted || clone.location) {
clone.modified = undefined;
clone.persisted = undefined;
clone.location = undefined;
delete clone.modified;
delete clone.persisted;
delete clone.location;
}
if (clone.composition) {
clone.composition = [];
}
clone.identifier = identifier;
return clone;
}
getKeyString(domainObject) {
return this.openmct.objects.makeKeyString(domainObject.identifier);
}
isCreatable(domainObject) {
return this.getTypeDefinition(domainObject, 'creatable');
}
}

View File

@@ -1,9 +1,9 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* Open MCT, Copyright (c) 2014-2019, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
@@ -14,31 +14,15 @@
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import DuplicateAction from "./DuplicateAction";
define(["./LocalClock"], function (LocalClock) {
describe("The LocalClock class", function () {
let clock;
let mockTimeout;
const timeoutHandle = {};
beforeEach(function () {
mockTimeout = jasmine.createSpy("timeout");
mockTimeout.and.returnValue(timeoutHandle);
clock = new LocalClock(0);
clock.start();
});
it("calls listeners on tick with current time", function () {
const mockListener = jasmine.createSpy("listener");
clock.on('tick', mockListener);
clock.tick();
expect(mockListener).toHaveBeenCalledWith(jasmine.any(Number));
});
});
});
export default function () {
return function (openmct) {
openmct.actions.register(new DuplicateAction(openmct));
};
}

View File

@@ -0,0 +1,157 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import DuplicateActionPlugin from './plugin.js';
import DuplicateAction from './DuplicateAction.js';
import DuplicateTask from './DuplicateTask.js';
import {
createOpenMct,
resetApplicationState,
getMockObjects
} from 'utils/testing';
describe("The Duplicate Action plugin", () => {
let openmct;
let duplicateTask;
let childObject;
let parentObject;
let anotherParentObject;
// this setups up the app
beforeEach((done) => {
openmct = createOpenMct();
childObject = getMockObjects({
objectKeyStrings: ['folder'],
overwrite: {
folder: {
name: "Child Folder",
identifier: {
namespace: "",
key: "child-folder-object"
}
}
}
}).folder;
parentObject = getMockObjects({
objectKeyStrings: ['folder'],
overwrite: {
folder: {
name: "Parent Folder",
composition: [childObject.identifier]
}
}
}).folder;
anotherParentObject = getMockObjects({
objectKeyStrings: ['folder'],
overwrite: {
folder: {
name: "Another Parent Folder"
}
}
}).folder;
let objectGet = openmct.objects.get.bind(openmct.objects);
spyOn(openmct.objects, 'get').and.callFake((identifier) => {
let obj = [childObject, parentObject, anotherParentObject].find((ob) => ob.identifier.key === identifier.key);
if (!obj) {
// not one of the mocked objs, callthrough basically
return objectGet(identifier);
}
return Promise.resolve(obj);
});
spyOn(openmct.composition, 'get').and.callFake((domainObject) => {
return {
load: async () => {
let obj = [childObject, parentObject, anotherParentObject].find((ob) => ob.identifier.key === domainObject.identifier.key);
let children = [];
if (obj) {
for (let i = 0; i < obj.composition.length; i++) {
children.push(await openmct.objects.get(obj.composition[i]));
}
}
return Promise.resolve(children);
},
add: (child) => {
domainObject.composition.push(child.identifier);
}
};
});
// already installed by default, but never hurts, just adds to context menu
openmct.install(DuplicateActionPlugin());
openmct.on('start', done);
openmct.startHeadless();
});
afterEach(() => {
resetApplicationState(openmct);
});
it("should be defined", () => {
expect(DuplicateActionPlugin).toBeDefined();
});
describe("when moving an object to a new parent", () => {
beforeEach(async (done) => {
duplicateTask = new DuplicateTask(openmct);
await duplicateTask.duplicate(parentObject, anotherParentObject);
done();
});
it("the duplicate child object's name (when not changing) should be the same as the original object", async () => {
let duplicatedObjectIdentifier = anotherParentObject.composition[0];
let duplicatedObject = await openmct.objects.get(duplicatedObjectIdentifier);
let duplicateObjectName = duplicatedObject.name;
expect(duplicateObjectName).toEqual(parentObject.name);
});
it("the duplicate child object's identifier should be new", () => {
let duplicatedObjectIdentifier = anotherParentObject.composition[0];
expect(duplicatedObjectIdentifier.key).not.toEqual(parentObject.identifier.key);
});
});
describe("when a new name is provided for the duplicated object", () => {
const NEW_NAME = 'New Name';
beforeEach(() => {
duplicateTask = new DuplicateAction(openmct);
duplicateTask.updateNameCheck(parentObject, NEW_NAME);
});
it("the name is updated", () => {
let childName = parentObject.name;
expect(childName).toEqual(NEW_NAME);
});
});
});

View File

@@ -27,7 +27,7 @@
</div>
<div class="c-grid-item__controls">
<div class="is-status__indicator"
title="This item is missing or suspect"
:title="`This item is ${status}`"
></div>
<div
class="icon-people"

View File

@@ -16,7 +16,7 @@
:class="item.type.cssClass"
>
<span class="is-status__indicator"
title="This item is missing or suspect"
:title="`This item is ${status}`"
></span>
</div>
<div class="c-object-label__name c-list-item__name__name">{{ item.model.name }}</div>

View File

@@ -1,121 +0,0 @@
/******************************* GRID ITEMS */
.c-grid-item {
// Mobile-first
@include button($bg: $colorItemBg, $fg: $colorItemFg);
cursor: pointer;
display: flex;
padding: $interiorMarginLg;
&__type-icon {
filter: $colorKeyFilter;
flex: 0 0 $gridItemMobile;
font-size: floor($gridItemMobile / 2);
margin-right: $interiorMarginLg;
}
&.is-alias {
// Object is an alias to an original.
[class*='__type-icon'] {
@include isAlias();
color: $colorIconAliasForKeyFilter;
}
}
&__details {
display: flex;
flex-flow: column nowrap;
flex: 1 1 auto;
}
&__name {
@include ellipsize();
color: $colorItemFg;
@include headerFont(1.2em);
margin-bottom: $interiorMarginSm;
}
&__metadata {
color: $colorItemFgDetails;
font-size: 0.9em;
body.mobile & {
[class*='__item-count'] {
&:before {
content: ' - ';
}
}
}
}
&__controls {
color: $colorItemFgDetails;
flex: 0 0 64px;
font-size: 1.2em;
display: flex;
align-items: center;
justify-content: flex-end;
> * + * {
margin-left: $interiorMargin;
}
}
body.desktop & {
$transOutMs: 300ms;
flex-flow: column nowrap;
transition: background $transOutMs ease-in-out;
&:hover {
background: $colorItemBgHov;
transition: $transIn;
.c-grid-item__type-icon {
filter: $colorKeyFilterHov;
transform: scale(1);
transition: $transInBounce;
}
}
> * {
margin: 0; // Reset from mobile
}
&__controls {
align-items: start;
flex: 0 0 auto;
order: 1;
.c-info-button,
.c-pointer-icon { display: none; }
}
&__type-icon {
flex: 1 1 auto;
font-size: floor($gridItemDesk / 3);
margin: $interiorMargin 22.5% $interiorMargin * 3 22.5%;
order: 2;
transform: scale(0.9);
transform-origin: center;
transition: all $transOutMs ease-in-out;
}
&__details {
flex: 0 0 auto;
justify-content: flex-end;
order: 3;
}
&__metadata {
display: flex;
&__type {
flex: 1 1 auto;
@include ellipsize();
}
&__item-count {
opacity: 0.7;
flex: 0 0 auto;
}
}
}
}

View File

@@ -43,18 +43,20 @@
}
}
&.is-status--missing {
@include isMissing();
&.is-status--notebook-default {
.is-status__indicator {
display: block;
[class*='__type-icon'],
[class*='__details'] {
opacity: $opacityMissing;
&:before {
color: $colorFilter;
content: $glyph-icon-notebook-page;
font-family: symbolsfont;
}
}
}
&.is-status--suspect {
@include isSuspect();
&[class*='is-status--missing'],
&[class*='is-status--suspect']{
[class*='__type-icon'],
[class*='__details'] {
opacity: $opacityMissing;

View File

@@ -36,7 +36,7 @@
<div class="c-imagery__main-image__bg"
:class="{'paused unnsynced': isPaused,'stale':false }"
>
<div class="c-imagery__main-image__image"
<div class="c-imagery__main-image__image js-imageryView-image"
:style="{
'background-image': imageUrl ? `url(${imageUrl})` : 'none',
'filter': `brightness(${filters.brightness}%) contrast(${filters.contrast}%)`
@@ -137,7 +137,8 @@ export default {
refreshCSS: false,
keyString: undefined,
focusedImageIndex: undefined,
numericDuration: undefined
numericDuration: undefined,
telemetryCollection: undefined
};
},
computed: {
@@ -222,6 +223,7 @@ export default {
// kickoff
this.subscribe();
this.requestHistory();
this.requestTelemetry();
},
updated() {
this.scrollToRight();
@@ -353,6 +355,15 @@ export default {
this.requestHistory();
}
},
async requestTelemetry() {
this.telemetryCollection = await this.openmct.telemetry.requestTelemetryCollection(this.domainObject);
this.telemetryCollection.on('add', (data) => {
console.log('added data', data);
});
this.telemetryCollection.on('remove', (data) => {
console.log('removed data', data);
});
},
async requestHistory() {
let bounds = this.openmct.time.bounds();
this.requestCount++;

View File

@@ -0,0 +1,306 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import ImageryPlugin from './plugin.js';
import Vue from 'vue';
import {
createOpenMct,
resetApplicationState,
simulateKeyEvent
} from 'utils/testing';
const ONE_MINUTE = 1000 * 60;
const TEN_MINUTES = ONE_MINUTE * 10;
const MAIN_IMAGE_CLASS = '.js-imageryView-image';
const NEW_IMAGE_CLASS = '.c-imagery__age.c-imagery--new';
const REFRESH_CSS_MS = 500;
function getImageInfo(doc) {
let imageElement = doc.querySelectorAll(MAIN_IMAGE_CLASS)[0];
let timestamp = imageElement.dataset.openmctImageTimestamp;
let identifier = imageElement.dataset.openmctObjectKeystring;
let url = imageElement.style.backgroundImage;
return {
timestamp,
identifier,
url
};
}
function isNew(doc) {
let newIcon = doc.querySelectorAll(NEW_IMAGE_CLASS);
return newIcon.length !== 0;
}
function generateTelemetry(start, count) {
let telemetry = [];
for (let i = 1, l = count + 1; i < l; i++) {
let stringRep = i + 'minute';
let logo = 'images/logo-openmct.svg';
telemetry.push({
"name": stringRep + " Imagery",
"utc": start + (i * ONE_MINUTE),
"url": location.host + '/' + logo + '?time=' + stringRep,
"timeId": stringRep
});
}
return telemetry;
}
describe("The Imagery View Layout", () => {
const imageryKey = 'example.imagery';
const START = Date.now();
const COUNT = 10;
let openmct;
let imageryPlugin;
let parent;
let child;
let timeFormat = 'utc';
let bounds = {
start: START - TEN_MINUTES,
end: START
};
let imageTelemetry = generateTelemetry(START - TEN_MINUTES, COUNT);
let imageryObject = {
identifier: {
namespace: "",
key: "imageryId"
},
name: "Example Imagery",
type: "example.imagery",
location: "parentId",
modified: 0,
persisted: 0,
telemetry: {
values: [
{
"name": "Image",
"key": "url",
"format": "image",
"hints": {
"image": 1,
"priority": 3
},
"source": "url"
},
{
"name": "Name",
"key": "name",
"source": "name",
"hints": {
"priority": 0
}
},
{
"name": "Time",
"key": "utc",
"format": "utc",
"hints": {
"domain": 2,
"priority": 1
},
"source": "utc"
},
{
"name": "Local Time",
"key": "local",
"format": "local-format",
"hints": {
"domain": 1,
"priority": 2
},
"source": "local"
}
]
}
};
// this setups up the app
beforeEach((done) => {
const appHolder = document.createElement('div');
appHolder.style.width = '640px';
appHolder.style.height = '480px';
openmct = createOpenMct();
parent = document.createElement('div');
child = document.createElement('div');
parent.appendChild(child);
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([]));
imageryPlugin = new ImageryPlugin();
openmct.install(imageryPlugin);
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({}));
openmct.time.timeSystem(timeFormat, {
start: 0,
end: 4
});
openmct.on('start', done);
openmct.startHeadless(appHolder);
});
afterEach(() => {
return resetApplicationState(openmct);
});
it("should provide an imagery view only for imagery producing objects", () => {
let applicableViews = openmct.objectViews.get(imageryObject);
let imageryView = applicableViews.find(
viewProvider => viewProvider.key === imageryKey
);
expect(imageryView).toBeDefined();
});
describe("imagery view", () => {
let applicableViews;
let imageryViewProvider;
let imageryView;
beforeEach(async (done) => {
let telemetryRequestResolve;
let telemetryRequestPromise = new Promise((resolve) => {
telemetryRequestResolve = resolve;
});
openmct.telemetry.request.and.callFake(() => {
telemetryRequestResolve(imageTelemetry);
return telemetryRequestPromise;
});
openmct.time.clock('local', {
start: bounds.start,
end: bounds.end + 100
});
applicableViews = openmct.objectViews.get(imageryObject);
imageryViewProvider = applicableViews.find(viewProvider => viewProvider.key === imageryKey);
imageryView = imageryViewProvider.view(imageryObject);
imageryView.show(child);
await telemetryRequestPromise;
await Vue.nextTick();
return done();
});
it("on mount should show the the most recent image", () => {
const imageInfo = getImageInfo(parent);
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 1].timeId)).not.toEqual(-1);
});
it("should show the clicked thumbnail as the main image", async () => {
const target = imageTelemetry[5].url;
parent.querySelectorAll(`img[src='${target}']`)[0].click();
await Vue.nextTick();
const imageInfo = getImageInfo(parent);
expect(imageInfo.url.indexOf(imageTelemetry[5].timeId)).not.toEqual(-1);
});
it("should show that an image is new", async (done) => {
await Vue.nextTick();
// used in code, need to wait to the 500ms here too
setTimeout(() => {
const imageIsNew = isNew(parent);
expect(imageIsNew).toBeTrue();
done();
}, REFRESH_CSS_MS);
});
it("should show that an image is not new", async (done) => {
const target = imageTelemetry[2].url;
parent.querySelectorAll(`img[src='${target}']`)[0].click();
await Vue.nextTick();
// used in code, need to wait to the 500ms here too
setTimeout(() => {
const imageIsNew = isNew(parent);
expect(imageIsNew).toBeFalse();
done();
}, REFRESH_CSS_MS);
});
it("should navigate via arrow keys", async () => {
let keyOpts = {
element: parent.querySelector('.c-imagery'),
key: 'ArrowLeft',
keyCode: 37,
type: 'keyup'
};
simulateKeyEvent(keyOpts);
await Vue.nextTick();
const imageInfo = getImageInfo(parent);
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 2].timeId)).not.toEqual(-1);
});
it("should navigate via numerous arrow keys", async () => {
let element = parent.querySelector('.c-imagery');
let type = 'keyup';
let leftKeyOpts = {
element,
type,
key: 'ArrowLeft',
keyCode: 37
};
let rightKeyOpts = {
element,
type,
key: 'ArrowRight',
keyCode: 39
};
// left thrice
simulateKeyEvent(leftKeyOpts);
simulateKeyEvent(leftKeyOpts);
simulateKeyEvent(leftKeyOpts);
// right once
simulateKeyEvent(rightKeyOpts);
await Vue.nextTick();
const imageInfo = getImageInfo(parent);
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 3].timeId)).not.toEqual(-1);
});
});
});

View File

@@ -20,40 +20,21 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(
['./AbstractComposeAction'],
function (AbstractComposeAction) {
/**
* The MoveAction is available from context menus and allows a user to
* move an object to another location of their choosing.
*
* @implements {Action}
* @constructor
* @memberof platform/entanglement
*/
function MoveAction(policyService, locationService, moveService, context) {
AbstractComposeAction.apply(
this,
[policyService, locationService, moveService, context, "Move"]
);
}
MoveAction.prototype = Object.create(AbstractComposeAction.prototype);
MoveAction.appliesTo = function (context) {
var applicableObject =
context.selectedObject || context.domainObject;
if (applicableObject && applicableObject.model.locked) {
return false;
export default function MissingObjectInterceptor(openmct) {
openmct.objects.addGetInterceptor({
appliesTo: (identifier, domainObject) => {
return identifier.key !== 'mine';
},
invoke: (identifier, object) => {
if (object === undefined) {
return {
identifier,
type: 'unknown',
name: 'Missing: ' + openmct.objects.makeKeyString(identifier)
};
}
return Boolean(applicableObject
&& applicableObject.hasCapability('context'));
};
return MoveAction;
}
);
return object;
}
});
}

View File

@@ -20,44 +20,24 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([], function () {
export default function MyItemsInterceptor(openmct) {
/**
* Disallow moves when either the parent or the child are not
* modifiable by users.
* @constructor
* @implements {Policy}
* @memberof platform/entanglement
*/
function MovePolicy() {
}
openmct.objects.addGetInterceptor({
appliesTo: (identifier, domainObject) => {
return identifier.key === 'mine';
},
invoke: (identifier, object) => {
if (object === undefined) {
return {
identifier,
"name": "My Items",
"type": "folder",
"composition": [],
"location": "ROOT"
};
}
function parentOf(domainObject) {
var context = domainObject.getCapability('context');
return context && context.getParent();
}
function allowMutation(domainObject) {
var type = domainObject && domainObject.getCapability('type');
return Boolean(type && type.hasFeature('creation'));
}
function selectedObject(context) {
return context.selectedObject || context.domainObject;
}
MovePolicy.prototype.allow = function (action, context) {
var key = action.getMetadata().key;
if (key === 'move') {
return allowMutation(selectedObject(context))
&& allowMutation(parentOf(selectedObject(context)));
return object;
}
return true;
};
return MovePolicy;
});
});
}

View File

@@ -0,0 +1,9 @@
import missingObjectInterceptor from "./missingObjectInterceptor";
import myItemsInterceptor from "./myItemsInterceptor";
export default function plugin() {
return function install(openmct) {
myItemsInterceptor(openmct);
missingObjectInterceptor(openmct);
};
}

View File

@@ -0,0 +1,114 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import {
createOpenMct,
resetApplicationState
} from 'utils/testing';
describe("The local time", () => {
const LOCAL_FORMAT_KEY = 'local-format';
const LOCAL_SYSTEM_KEY = 'local';
const JUNK = "junk";
const TIMESTAMP = -14256000000;
const DATESTRING = '1969-07-20 12:00:00.000 am';
let openmct;
beforeEach((done) => {
openmct = createOpenMct();
openmct.install(openmct.plugins.LocalTimeSystem());
openmct.on('start', done);
openmct.startHeadless();
});
afterEach(() => {
return resetApplicationState(openmct);
});
describe("system", function () {
let localTimeSystem;
beforeEach(() => {
localTimeSystem = openmct.time.timeSystem(LOCAL_SYSTEM_KEY, {
start: 0,
end: 4
});
});
it("is installed", () => {
let timeSystems = openmct.time.getAllTimeSystems();
let local = timeSystems.find(ts => ts.key === LOCAL_SYSTEM_KEY);
expect(local).not.toEqual(-1);
});
it("can be set to be the main time system", () => {
expect(openmct.time.timeSystem().key).toBe(LOCAL_SYSTEM_KEY);
});
it("uses the local-format time format", () => {
expect(localTimeSystem.timeFormat).toBe(LOCAL_FORMAT_KEY);
});
it("is UTC based", () => {
expect(localTimeSystem.isUTCBased).toBe(true);
});
it("defines expected metadata", () => {
expect(localTimeSystem.key).toBe(LOCAL_SYSTEM_KEY);
expect(localTimeSystem.name).toBeDefined();
expect(localTimeSystem.cssClass).toBeDefined();
expect(localTimeSystem.durationFormat).toBeDefined();
});
});
describe("formatter can be obtained from the telemetry API and", () => {
let localTimeFormatter;
let dateString;
let timeStamp;
beforeEach(() => {
localTimeFormatter = openmct.telemetry.getFormatter(LOCAL_FORMAT_KEY);
dateString = localTimeFormatter.format(TIMESTAMP);
timeStamp = localTimeFormatter.parse(DATESTRING);
});
it("will format a timestamp in local time format", () => {
expect(localTimeFormatter.format(TIMESTAMP)).toBe(dateString);
});
it("will parse an local time Date String into milliseconds", () => {
expect(localTimeFormatter.parse(DATESTRING)).toBe(timeStamp);
});
it("will validate correctly", () => {
expect(localTimeFormatter.validate(DATESTRING)).toBe(true);
expect(localTimeFormatter.validate(JUNK)).toBe(false);
});
});
});

View File

@@ -0,0 +1,169 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
export default class MoveAction {
constructor(openmct) {
this.name = 'Move';
this.key = 'move';
this.description = 'Move this object from its containing object to another object.';
this.cssClass = "icon-move";
this.group = "action";
this.priority = 7;
this.openmct = openmct;
}
async invoke(objectPath) {
let object = objectPath[0];
let inNavigationPath = this.inNavigationPath(object);
let oldParent = objectPath[1];
let dialogService = this.openmct.$injector.get('dialogService');
let dialogForm = this.getDialogForm(object, oldParent);
let userInput = await dialogService.getUserInput(dialogForm, { name: object.name });
// if we need to update name
if (object.name !== userInput.name) {
this.openmct.objects.mutate(object, 'name', userInput.name);
}
let parentContext = userInput.location.getCapability('context');
let newParent = await this.openmct.objects.get(parentContext.domainObject.id);
if (inNavigationPath && this.openmct.editor.isEditing()) {
this.openmct.editor.save();
}
this.addToNewParent(object, newParent);
this.removeFromOldParent(oldParent, object);
if (inNavigationPath) {
let newObjectPath = await this.openmct.objects.getOriginalPath(object.identifier);
let root = await this.openmct.objects.getRoot();
let rootChildCount = root.composition.length;
// if not multiple root children, remove root from path
if (rootChildCount < 2) {
newObjectPath.pop(); // remove ROOT
}
this.navigateTo(newObjectPath);
}
}
inNavigationPath(object) {
return this.openmct.router.path
.some(objectInPath => this.openmct.objects.areIdsEqual(objectInPath.identifier, object.identifier));
}
navigateTo(objectPath) {
let urlPath = objectPath.reverse()
.map(object => this.openmct.objects.makeKeyString(object.identifier))
.join("/");
window.location.href = '#/browse/' + urlPath;
}
addToNewParent(child, newParent) {
let newParentKeyString = this.openmct.objects.makeKeyString(newParent.identifier);
let compositionCollection = this.openmct.composition.get(newParent);
this.openmct.objects.mutate(child, 'location', newParentKeyString);
compositionCollection.add(child);
}
removeFromOldParent(parent, child) {
let compositionCollection = this.openmct.composition.get(parent);
compositionCollection.remove(child);
}
getDialogForm(object, parent) {
return {
name: "Move Item",
sections: [
{
rows: [
{
key: "name",
control: "textfield",
name: "Folder Name",
pattern: "\\S+",
required: true,
cssClass: "l-input-lg"
},
{
name: "location",
control: "locator",
validate: this.validate(object, parent),
key: 'location'
}
]
}
]
};
}
validate(object, currentParent) {
return (parentCandidate) => {
let currentParentKeystring = this.openmct.objects.makeKeyString(currentParent.identifier);
let parentCandidateKeystring = this.openmct.objects.makeKeyString(parentCandidate.getId());
let objectKeystring = this.openmct.objects.makeKeyString(object.identifier);
if (!parentCandidateKeystring || !currentParentKeystring) {
return false;
}
if (parentCandidateKeystring === currentParentKeystring) {
return false;
}
if (parentCandidateKeystring === objectKeystring) {
return false;
}
if (parentCandidate.getModel().composition.indexOf(objectKeystring) !== -1) {
return false;
}
return this.openmct.composition.checkPolicy(
parentCandidate.useCapability('adapter'),
object
);
};
}
appliesTo(objectPath) {
let parent = objectPath[1];
let parentType = parent && this.openmct.types.get(parent.type);
let child = objectPath[0];
let childType = child && this.openmct.types.get(child.type);
if (child.locked || (parent && parent.locked)) {
return false;
}
return parentType
&& parentType.definition.creatable
&& childType
&& childType.definition.creatable
&& Array.isArray(parent.composition);
}
}

View File

@@ -0,0 +1,28 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import MoveAction from "./MoveAction";
export default function () {
return function (openmct) {
openmct.actions.register(new MoveAction(openmct));
};
}

View File

@@ -0,0 +1,110 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import MoveActionPlugin from './plugin.js';
import MoveAction from './MoveAction.js';
import {
createOpenMct,
resetApplicationState,
getMockObjects
} from 'utils/testing';
describe("The Move Action plugin", () => {
let openmct;
let moveAction;
let childObject;
let parentObject;
let anotherParentObject;
// this setups up the app
beforeEach((done) => {
const appHolder = document.createElement('div');
appHolder.style.width = '640px';
appHolder.style.height = '480px';
openmct = createOpenMct();
childObject = getMockObjects({
objectKeyStrings: ['folder'],
overwrite: {
folder: {
name: "Child Folder",
identifier: {
namespace: "",
key: "child-folder-object"
}
}
}
}).folder;
parentObject = getMockObjects({
objectKeyStrings: ['folder'],
overwrite: {
folder: {
name: "Parent Folder",
composition: [childObject.identifier]
}
}
}).folder;
anotherParentObject = getMockObjects({
objectKeyStrings: ['folder'],
overwrite: {
folder: {
name: "Another Parent Folder"
}
}
}).folder;
// already installed by default, but never hurts, just adds to context menu
openmct.install(MoveActionPlugin());
openmct.on('start', done);
openmct.startHeadless(appHolder);
});
afterEach(() => {
resetApplicationState(openmct);
});
it("should be defined", () => {
expect(MoveActionPlugin).toBeDefined();
});
describe("when moving an object to a new parent and removing from the old parent", () => {
beforeEach(() => {
moveAction = new MoveAction(openmct);
moveAction.addToNewParent(childObject, anotherParentObject);
moveAction.removeFromOldParent(parentObject, childObject);
});
it("the child object's identifier should be in the new parent's composition", () => {
let newParentChild = anotherParentObject.composition[0];
expect(newParentChild).toEqual(childObject.identifier);
});
it("the child object's identifier should be removed from the old parent's composition", () => {
let oldParentComposition = parentObject.composition;
expect(oldParentComposition.length).toEqual(0);
});
});
});

View File

@@ -6,11 +6,11 @@ export default class CopyToNotebookAction {
this.openmct = openmct;
this.cssClass = 'icon-duplicate';
this.description = 'Copy to Notebook action';
this.description = 'Copy value to notebook as an entry';
this.group = "action";
this.key = 'copyToNotebook';
this.name = 'Copy to Notebook';
this.priority = 9;
this.priority = 1;
}
copyToNotebook(entryText) {
@@ -25,15 +25,16 @@ export default class CopyToNotebookAction {
});
}
invoke(objectPath, viewContext) {
invoke(objectPath, view = {}) {
let viewContext = view.getViewContext && view.getViewContext();
this.copyToNotebook(viewContext.formattedValueForCopy());
}
appliesTo(objectPath, viewContext) {
if (viewContext && viewContext.getViewKey) {
return viewContext.getViewKey().includes('alphanumeric-format');
}
appliesTo(objectPath, view = {}) {
let viewContext = view.getViewContext && view.getViewContext();
return false;
return viewContext && viewContext.formattedValueForCopy
&& typeof viewContext.formattedValueForCopy === 'function';
}
}

View File

@@ -24,12 +24,12 @@
:default-section-id="defaultSectionId"
:domain-object="internalDomainObject"
:page-title="internalDomainObject.configuration.pageTitle"
:pages="pages"
:section-title="internalDomainObject.configuration.sectionTitle"
:sections="sections"
:selected-section="selectedSection"
:sidebar-covers-entries="sidebarCoversEntries"
@updatePage="updatePage"
@updateSection="updateSection"
@pagesChanged="pagesChanged"
@sectionsChanged="sectionsChanged"
@toggleNav="toggleNav"
/>
<div class="c-notebook__page-view">
@@ -111,7 +111,7 @@ import Search from '@/ui/components/search.vue';
import SearchResults from './SearchResults.vue';
import Sidebar from './Sidebar.vue';
import { clearDefaultNotebook, getDefaultNotebook, setDefaultNotebook, setDefaultNotebookSection, setDefaultNotebookPage } from '../utils/notebook-storage';
import { addNotebookEntry, createNewEmbed, getNotebookEntries } from '../utils/notebook-entries';
import { addNotebookEntry, createNewEmbed, getNotebookEntries, mutateObject } from '../utils/notebook-entries';
import objectUtils from 'objectUtils';
import { throttle } from 'lodash';
@@ -220,7 +220,7 @@ export default {
return s;
});
this.updateSection({ sections });
this.sectionsChanged({ sections });
this.throttledSearchItem('');
},
createNotebookStorageObject() {
@@ -309,7 +309,7 @@ export default {
return null;
}
return this.openmct.objects.get(oldNotebookStorage.notebookMeta.identifier).then(d => d);
return this.openmct.objects.get(oldNotebookStorage.notebookMeta.identifier);
},
getPage(section, id) {
return section.pages.find(p => p.id === id);
@@ -379,9 +379,6 @@ export default {
return this.sections.find(section => section.isSelected);
},
mutateObject(key, value) {
this.openmct.objects.mutate(this.internalDomainObject, key, value);
},
navigateToSectionPage() {
const { pageId, sectionId } = this.openmct.router.getParams();
if (!pageId || !sectionId) {
@@ -398,7 +395,7 @@ export default {
return s;
});
this.updateSection({ sections });
this.sectionsChanged({ sections });
},
newEntry(embed = null) {
this.search = '';
@@ -411,6 +408,24 @@ export default {
orientationChange() {
this.formatSidebar();
},
pagesChanged({ pages = [], id = null}) {
const selectedSection = this.getSelectedSection();
if (!selectedSection) {
return;
}
selectedSection.pages = pages;
const sections = this.sections.map(section => {
if (section.id === selectedSection.id) {
section = selectedSection;
}
return section;
});
this.sectionsChanged({ sections });
this.updateDefaultNotebookPage(pages, id);
},
removeDefaultClass(domainObject) {
if (!domainObject) {
return;
@@ -433,12 +448,14 @@ export default {
setDefaultNotebook(this.openmct, notebookStorage);
}
if (this.defaultSectionId.length === 0 || this.defaultSectionId !== notebookStorage.section.id) {
if (this.defaultSectionId && this.defaultSectionId.length === 0 || this.defaultSectionId !== notebookStorage.section.id) {
this.defaultSectionId = notebookStorage.section.id;
setDefaultNotebookSection(notebookStorage.section);
}
if (this.defaultPageId.length === 0 || this.defaultPageId !== notebookStorage.page.id) {
if (this.defaultPageId && this.defaultPageId.length === 0 || this.defaultPageId !== notebookStorage.page.id) {
this.defaultPageId = notebookStorage.page.id;
setDefaultNotebookPage(notebookStorage.page);
}
},
updateDefaultNotebookPage(pages, id) {
@@ -502,29 +519,11 @@ export default {
const notebookEntries = configuration.entries || {};
notebookEntries[this.selectedSection.id][this.selectedPage.id] = entries;
this.mutateObject('configuration.entries', notebookEntries);
mutateObject(this.openmct, this.internalDomainObject, 'configuration.entries', notebookEntries);
},
updateInternalDomainObject(domainObject) {
this.internalDomainObject = domainObject;
},
updatePage({ pages = [], id = null}) {
const selectedSection = this.getSelectedSection();
if (!selectedSection) {
return;
}
selectedSection.pages = pages;
const sections = this.sections.map(section => {
if (section.id === selectedSection.id) {
section = selectedSection;
}
return section;
});
this.updateSection({ sections });
this.updateDefaultNotebookPage(pages, id);
},
updateParams(sections) {
const selectedSection = sections.find(s => s.isSelected);
if (!selectedSection) {
@@ -548,8 +547,8 @@ export default {
pageId
});
},
updateSection({ sections, id = null }) {
this.mutateObject('configuration.sections', sections);
sectionsChanged({ sections, id = null }) {
mutateObject(this.openmct, this.internalDomainObject, 'configuration.sections', sections);
this.updateParams(sections);
this.updateDefaultNotebookSection(sections, id);

View File

@@ -118,7 +118,7 @@ export default {
painterroInstance.show(this.embed.snapshot.src);
},
changeLocation() {
const link = this.embed.historicLink;
const hash = this.embed.historicLink;
const bounds = this.openmct.time.bounds();
const isTimeBoundChanged = this.embed.bounds.start !== bounds.start
@@ -143,8 +143,9 @@ export default {
this.openmct.notifications.alert(message);
}
const url = new URL(link);
window.location.href = url.hash;
const relativeHash = hash.slice(hash.indexOf('#'));
const url = new URL(relativeHash, `${location.protocol}//${location.host}${location.pathname}`);
window.location.hash = url.hash;
},
formatTime(unixTime, timeFormat) {
return Moment.utc(unixTime).format(timeFormat);

View File

@@ -17,7 +17,7 @@
<script>
import Snapshot from '../snapshot';
import { getDefaultNotebook } from '../utils/notebook-storage';
import { getDefaultNotebook, validateNotebookStorageObject } from '../utils/notebook-storage';
import { NOTEBOOK_DEFAULT, NOTEBOOK_SNAPSHOT } from '../notebook-constants';
export default {
@@ -49,6 +49,8 @@ export default {
};
},
mounted() {
validateNotebookStorageObject();
this.notebookSnapshot = new Snapshot(this.openmct);
this.setDefaultNotebookStatus();
},
@@ -86,7 +88,7 @@ export default {
this.openmct.menus.showMenu(x, y, notebookTypes);
},
snapshot(notebook) {
snapshot(notebookType) {
this.$nextTick(() => {
const element = document.querySelector('.c-overlay__contents')
|| document.getElementsByClassName('l-shell__main-container')[0];
@@ -104,7 +106,7 @@ export default {
openmct: this.openmct
};
this.notebookSnapshot.capture(snapshotMeta, notebook.type, element);
this.notebookSnapshot.capture(snapshotMeta, notebookType, element);
});
},
setDefaultNotebookStatus() {

View File

@@ -66,14 +66,10 @@ export default {
}
}
},
data() {
return {
};
},
methods: {
deletePage(id) {
const selectedSection = this.sections.find(s => s.isSelected);
const page = this.pages.filter(p => p.id !== id);
const page = this.pages.find(p => p.id !== id);
deleteNotebookEntries(this.openmct, this.domainObject, selectedSection, page);
const selectedPage = this.pages.find(p => p.isSelected);

View File

@@ -53,10 +53,6 @@ export default {
}
}
},
data() {
return {
};
},
methods: {
deleteSection(id) {
const section = this.sections.find(s => s.id === id);

View File

@@ -18,7 +18,7 @@
:domain-object="domainObject"
:sections="sections"
:section-title="sectionTitle"
@updateSection="updateSection"
@updateSection="sectionsChanged"
/>
</div>
</div>
@@ -48,7 +48,7 @@
:sidebar-covers-entries="sidebarCoversEntries"
:page-title="pageTitle"
@toggleNav="toggleNav"
@updatePage="updatePage"
@updatePage="pagesChanged"
/>
</div>
</div>
@@ -85,13 +85,6 @@ export default {
return {};
}
},
pages: {
type: Array,
required: true,
default() {
return [];
}
},
pageTitle: {
type: String,
default() {
@@ -122,9 +115,16 @@ export default {
return {
};
},
computed: {
pages() {
const selectedSection = this.sections.find(section => section.isSelected);
return selectedSection && selectedSection.pages || [];
}
},
watch: {
pages(newpages) {
if (!newpages.length) {
pages(newPages) {
if (!newPages.length) {
this.addPage();
}
},
@@ -141,55 +141,79 @@ export default {
},
methods: {
addPage() {
const newPage = this.createNewPage();
const pages = this.addNewPage(newPage);
this.pagesChanged({
pages,
id: newPage.id
});
},
addSection() {
const newSection = this.createNewSection();
const sections = this.addNewSection(newSection);
this.sectionsChanged({
sections,
id: newSection.id
});
},
addNewPage(page) {
const pages = this.pages.map(p => {
p.isSelected = false;
return p;
});
return pages.concat(page);
},
addNewSection(section) {
const sections = this.sections.map(s => {
s.isSelected = false;
return s;
});
return sections.concat(section);
},
createNewPage() {
const pageTitle = this.pageTitle;
const id = uuid();
const page = {
return {
id,
isDefault: false,
isSelected: true,
name: `Unnamed ${pageTitle}`,
pageTitle
};
this.pages.forEach(p => p.isSelected = false);
const pages = this.pages.concat(page);
this.updatePage({
pages,
id
});
},
addSection() {
createNewSection() {
const sectionTitle = this.sectionTitle;
const id = uuid();
const section = {
const page = this.createNewPage();
const pages = [page];
return {
id,
isDefault: false,
isSelected: true,
name: `Unnamed ${sectionTitle}`,
pages: [],
pages,
sectionTitle
};
this.sections.forEach(s => s.isSelected = false);
const sections = this.sections.concat(section);
this.updateSection({
sections,
id
});
},
toggleNav() {
this.$emit('toggleNav');
},
updatePage({ pages, id }) {
this.$emit('updatePage', {
pagesChanged({ pages, id }) {
this.$emit('pagesChanged', {
pages,
id
});
},
updateSection({ sections, id }) {
this.$emit('updateSection', {
sectionsChanged({ sections, id }) {
this.$emit('sectionsChanged', {
sections,
id
});

View File

@@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { createOpenMct, resetApplicationState } from 'utils/testing';
import { createOpenMct, createMouseEvent, resetApplicationState } from 'utils/testing';
import NotebookPlugin from './plugin';
import Vue from 'vue';
@@ -133,4 +133,89 @@ describe("Notebook plugin:", () => {
expect(hasMajorElements).toBe(true);
});
});
describe("Notebook Snapshots view:", () => {
let snapshotIndicator;
let drawerElement;
function clickSnapshotIndicator() {
const indicator = element.querySelector('.icon-camera');
const button = indicator.querySelector('button');
const clickEvent = createMouseEvent('click');
button.dispatchEvent(clickEvent);
}
beforeAll(() => {
snapshotIndicator = openmct.indicators.indicatorObjects
.find(indicator => indicator.key === 'notebook-snapshot-indicator').element;
element.append(snapshotIndicator);
return Vue.nextTick();
});
afterAll(() => {
snapshotIndicator.remove();
snapshotIndicator = undefined;
if (drawerElement) {
drawerElement.remove();
drawerElement = undefined;
}
});
beforeEach(() => {
drawerElement = document.querySelector('.l-shell__drawer');
});
afterEach(() => {
if (drawerElement) {
drawerElement.classList.remove('is-expanded');
}
});
it("has Snapshots indicator", () => {
const hasSnapshotIndicator = snapshotIndicator !== null && snapshotIndicator !== undefined;
expect(hasSnapshotIndicator).toBe(true);
});
it("snapshots container has class isExpanded", () => {
let classes = drawerElement.classList;
const isExpandedBefore = classes.contains('is-expanded');
clickSnapshotIndicator();
classes = drawerElement.classList;
const isExpandedAfterFirstClick = classes.contains('is-expanded');
expect(isExpandedBefore).toBeFalse();
expect(isExpandedAfterFirstClick).toBeTrue();
});
it("snapshots container does not have class isExpanded", () => {
let classes = drawerElement.classList;
const isExpandedBefore = classes.contains('is-expanded');
clickSnapshotIndicator();
classes = drawerElement.classList;
const isExpandedAfterFirstClick = classes.contains('is-expanded');
clickSnapshotIndicator();
classes = drawerElement.classList;
const isExpandedAfterSecondClick = classes.contains('is-expanded');
expect(isExpandedBefore).toBeFalse();
expect(isExpandedAfterFirstClick).toBeTrue();
expect(isExpandedAfterSecondClick).toBeFalse();
});
it("show notebook snapshots container text", () => {
clickSnapshotIndicator();
const notebookSnapshots = drawerElement.querySelector('.l-browse-bar__object-name');
const snapshotsText = notebookSnapshots.textContent.trim();
expect(snapshotsText).toBe('Notebook Snapshots');
});
});
});

View File

@@ -7,15 +7,14 @@ export default class Snapshot {
constructor(openmct) {
this.openmct = openmct;
this.snapshotContainer = new SnapshotContainer(openmct);
this.exportImageService = openmct.$injector.get('exportImageService');
this.dialogService = openmct.$injector.get('dialogService');
this.capture = this.capture.bind(this);
this._saveSnapShot = this._saveSnapShot.bind(this);
}
capture(snapshotMeta, notebookType, domElement) {
this.exportImageService.exportPNGtoSRC(domElement, 's-status-taking-snapshot')
const exportImageService = this.openmct.$injector.get('exportImageService');
exportImageService.exportPNGtoSRC(domElement, 's-status-taking-snapshot')
.then(function (blob) {
const reader = new window.FileReader();
reader.readAsDataURL(blob);

View File

@@ -8,6 +8,29 @@ const TIME_BOUNDS = {
END_DELTA: 'tc.endDelta'
};
export function addEntryIntoPage(notebookStorage, entries, entry) {
const defaultSection = notebookStorage.section;
const defaultPage = notebookStorage.page;
if (!defaultSection || !defaultPage) {
return;
}
const newEntries = JSON.parse(JSON.stringify(entries));
let section = newEntries[defaultSection.id];
if (!section) {
newEntries[defaultSection.id] = {};
}
let page = newEntries[defaultSection.id][defaultPage.id];
if (!page) {
newEntries[defaultSection.id][defaultPage.id] = [];
}
newEntries[defaultSection.id][defaultPage.id].push(entry);
return newEntries;
}
export function getHistoricLinkInFixedMode(openmct, bounds, historicLink) {
if (historicLink.includes('tc.mode=fixed')) {
return historicLink;
@@ -38,35 +61,6 @@ export function getHistoricLinkInFixedMode(openmct, bounds, historicLink) {
return params.join('&');
}
export function getNotebookDefaultEntries(notebookStorage, domainObject) {
if (!notebookStorage || !domainObject) {
return null;
}
const defaultSection = notebookStorage.section;
const defaultPage = notebookStorage.page;
if (!defaultSection || !defaultPage) {
return null;
}
const configuration = domainObject.configuration;
const entries = configuration.entries || {};
let section = entries[defaultSection.id];
if (!section) {
section = {};
entries[defaultSection.id] = section;
}
let page = entries[defaultSection.id][defaultPage.id];
if (!page) {
page = [];
entries[defaultSection.id][defaultPage.id] = [];
}
return entries[defaultSection.id][defaultPage.id];
}
export function createNewEmbed(snapshotMeta, snapshot = '') {
const {
bounds,
@@ -120,24 +114,25 @@ export function addNotebookEntry(openmct, domainObject, notebookStorage, embed =
? [embed]
: [];
const defaultEntries = getNotebookDefaultEntries(notebookStorage, domainObject);
const id = `entry-${date}`;
defaultEntries.push({
const entry = {
id,
createdOn: date,
text: entryText,
embeds
});
};
const newEntries = addEntryIntoPage(notebookStorage, entries, entry);
addDefaultClass(domainObject, openmct);
openmct.objects.mutate(domainObject, 'configuration.entries', entries);
openmct.objects.mutate(domainObject, 'configuration.entries', newEntries);
return id;
}
export function getNotebookEntries(domainObject, selectedSection, selectedPage) {
if (!domainObject || !selectedSection || !selectedPage) {
return null;
return;
}
const configuration = domainObject.configuration;
@@ -145,12 +140,12 @@ export function getNotebookEntries(domainObject, selectedSection, selectedPage)
let section = entries[selectedSection.id];
if (!section) {
return null;
return;
}
let page = entries[selectedSection.id][selectedPage.id];
if (!page) {
return null;
return;
}
return entries[selectedSection.id][selectedPage.id];
@@ -196,7 +191,11 @@ export function deleteNotebookEntries(openmct, domainObject, selectedSection, se
delete entries[selectedSection.id][selectedPage.id];
openmct.objects.mutate(domainObject, 'configuration.entries', entries);
mutateObject(openmct, domainObject, 'configuration.entries', entries);
}
export function mutateObject(openmct, object, key, value) {
openmct.objects.mutate(object, key, value);
}
function addDefaultClass(domainObject, openmct) {

View File

@@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import * as NotebookEntries from './notebook-entries';
import { createOpenMct, spyOnBuiltins, resetApplicationState } from 'utils/testing';
import { createOpenMct, resetApplicationState } from 'utils/testing';
const notebookStorage = {
domainObject: {
@@ -121,7 +121,6 @@ describe('Notebook Entries:', () => {
beforeEach(done => {
openmct = createOpenMct();
window.localStorage.setItem('notebook-storage', null);
spyOnBuiltins(['mutate'], openmct.objects);
done();
});
@@ -137,24 +136,16 @@ describe('Notebook Entries:', () => {
expect(entries.length).toEqual(0);
});
it('addNotebookEntry mutates object', () => {
it('addNotebookEntry adds entry', (done) => {
const unlisten = openmct.objects.observe(notebookDomainObject, '*', (object) => {
const entries = NotebookEntries.getNotebookEntries(notebookDomainObject, selectedSection, selectedPage);
expect(entries.length).toEqual(1);
done();
unlisten();
});
NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage);
expect(openmct.objects.mutate).toHaveBeenCalled();
});
it('addNotebookEntry adds entry', () => {
NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage);
const entries = NotebookEntries.getNotebookEntries(notebookDomainObject, selectedSection, selectedPage);
expect(entries.length).toEqual(1);
});
it('getEntryPosById returns valid position', () => {
const entryId = NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage);
const position = NotebookEntries.getEntryPosById(entryId, notebookDomainObject, selectedSection, selectedPage);
expect(position).toEqual(0);
});
it('getEntryPosById returns valid position', () => {
@@ -174,22 +165,13 @@ describe('Notebook Entries:', () => {
expect(success).toBe(true);
});
it('deleteNotebookEntries mutates object', () => {
openmct.objects.mutate.calls.reset();
NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage);
NotebookEntries.deleteNotebookEntries(openmct, notebookDomainObject, selectedSection, selectedPage);
expect(openmct.objects.mutate).toHaveBeenCalledTimes(2);
});
it('deleteNotebookEntries deletes correct entry', () => {
it('deleteNotebookEntries deletes correct page entries', () => {
NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage);
NotebookEntries.addNotebookEntry(openmct, notebookDomainObject, notebookStorage);
NotebookEntries.deleteNotebookEntries(openmct, notebookDomainObject, selectedSection, selectedPage);
const afterEntries = NotebookEntries.getNotebookEntries(notebookDomainObject, selectedSection, selectedPage);
expect(afterEntries).toEqual(null);
expect(afterEntries).toEqual(undefined);
});
});

View File

@@ -67,3 +67,24 @@ export function setDefaultNotebookPage(page) {
notebookStorage.page = page;
saveDefaultNotebook(notebookStorage);
}
export function validateNotebookStorageObject() {
const notebookStorage = getDefaultNotebook();
let valid = false;
if (notebookStorage) {
Object.entries(notebookStorage).forEach(([key, value]) => {
const validKey = key !== undefined && key !== null;
const validValue = value !== undefined && value !== null;
valid = validKey && validValue;
});
}
if (valid) {
return notebookStorage;
}
console.warn('Invalid Notebook object, clearing default notebook storage');
clearDefaultNotebook();
}

View File

@@ -87,7 +87,8 @@ export default class CouchObjectProvider {
}
//Sometimes CouchDB returns the old rev which fetching the object if there is a document update in progress
if (!this.objectQueue[key].pending) {
//Only update the rev if it's the first time we're getting the object from CouchDB. Subsequent revs should only be updated by updates.
if (!this.objectQueue[key].pending && !this.objectQueue[key].rev) {
this.objectQueue[key].updateRevision(response[REV]);
}

View File

@@ -171,6 +171,7 @@ define([
* Update yAxis format, values, and label from known series.
*/
updateFromSeries: function (series) {
this.unset('displayRange');
const plotModel = this.plot.get('domainObject');
const label = _.get(plotModel, 'configuration.yAxis.label');
const sampleSeries = series.first();

View File

@@ -117,8 +117,10 @@ define(
* @returns {promise}
*/
ExportImageService.prototype.exportJPG = function (element, filename, className) {
const processedFilename = replaceDotsWithUnderscores(filename);
return this.renderElement(element, "jpg", className).then(function (img) {
saveAs(img, filename);
saveAs(img, processedFilename);
});
};
@@ -130,8 +132,10 @@ define(
* @returns {promise}
*/
ExportImageService.prototype.exportPNG = function (element, filename, className) {
const processedFilename = replaceDotsWithUnderscores(filename);
return this.renderElement(element, "png", className).then(function (img) {
saveAs(img, filename);
saveAs(img, processedFilename);
});
};
@@ -146,6 +150,12 @@ define(
return this.renderElement(element, "png", className);
};
function replaceDotsWithUnderscores(filename) {
const regex = /\./gi;
return filename.replace(regex, '_');
}
/**
* canvas.toBlob() not supported in IE < 10, Opera, and Safari. This polyfill
* implements the method in browsers that would not otherwise support it.

View File

@@ -39,10 +39,11 @@ define([], function () {
const thisRequest = {
pending: 0
};
currentRequest = thisRequest;
$scope.currentRequest = thisRequest;
const telemetryObjects = $scope.telemetryObjects = [];
const thisTickWidthMap = {};
currentRequest = thisRequest;
$scope.currentRequest = thisRequest;
tickWidthMap = thisTickWidthMap;
if (unlisten) {
@@ -52,14 +53,10 @@ define([], function () {
function addChild(child) {
const id = openmct.objects.makeKeyString(child.identifier);
const legacyObject = openmct.legacyObject(child);
thisTickWidthMap[id] = 0;
thisRequest.pending += 1;
objectService.getObjects([id])
.then(function (objects) {
thisRequest.pending -= 1;
const childObj = objects[id];
telemetryObjects.push(childObj);
});
telemetryObjects.push(legacyObject);
}
function removeChild(childIdentifier) {
@@ -84,6 +81,7 @@ define([], function () {
}
thisRequest.pending += 1;
openmct.objects.get(domainObject.getId())
.then(function (obj) {
thisRequest.pending -= 1;

View File

@@ -59,7 +59,8 @@ define([
'./persistence/couch/plugin',
'./defaultRootName/plugin',
'./timeline/plugin',
'./viewDatumAction/plugin'
'./viewDatumAction/plugin',
'./interceptors/plugin'
], function (
_,
UTCTimeSystem,
@@ -99,7 +100,8 @@ define([
CouchDBPlugin,
DefaultRootName,
Timeline,
ViewDatumAction
ViewDatumAction,
ObjectInterceptors
) {
const bundleMap = {
LocalStorage: 'platform/persistence/local',
@@ -194,6 +196,7 @@ define([
plugins.DefaultRootName = DefaultRootName.default;
plugins.Timeline = Timeline.default;
plugins.ViewDatumAction = ViewDatumAction.default;
plugins.ObjectInterceptors = ObjectInterceptors.default;
return plugins;
});

View File

@@ -20,8 +20,8 @@
Drag objects here to add them to this view.
</div>
<div
v-for="(tab,index) in tabsList"
:key="index"
v-for="(tab, index) in tabsList"
:key="tab.keyString"
class="c-tab c-tabs-view__tab"
:class="{
'is-current': isCurrent(tab)
@@ -29,13 +29,13 @@
@click="showTab(tab, index)"
>
<div class="c-tabs-view__tab__label c-object-label"
:class="[tab.status ? `is-${tab.status}` : '']"
:class="[tab.status ? `is-status--${tab.status}` : '']"
>
<div class="c-object-label__type-icon"
:class="tab.type.definition.cssClass"
>
<span class="is-status__indicator"
title="This item is missing or suspect"
:title="`This item is ${tab.status}`"
></span>
</div>
<span class="c-button__label c-object-label__name">{{ tab.domainObject.name }}</span>
@@ -47,8 +47,8 @@
</div>
</div>
<div
v-for="(tab, index) in tabsList"
:key="index"
v-for="tab in tabsList"
:key="tab.keyString"
class="c-tabs-view__object-holder"
:class="{'c-tabs-view__object-holder--hidden': !isCurrent(tab)}"
>
@@ -56,6 +56,7 @@
v-if="internalDomainObject.keep_alive ? currentTab : isCurrent(tab)"
class="c-tabs-view__object"
:object="tab.domainObject"
:object-path="tab.objectPath"
/>
</div>
</div>
@@ -78,7 +79,7 @@ const unknownObjectType = {
};
export default {
inject: ['openmct', 'domainObject', 'composition'],
inject: ['openmct', 'domainObject', 'composition', 'objectPath'],
components: {
ObjectView
},
@@ -139,6 +140,10 @@ export default {
this.composition.off('remove', this.removeItem);
this.composition.off('reorder', this.onReorder);
this.tabsList.forEach(tab => {
tab.statusUnsubscribe();
});
this.unsubscribe();
this.clearCurrentTabIndexFromURL();
@@ -192,12 +197,19 @@ export default {
},
addItem(domainObject) {
let type = this.openmct.types.get(domainObject.type) || unknownObjectType;
let keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
let status = this.openmct.status.get(domainObject.identifier);
let statusUnsubscribe = this.openmct.status.observe(keyString, (updatedStatus) => {
this.updateStatus(keyString, updatedStatus);
});
let objectPath = [domainObject].concat(this.objectPath.slice());
let tabItem = {
domainObject,
status,
type: type,
key: this.openmct.objects.makeKeyString(domainObject.identifier)
statusUnsubscribe,
objectPath,
type,
keyString
};
this.tabsList.push(tabItem);
@@ -213,10 +225,12 @@ export default {
},
removeItem(identifier) {
let pos = this.tabsList.findIndex(tab =>
tab.domainObject.identifier.namespace === identifier.namespace && tab.domainObject.identifier.key === identifier.key
tab.domainObject.identifier.namespace === identifier.namespace && tab.domainObject.identifier.keyString === identifier.keyString
);
let tabToBeRemoved = this.tabsList[pos];
tabToBeRemoved.statusUnsubscribe();
this.tabsList.splice(pos, 1);
if (this.isCurrent(tabToBeRemoved)) {
@@ -254,7 +268,7 @@ export default {
this.allowDrop = false;
},
isCurrent(tab) {
return this.currentTab.key === tab.key;
return this.currentTab.keyString === tab.keyString;
},
updateInternalDomainObject(domainObject) {
this.internalDomainObject = domainObject;
@@ -272,6 +286,16 @@ export default {
},
clearCurrentTabIndexFromURL() {
deleteSearchParam(this.searchTabKey);
},
updateStatus(keyString, status) {
let tabPos = this.tabsList.findIndex((tab) => {
return tab.keyString === keyString;
});
if (tabPos !== -1) {
let tab = this.tabsList[tabPos];
this.$set(tab, 'status', status);
}
}
}
};

View File

@@ -38,7 +38,7 @@ define([
canEdit: function (domainObject) {
return domainObject.type === 'tabs';
},
view: function (domainObject) {
view: function (domainObject, objectPath) {
let component;
return {
@@ -56,6 +56,7 @@ define([
provide: {
openmct,
domainObject,
objectPath,
composition: openmct.composition.get(domainObject)
},
template: '<tabs-component :isEditing="isEditing"></tabs-component>'

View File

@@ -30,13 +30,13 @@ let exportCSV = {
},
group: 'view'
};
let exportMarkedRows = {
let exportMarkedDataAsCSV = {
name: 'Export Marked Rows',
key: 'export-csv-marked',
description: "Export marked rows as CSV",
cssClass: 'icon-download labeled',
invoke: (objectPath, viewProvider) => {
viewProvider.getViewContext().exportMarkedRows();
viewProvider.getViewContext().exportMarkedDataAsCSV();
},
group: 'view'
};
@@ -98,7 +98,7 @@ let autosizeColumns = {
let viewActions = [
exportCSV,
exportMarkedRows,
exportMarkedDataAsCSV,
unmarkAllRows,
pause,
play,

View File

@@ -108,8 +108,7 @@ export default {
return {
viewHistoricalData: true,
viewDatumAction: true,
getDatum: this.getDatum,
skipCache: true
getDatum: this.getDatum
};
}
}
@@ -192,7 +191,8 @@ export default {
let contextualObjectPath = this.objectPath.slice();
contextualObjectPath.unshift(domainObject);
let allActions = this.openmct.actions.get(contextualObjectPath, this.actionsViewContext);
let actionsCollection = this.openmct.actions.get(contextualObjectPath, this.actionsViewContext);
let allActions = actionsCollection.getActionsObject();
let applicableActions = this.row.getContextMenuActions().map(key => allActions[key]);
if (applicableActions.length) {

View File

@@ -960,7 +960,7 @@ export default {
return {
type: 'telemetry-table',
exportAllDataAsCSV: this.exportAllDataAsCSV,
exportMarkedRows: this.exportMarkedRows,
exportMarkedDataAsCSV: this.exportMarkedDataAsCSV,
unmarkAllRows: this.unmarkAllRows,
togglePauseByButton: this.togglePauseByButton,
expandColumns: this.recalculateColumnWidths,

View File

@@ -61,13 +61,21 @@ export default {
this.openmct.time.on("timeSystem", this.setScaleAndPlotActivities);
this.openmct.time.on("bounds", this.updateViewBounds);
this.resizeTimer = setInterval(this.resize, RESIZE_POLL_INTERVAL);
this.unlisten = this.openmct.objects.observe(this.domainObject, '*', this.observeForChanges);
},
destroyed() {
clearInterval(this.resizeTimer);
this.openmct.time.off("timeSystem", this.setScaleAndPlotActivities);
this.openmct.time.off("bounds", this.updateViewBounds);
if (this.unlisten) {
this.unlisten();
}
},
methods: {
observeForChanges(mutatedObject) {
this.validateJSON(mutatedObject.selectFile.body);
this.setScaleAndPlotActivities();
},
resize() {
if (this.$refs.axisHolder.clientWidth !== this.width) {
this.setDimensions();
@@ -200,7 +208,7 @@ export default {
return 0;
},
// Get the row where the next activity will land.
getRowForActivity(rectX, width, defaultActivityRow = 0) {
getRowForActivity(rectX, width, minimumActivityRow = 0) {
let currentRow;
let sortedActivityRows = Object.keys(this.activitiesByRow).sort(this.sortFn);
@@ -216,17 +224,18 @@ export default {
for (let i = 0; i < sortedActivityRows.length; i++) {
let row = sortedActivityRows[i];
if (getOverlap(this.activitiesByRow[row])) {
if (row >= minimumActivityRow && getOverlap(this.activitiesByRow[row])) {
currentRow = row;
break;
}
}
if (currentRow === undefined && sortedActivityRows.length) {
currentRow = parseInt(sortedActivityRows[sortedActivityRows.length - 1], 10) + ROW_HEIGHT + ROW_PADDING;
let row = Math.max(parseInt(sortedActivityRows[sortedActivityRows.length - 1], 10), minimumActivityRow);
currentRow = row + ROW_HEIGHT + ROW_PADDING;
}
return (currentRow || defaultActivityRow);
return (currentRow || minimumActivityRow);
},
calculatePlanLayout() {
this.activitiesByRow = {};
@@ -236,8 +245,9 @@ export default {
let groups = Object.keys(this.json);
groups.forEach((key, index) => {
let activities = this.json[key];
//set the currentRow to the beginning of the next logical row
currentRow = currentRow + ROW_HEIGHT * index;
//set the new group's first row. It should be greater than the largest row of the last group
let sortedActivityRows = Object.keys(this.activitiesByRow).sort(this.sortFn);
const groupRowStart = sortedActivityRows.length ? parseInt(sortedActivityRows[sortedActivityRows.length - 1], 10) + 1 : 0;
let newGroup = true;
activities.forEach((activity) => {
if (this.isActivityInBounds(activity)) {
@@ -256,9 +266,9 @@ export default {
const textWidth = textStart + this.getTextWidth(textLines[0]) + TEXT_LEFT_PADDING;
if (activityNameFitsRect) {
currentRow = this.getRowForActivity(rectX, rectWidth);
currentRow = this.getRowForActivity(rectX, rectWidth, groupRowStart);
} else {
currentRow = this.getRowForActivity(rectX, textWidth);
currentRow = this.getRowForActivity(rectX, textWidth, groupRowStart);
}
let textY = parseInt(currentRow, 10) + (activityNameFitsRect ? INNER_TEXT_PADDING : OUTER_TEXT_PADDING);

View File

@@ -98,6 +98,10 @@ describe('the plugin', function () {
beforeEach((done) => {
planDomainObject = {
identifier: {
key: 'test-object',
namespace: ''
},
type: 'plan',
id: "test-object",
selectFile: {

View File

@@ -1,48 +0,0 @@
/*****************************************************************************
* Open MCT Web, Copyright (c) 2014-2015, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT Web is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT Web includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(['./UTCTimeSystem'], function (UTCTimeSystem) {
describe("The UTCTimeSystem class", function () {
let timeSystem;
let mockTimeout;
beforeEach(function () {
mockTimeout = jasmine.createSpy("timeout");
timeSystem = new UTCTimeSystem(mockTimeout);
});
it("Uses the UTC time format", function () {
expect(timeSystem.timeFormat).toBe('utc');
});
it("is UTC based", function () {
expect(timeSystem.isUTCBased).toBe(true);
});
it("defines expected metadata", function () {
expect(timeSystem.key).toBeDefined();
expect(timeSystem.name).toBeDefined();
expect(timeSystem.cssClass).toBeDefined();
expect(timeSystem.durationFormat).toBeDefined();
});
});
});

View File

@@ -0,0 +1,103 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import LocalClock from './LocalClock.js';
import UTCTimeSystem from './UTCTimeSystem';
import {
createOpenMct,
resetApplicationState
} from 'utils/testing';
describe("The UTC Time System", () => {
const UTC_SYSTEM_AND_FORMAT_KEY = 'utc';
let openmct;
let utcTimeSystem;
let mockTimeout;
beforeEach(() => {
openmct = createOpenMct();
openmct.install(openmct.plugins.UTCTimeSystem());
});
afterEach(() => {
return resetApplicationState(openmct);
});
describe("plugin", function () {
beforeEach(function () {
mockTimeout = jasmine.createSpy("timeout");
utcTimeSystem = new UTCTimeSystem(mockTimeout);
});
it("is installed", () => {
let timeSystems = openmct.time.getAllTimeSystems();
let utc = timeSystems.find(ts => ts.key === UTC_SYSTEM_AND_FORMAT_KEY);
expect(utc).not.toEqual(-1);
});
it("can be set to be the main time system", () => {
openmct.time.timeSystem(UTC_SYSTEM_AND_FORMAT_KEY, {
start: 0,
end: 4
});
expect(openmct.time.timeSystem().key).toBe(UTC_SYSTEM_AND_FORMAT_KEY);
});
it("uses the utc time format", () => {
expect(utcTimeSystem.timeFormat).toBe(UTC_SYSTEM_AND_FORMAT_KEY);
});
it("is UTC based", () => {
expect(utcTimeSystem.isUTCBased).toBe(true);
});
it("defines expected metadata", () => {
expect(utcTimeSystem.key).toBe(UTC_SYSTEM_AND_FORMAT_KEY);
expect(utcTimeSystem.name).toBeDefined();
expect(utcTimeSystem.cssClass).toBeDefined();
expect(utcTimeSystem.durationFormat).toBeDefined();
});
});
describe("LocalClock class", function () {
let clock;
const timeoutHandle = {};
beforeEach(function () {
mockTimeout = jasmine.createSpy("timeout");
mockTimeout.and.returnValue(timeoutHandle);
clock = new LocalClock(0);
clock.start();
});
it("calls listeners on tick with current time", function () {
const mockListener = jasmine.createSpy("listener");
clock.on('tick', mockListener);
clock.tick();
expect(mockListener).toHaveBeenCalledWith(jasmine.any(Number));
});
});
});

View File

@@ -56,7 +56,7 @@ export default class ViewDatumAction {
});
}
appliesTo(objectPath, view = {}) {
let viewContext = view.getViewContext && view.getViewContext() || {};
let viewContext = (view.getViewContext && view.getViewContext()) || {};
let datum = viewContext.getDatum;
let enabled = viewContext.viewDatumAction;

View File

@@ -70,11 +70,8 @@ define(
if (multiSelect) {
this.handleMultiSelect(selectable);
} else {
this.setSelectionStyles(selectable);
this.selected = [selectable];
this.handleSingleSelect(selectable);
}
this.emit('change', this.selected);
};
/**
@@ -87,6 +84,20 @@ define(
this.addSelectionAttributes(selectable);
this.selected.push(selectable);
}
this.emit('change', this.selected);
};
/**
* @private
*/
Selection.prototype.handleSingleSelect = function (selectable) {
if (!_.isEqual([selectable], this.selected)) {
this.setSelectionStyles(selectable);
this.selected = [selectable];
this.emit('change', this.selected);
}
};
/**

View File

@@ -126,7 +126,7 @@ button {
.c-icon-button {
[class*='label'] {
opacity: 0.6;
opacity: 0.8;
}
&--mixed {
@@ -468,9 +468,6 @@ select {
> * {
flex: 0 0 auto;
//+ * {
// margin-top: $interiorMarginSm;
//}
}
}
@@ -485,7 +482,7 @@ select {
transition: $transIn;
white-space: nowrap;
&:hover {
@include hover {
background: $colorMenuHovBg;
color: $colorMenuHovFg;
&:before {
@@ -500,8 +497,12 @@ select {
min-width: 1em;
}
&:not([class]):before {
content: ''; // Add this element so that menu items without an icon still indent properly
&:not([class*='icon']):before {
content: ''; // Enable :before so that menu items without an icon still indent properly
}
.menus-no-icon & {
&:before { display: none; }
}
}
}
@@ -734,7 +735,6 @@ select {
.c-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
> * {
// First level items

View File

@@ -47,6 +47,7 @@ mct-plot {
.c-plot,
.gl-plot {
overflow: hidden;
min-height: 100px;
.s-status-taking-snapshot & {
.c-control-bar {
@@ -60,13 +61,7 @@ mct-plot {
/*********************** MISSING ITEM INDICATORS */
.is-status__indicator {
display: none;
}
.is-status--missing {
@include isMissing();
.is-status__indicator {
font-size: 0.8em;
}
font-size: 0.8em;
}
}
@@ -85,7 +80,8 @@ mct-plot {
display: flex;
flex: 1 1 auto;
flex-direction: column;
overflow: hidden;
overflow-y: auto;
overflow-x: hidden;
}
&--stacked {

View File

@@ -129,44 +129,25 @@
}
}
@mixin isStatus($absPos: false) {
@mixin isStatus($absPos: false, $glyph: '', $color: $colorBodyFg) {
// Supports CSS classing as follows:
// is-status--missing, is-status--suspect, etc.
// Common styles to be applied to tree items, object labels, grid and list item views
.is-status__indicator {
display: none ;
display: block ; // Set to display: none in status.scss
text-shadow: $colorBodyBg 0 0 2px;
font-family: symbolsfont;
&[class^='is-status'] .is-status__indicator,
[class^='is-status'] .is-status__indicator {
display: block !important;
}
@if $absPos {
display: block;
position: absolute;
z-index: 3;
}
}
}
@mixin isMissing($absPos: false) {
@include isStatus($absPos);
.is-status__indicator:before {
color: $colorAlert;
content: $glyph-icon-alert-triangle;
}
}
@mixin isSuspect($absPos: false) {
@include isStatus($absPos);
.is-status__indicator:before {
color: $colorWarningLo;
content: $glyph-icon-alert-rect;
&:before {
color: $color;
content: $glyph;
}
}
}
@@ -571,7 +552,8 @@
}
}
&[class*='--menus-left'] {
&[class*='--menus-left'],
&[class*='menus-to-left'] {
.c-menu {
left: auto; right: 0;
}

View File

@@ -198,3 +198,17 @@ tr {
.u-alert { @include uIndicator($colorAlert, $colorAlertFg, $glyph-icon-alert-triangle); }
.u-error { @include uIndicator($colorError, $colorErrorFg, $glyph-icon-alert-triangle); }
.is-status {
&__indicator {
display: none; // Default state; is set to block when within an actual is-status class
}
&--missing {
@include isStatus($glyph: $glyph-icon-alert-triangle, $color: $colorAlert);
}
&--suspect {
@include isStatus($glyph: $glyph-icon-alert-rect, $color: $colorWarningLo);
}
}

View File

@@ -213,17 +213,16 @@
}
}
.is-notebook-default {
.is-notebook-default,
.is-status--notebook-default {
&:after {
color: $colorFilter;
content: $glyph-icon-notebook-page;
display: block;
font-family: symbolsfont;
font-size: 0.9em;
margin-left: $interiorMargin;
}
&.c-list__item:after {
content: $glyph-icon-notebook-page;
flex: 1 0 auto;
text-align: right;
}

View File

@@ -23,11 +23,11 @@
<div
class="c-so-view"
:class="[
statusClass,
'c-so-view--' + domainObject.type,
{
'c-so-view--no-frame': !hasFrame,
'has-complex-content': complexContent,
'is-missing': domainObject.status === 'missing'
'has-complex-content': complexContent
}
]"
>
@@ -85,9 +85,6 @@
</div>
</div>
<div class="is-status__indicator"
title="This item is missing or suspect"
></div>
<object-view
ref="objectView"
class="c-so-view__object-view"
@@ -96,7 +93,7 @@
:object-path="objectPath"
:layout-font-size="layoutFontSize"
:layout-font="layoutFont"
@change-provider="setViewProvider"
@change-action-collection="setActionCollection"
/>
</div>
</template>
@@ -149,15 +146,10 @@ export default {
let complexContent = !SIMPLE_CONTENT_TYPES.includes(this.domainObject.type);
let viewProvider = {};
let statusBarItems = {};
return {
cssClass,
complexContent,
viewProvider,
statusBarItems,
statusBarItems: [],
status: ''
};
},
@@ -204,6 +196,7 @@ export default {
},
getPreviewHeader() {
const domainObject = this.objectPath[0];
const actionCollection = this.actionCollection;
const preview = new Vue({
components: {
PreviewHeader
@@ -214,10 +207,11 @@ export default {
},
data() {
return {
domainObject
domainObject,
actionCollection
};
},
template: '<PreviewHeader :domainObject="domainObject" :hideViewSwitcher="true" :showNotebookMenuSwitcher="true"></PreviewHeader>'
template: '<PreviewHeader :actionCollection="actionCollection" :domainObject="domainObject" :hideViewSwitcher="true" :showNotebookMenuSwitcher="true"></PreviewHeader>'
});
return preview.$mount().$el;
@@ -225,27 +219,17 @@ export default {
getSelectionContext() {
return this.$refs.objectView.getSelectionContext();
},
setViewProvider(provider) {
this.viewProvider = provider;
this.initializeStatusBarItems();
},
initializeStatusBarItems() {
setActionCollection(actionCollection) {
if (this.actionCollection) {
this.unlistenToActionCollection();
}
if (this.viewProvider) {
this.actionCollection = this.openmct.actions.get(this.objectPath, this.viewProvider);
this.actionCollection.on('update', this.updateActionItems);
this.updateActionItems(this.actionCollection.applicableActions);
} else {
this.statusBarItems = [];
this.menuActionItems = [];
}
this.actionCollection = actionCollection;
this.actionCollection.on('update', this.updateActionItems);
this.updateActionItems(this.actionCollection.applicableActions);
},
unlistenToActionCollection() {
this.actionCollection.off('update', this.updateActionItems);
this.actionCollection.destroy();
delete this.actionCollection;
},
updateActionItems(actionItems) {
@@ -253,15 +237,7 @@ export default {
this.menuActionItems = this.actionCollection.getVisibleActions();
},
showMenuItems(event) {
let actions;
if (this.menuActionItems.length) {
actions = this.menuActionItems;
} else {
actions = this.openmct.actions.get(this.objectPath);
}
let sortedActions = this.openmct.actions._groupAndSortActions(actions);
let sortedActions = this.openmct.actions._groupAndSortActions(this.menuActionItems);
this.openmct.menus.showMenu(event.x, event.y, sortedActions);
},
setStatus(status) {

View File

@@ -42,6 +42,15 @@ export default {
navigateToPath: {
type: String,
default: undefined
},
liteObject: {
type: Boolean,
default: false
},
beforeInteraction: {
type: Function,
required: false,
default: undefined
}
},
data() {
@@ -52,7 +61,8 @@ export default {
},
computed: {
typeClass() {
let type = this.openmct.types.get(this.observedObject.type);
let domainObjectType = this.liteObject ? this.domainObject.type : this.observedObject.type;
let type = this.openmct.types.get(domainObjectType);
if (!type) {
return 'icon-object-unknown';
}
@@ -64,13 +74,15 @@ export default {
}
},
mounted() {
if (this.observedObject) {
// if it's a liteObject nothing to observe
if (this.observedObject && !this.liteObject) {
let removeListener = this.openmct.objects.observe(this.observedObject, '*', (newObject) => {
this.observedObject = newObject;
});
this.$once('hook:destroyed', removeListener);
}
// liteObjects do have identifiers, so statuses can be observed
this.removeStatusListener = this.openmct.status.observe(this.observedObject.identifier, this.setStatus);
this.status = this.openmct.status.get(this.observedObject.identifier);
this.previewAction = new PreviewAction(this.openmct);
@@ -79,35 +91,74 @@ export default {
this.removeStatusListener();
},
methods: {
navigateOrPreview(event) {
async navigateOrPreview(event) {
// skip if editing or is a lite object with an interaction function
if (this.openmct.editor.isEditing() || !(this.liteObject && this.beforeInteraction)) {
return;
}
event.preventDefault();
if (this.openmct.editor.isEditing()) {
event.preventDefault();
this.preview();
} else if (this.liteObject && this.beforeInteraction) {
let fullObjectInfo = await this.getFullObjectInfo();
// need to update when new route functions are merged (back button PR)
window.location.href = '#/browse/' + fullObjectInfo.navigationPath;
}
},
preview() {
if (this.previewAction.appliesTo(this.objectPath)) {
this.previewAction.invoke(this.objectPath);
async preview() {
let objectPath = this.objectPath;
if (this.liteObject && this.beforeInteraction) {
let fullObjectInfo = await this.getFullObjectInfo();
objectPath = fullObjectInfo.objectPath;
}
if (this.previewAction.appliesTo(objectPath)) {
this.previewAction.invoke(objectPath);
}
},
dragStart(event) {
const LITE_DOMAIN_OBJECT_TYPE = "openmct/domain-object-lite";
let navigatedObject = this.openmct.router.path[0];
let serializedPath = JSON.stringify(this.objectPath);
let keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
let serializedPath = JSON.stringify(this.objectPath);
/*
* Cannot inspect data transfer objects on dragover/dragenter so impossible to determine composability at
* that point. If dragged object can be composed by navigated object, then indicate with presence of
* 'composable-domain-object' in data transfer
*/
if (this.openmct.composition.checkPolicy(navigatedObject, this.observedObject)) {
event.dataTransfer.setData("openmct/composable-domain-object", JSON.stringify(this.domainObject));
if (this.liteObject) {
event.dataTransfer.setData(LITE_DOMAIN_OBJECT_TYPE, JSON.stringify(this.domainObject.identifier));
} else {
/*
* Cannot inspect data transfer objects on dragover/dragenter so impossible to determine composability at
* that point. If dragged object can be composed by navigated object, then indicate with presence of
* 'composable-domain-object' in data transfer
*/
if (this.openmct.composition.checkPolicy(navigatedObject, this.observedObject)) {
event.dataTransfer.setData("openmct/composable-domain-object", JSON.stringify(this.domainObject));
}
// serialize domain object anyway, because some views can drag-and-drop objects without composition
// (eg. notabook.)
event.dataTransfer.setData("openmct/domain-object-path", serializedPath);
event.dataTransfer.setData(`openmct/domain-object/${keyString}`, this.domainObject);
}
},
async getFullObjectInfo() {
let fullObjectInfo = await this.beforeInteraction();
let objectPath = fullObjectInfo.objectPath;
let navigationPath = objectPath
.reverse()
.map(object =>
this.openmct.objects.makeKeyString(object.identifier)
).join('/');
// serialize domain object anyway, because some views can drag-and-drop objects without composition
// (eg. notabook.)
event.dataTransfer.setData("openmct/domain-object-path", serializedPath);
event.dataTransfer.setData(`openmct/domain-object/${keyString}`, this.domainObject);
fullObjectInfo.objectPath = objectPath;
fullObjectInfo.navigationPath = navigationPath;
return fullObjectInfo;
},
setStatus(status) {
this.status = status;

View File

@@ -74,6 +74,11 @@ export default {
this.styleRuleManager.destroy();
delete this.styleRuleManager;
}
if (this.actionCollection) {
this.actionCollection.destroy();
delete this.actionCollection;
}
},
created() {
this.debounceUpdateView = _.debounce(this.updateView, 10);
@@ -149,6 +154,7 @@ export default {
let keys = Object.keys(styleObj);
let elemToStyle = this.getStyleReceiver();
keys.forEach(key => {
if (elemToStyle) {
if ((typeof styleObj[key] === 'string') && (styleObj[key].indexOf('__no_value') > -1)) {
@@ -207,6 +213,7 @@ export default {
}
}
this.getActionCollection();
this.currentView.show(this.viewContainer, this.openmct.editor.isEditing());
if (immediatelySelect) {
@@ -214,9 +221,16 @@ export default {
this.$el, this.getSelectionContext(), true);
}
this.$emit('change-provider', this.currentView);
this.openmct.objectViews.on('clearData', this.clearData);
},
getActionCollection() {
if (this.actionCollection) {
this.actionCollection.destroy();
}
this.actionCollection = this.openmct.actions._get(this.currentObjectPath || this.objectPath, this.currentView);
this.$emit('change-action-collection', this.actionCollection);
},
show(object, viewKey, immediatelySelect, currentObjectPath) {
this.updateStyle();

View File

@@ -9,6 +9,7 @@
justify-content: space-between;
align-items: center;
margin-bottom: $interiorMarginSm;
overflow: hidden;
padding: 3px;
.c-object-label {
@@ -35,6 +36,7 @@
/*************************** FRAME CONTROLS */
&__frame-controls {
display: flex;
flex: 0 0 auto;
&__btns,
&__more {
@@ -110,22 +112,8 @@
}
}
&.is-status--missing {
@include isMissing($absPos: true);
.is-status__indicator {
top: $interiorMargin;
left: $interiorMargin;
}
}
&.is-status--suspect {
@include isSuspect($absPos: true);
.is-status__indicator {
top: $interiorMargin;
left: $interiorMargin;
}
&[class*='is-status'] {
border: $borderMissing;
}
}
@@ -158,10 +146,6 @@
border: $borderMissing;
}
&.is-status--suspect {
border: $borderMissing;
}
&__object-view {
display: flex;
flex: 1 1 auto;
@@ -178,4 +162,5 @@
// This element is the recipient for object styling; cannot be display: contents
flex: 1 1 auto;
overflow: hidden;
display: block;
}

View File

@@ -19,30 +19,31 @@
display: block;
flex: 0 0 auto;
font-size: 1.1em;
opacity: $objectLabelTypeIconOpacity;
}
&.is-status--missing {
@include isMissing($absPos: true);
[class*='__type-icon']:before,
[class*='__type-icon']:after{
opacity: $opacityMissing;
}
.is-status__indicator {
right: -3px;
top: -3px;
transform: scale(0.7);
}
.is-status__indicator {
position: absolute;
right: -3px;
top: -3px;
transform: scale(0.5);
}
&.is-status--missing,
&.is-status--suspect {
@include isSuspect($absPos: true);
[class*='__type-icon'] {
&:before,
&:after {
opacity: $opacityMissing;
}
}
}
.is-status__indicator {
right: -3px;
top: -3px;
transform: scale(0.7);
&.is-status--notebook-default {
&:after {
content: $glyph-icon-notebook-page;
display: block;
margin-left: $interiorMargin;
}
}
}

View File

@@ -2,13 +2,13 @@
<div class="c-inspector__header">
<div v-if="!multiSelect"
class="c-inspector__selected c-object-label"
:class="{'is-status--missing': isMissing }"
:class="[statusClass]"
>
<div class="c-object-label__type-icon"
:class="typeCssClass"
>
<span class="is-status__indicator"
title="This item is missing or suspect"
:title="`This item is ${status}`"
></span>
</div>
<span v-if="!singleSelectNonObject"
@@ -37,8 +37,10 @@ export default {
data() {
return {
domainObject: {},
keyString: undefined,
multiSelect: false,
itemsSelected: 0
itemsSelected: 0,
status: undefined
};
},
computed: {
@@ -58,9 +60,8 @@ export default {
singleSelectNonObject() {
return !this.item.identifier && !this.multiSelect;
},
isMissing() {
// safe check this.domainObject since for layout objects this.domainOjbect is undefined
return this.domainObject && this.domainObject.status === 'missing';
statusClass() {
return this.status ? `is-status--${this.status}` : '';
}
},
mounted() {
@@ -69,25 +70,48 @@ export default {
},
beforeDestroy() {
this.openmct.selection.off('change', this.updateSelection);
if (this.statusUnsubscribe) {
this.statusUnsubscribe();
}
},
methods: {
updateSelection(selection) {
if (this.statusUnsubscribe) {
this.statusUnsubscribe();
this.statusUnsubscribe = undefined;
}
if (selection.length === 0 || selection[0].length === 0) {
this.domainObject = {};
this.resetDomainObject();
return;
}
if (selection.length > 1) {
this.multiSelect = true;
this.domainObject = {};
this.itemsSelected = selection.length;
this.resetDomainObject();
return;
} else {
this.multiSelect = false;
this.domainObject = selection[0][0].context.item;
if (this.domainObject) {
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.status = this.openmct.status.get(this.keyString);
this.statusUnsubscribe = this.openmct.status.observe(this.keyString, this.updateStatus);
}
}
},
resetDomainObject() {
this.domainObject = {};
this.status = undefined;
this.keyString = undefined;
},
updateStatus(status) {
this.status = status;
}
}
};

View File

@@ -3,11 +3,13 @@
<toolbar-select-menu
:options="fontSizeMenuOptions"
@change="setFontSize"
class="menus-to-left menus-no-icon"
/>
<div class="c-toolbar__separator"></div>
<toolbar-select-menu
:options="fontMenuOptions"
@change="setFont"
class="menus-to-left menus-no-icon"
/>
</div>
</template>

View File

@@ -15,12 +15,12 @@
:class="type.cssClass"
>
<span class="is-status__indicator"
title="This item is missing or suspect"
:title="`This item is ${status}`"
></span>
</div>
<span
class="l-browse-bar__object-name c-object-label__name c-input-inline"
contenteditable
:contenteditable="type.creatable"
@blur="updateName"
@keydown.enter.prevent
@keyup.enter.prevent="updateNameOnEnterKeyPress"
@@ -130,7 +130,7 @@ export default {
ViewSwitcher
},
props: {
viewProvider: {
actionCollection: {
type: Object,
default: () => {
return {};
@@ -226,20 +226,14 @@ export default {
this.status = this.openmct.status.get(this.domainObject.identifier, this.setStatus);
this.removeStatusListener = this.openmct.status.observe(this.domainObject.identifier, this.setStatus);
},
viewProvider(viewProvider) {
actionCollection(actionCollection) {
if (this.actionCollection) {
this.unlistenToActionCollection();
}
if (viewProvider) {
this.actionCollection = this.openmct.actions.get(this.openmct.router.path, viewProvider);
this.actionCollection.on('update', this.updateActionItems);
this.updateActionItems(this.actionCollection.applicableActions);
} else {
this.statusBarViewKey = undefined;
this.statusBarItems = [];
this.menuActionItems = [];
}
this.actionCollection = actionCollection;
this.actionCollection.on('update', this.updateActionItems);
this.updateActionItems(this.actionCollection.getActionsObject());
}
},
mounted: function () {
@@ -352,20 +346,11 @@ export default {
this.menuActionItems = this.actionCollection.getVisibleActions();
},
showMenuItems(event) {
let actions;
if (this.menuActionItems.length) {
actions = this.menuActionItems;
} else {
actions = this.openmct.actions.get(this.openmct.router.path);
}
let sortedActions = this.openmct.actions._groupAndSortActions(actions);
let sortedActions = this.openmct.actions._groupAndSortActions(this.menuActionItems);
this.openmct.menus.showMenu(event.x, event.y, sortedActions);
},
unlistenToActionCollection() {
this.actionCollection.off('update', this.updateActionItems);
this.actionCollection.destroy();
delete this.actionCollection;
},
toggleLock(flag) {

View File

@@ -70,7 +70,7 @@
<browse-bar
ref="browseBar"
class="l-shell__main-view-browse-bar"
:view-provider="viewProvider"
:action-collection="actionCollection"
@sync-tree-navigation="handleSyncTreeNavigation"
/>
<toolbar
@@ -82,7 +82,7 @@
class="l-shell__main-container"
data-selectable
:show-edit-view="true"
@change-provider="setProvider"
@change-action-collection="setActionCollection"
/>
<component
:is="conductorComponent"
@@ -146,9 +146,9 @@ export default {
conductorComponent: undefined,
isEditing: false,
hasToolbar: false,
viewProvider: undefined,
headExpanded,
triggerSync: false
actionCollection: undefined,
triggerSync: false,
headExpanded
};
},
computed: {
@@ -223,8 +223,8 @@ export default {
this.hasToolbar = structure.length > 0;
},
setProvider(provider) {
this.viewProvider = provider;
setActionCollection(actionCollection) {
this.actionCollection = actionCollection;
},
handleSyncTreeNavigation() {
this.triggerSync = !this.triggerSync;

View File

@@ -279,10 +279,12 @@
}
&__toolbar {
// Toolbar in the main shell, used by Display Layouts
$p: $interiorMargin;
background: $editUIBaseColor;
border-radius: $basicCr;
height: $p + 24px; // Need to standardize the height
justify-content: space-between;
padding: $p;
}
}

View File

@@ -98,7 +98,11 @@
:style="scrollableStyles()"
@scroll="scrollItems"
>
<div :style="{ height: childrenHeight + 'px' }">
<!-- Regular Tree Items -->
<div
v-if="!activeSearch"
:style="{ height: childrenHeight + 'px' }"
>
<tree-item
v-for="(treeItem, index) in visibleItems"
:key="treeItem.id"
@@ -108,7 +112,32 @@
:item-index="index"
:item-height="itemHeight"
:virtual-scroll="true"
:show-down="activeSearch ? false : true"
:show-down="true"
@expanded="beginNavigationRequest('handleExpanded', treeItem)"
/>
<div
v-if="showNoItems"
:style="indicatorLeftOffset"
class="c-tree__item c-tree__item--empty"
>
No items
</div>
</div>
<!-- Search Result Items (Index Only) -->
<div
v-if="activeSearch"
:style="{ height: childrenHeight + 'px' }"
>
<tree-item-lite
v-for="(treeItem, index) in visibleItems"
:key="treeItem.id"
:node="treeItem"
:left-offset="itemLeftOffset"
:item-offset="itemOffset"
:item-index="index"
:item-height="itemHeight"
:virtual-scroll="true"
:show-down="false"
@expanded="beginNavigationRequest('handleExpanded', treeItem)"
/>
<div
@@ -131,6 +160,7 @@
<script>
import _ from 'lodash';
import treeItem from './tree-item.vue';
import treeItemLite from './tree-item-lite.vue';
import search from '../components/search.vue';
import objectUtils from 'objectUtils';
import uuid from 'uuid';
@@ -145,7 +175,8 @@ export default {
name: 'MctTree',
components: {
search,
treeItem
treeItem,
treeItemLite
},
props: {
syncTreeNavigation: {
@@ -596,6 +627,21 @@ export default {
navigationPath
};
},
buildTreeItemLite(indexResult) {
let liteObject = {
identifier: objectUtils.parseKeyString(indexResult.id),
name: indexResult.name,
type: indexResult.type
};
let navigationPath = '';
return {
id: indexResult.id,
object: liteObject,
objectPath: [],
navigationPath
};
},
// domainObject: the item we're building the path for (will be used in url and links)
// objects: array of domainObjects representing path to domainobject passed in
buildNavigationPath(domainObject, objects) {
@@ -693,30 +739,37 @@ export default {
}
},
async getSearchResults() {
let results = await this.searchService.query(this.searchValue);
let results = await this.searchService.queryLite(this.searchValue);
this.searchResultItems = [];
// build out tree-item-lite results
for (let i = 0; i < results.hits.length; i++) {
let result = results.hits[i];
let newStyleObject = objectUtils.toNewFormat(result.object.getModel(), result.object.getId());
let objectPath = await this.openmct.objects.getOriginalPath(newStyleObject.identifier);
// removing the item itself, as the path we pass to buildTreeItem is a parent path
objectPath.shift();
// if root, remove, we're not using in object path for tree
let lastObject = objectPath.length ? objectPath[objectPath.length - 1] : false;
if (lastObject && lastObject.type === 'root') {
objectPath.pop();
}
// we reverse the objectPath in the tree, so have to do it here first,
// since this one is already in the correct direction
let resultObject = this.buildTreeItem(newStyleObject, objectPath.reverse());
let resultObject = this.buildTreeItemLite(result);
this.searchResultItems.push(resultObject);
}
// for (let i = 0; i < results.hits.length; i++) {
// let result = results.hits[i];
// let newStyleObject = objectUtils.toNewFormat(result.object.getModel(), result.object.getId());
// let objectPath = await this.openmct.objects.getOriginalPath(newStyleObject.identifier);
// // removing the item itself, as the path we pass to buildTreeItem is a parent path
// objectPath.shift();
// // if root, remove, we're not using in object path for tree
// let lastObject = objectPath.length ? objectPath[objectPath.length - 1] : false;
// if (lastObject && lastObject.type === 'root') {
// objectPath.pop();
// }
// // we reverse the objectPath in the tree, so have to do it here first,
// // since this one is already in the correct direction
// let resultObject = this.buildTreeItem(newStyleObject, objectPath.reverse());
// this.searchResultItems.push(resultObject);
// }
this.searchLoading = false;
},
searchTree(value) {

View File

@@ -0,0 +1,108 @@
<template>
<div
ref="me"
:style="{
'top': virtualScroll ? itemTop : 'auto',
'position': virtualScroll ? 'absolute' : 'relative'
}"
class="c-tree__item-h"
>
<div
class="c-tree__item"
>
<view-control
v-model="expanded"
class="c-tree__item__view-control"
:control-class="'c-nav__up'"
:enabled="showUp"
/>
<object-label
:domain-object="node.object"
:object-path="node.objectPath"
:navigate-to-path="''"
:style="{ paddingLeft: leftOffset }"
:lite-object="true"
:before-interaction="onInteraction"
/>
<view-control
v-model="expanded"
class="c-tree__item__view-control"
:control-class="'c-nav__down'"
:enabled="showDown"
/>
</div>
</div>
</template>
<script>
import viewControl from '../components/viewControl.vue';
import ObjectLabel from '../components/ObjectLabel.vue';
export default {
name: 'TreeItem',
inject: ['openmct'],
components: {
viewControl,
ObjectLabel
},
props: {
node: {
type: Object,
required: true
},
leftOffset: {
type: String,
default: '0px'
},
showUp: {
type: Boolean,
default: false
},
showDown: {
type: Boolean,
default: true
},
itemIndex: {
type: Number,
required: false,
default: undefined
},
itemOffset: {
type: Number,
required: false,
default: undefined
},
itemHeight: {
type: Number,
required: false,
default: 0
},
virtualScroll: {
type: Boolean,
default: false
}
},
data() {
return {
expanded: false
};
},
computed: {
itemTop() {
return (this.itemOffset + this.itemIndex) * this.itemHeight + 'px';
}
},
methods: {
async onInteraction() {
let domainObject = await this.openmct.objects.get(this.node.object.identifier);
let objectPath = await this.openmct.objects.getOriginalPath(this.node.object.identifier);
objectPath.pop();
return {
domainObject,
objectPath
};
}
}
};
</script>

View File

@@ -13,25 +13,36 @@ export default {
this.$el.addEventListener('contextmenu', this.showContextMenu);
function updateObject(oldObject, newObject) {
if (oldObject.name == 'drag n drop sg') console.log({ oldObject, newObject});
Object.assign(oldObject, newObject);
}
this.objectPath.forEach(object => {
if (object) {
this.$once('hook:destroyed',
this.openmct.objects.observe(object, '*', updateObject.bind(this, object)));
}
});
if (!this.liteObject) {
this.objectPath.forEach(object => {
if (object) {
this.$once('hook:destroyed',
this.openmct.objects.observe(object, '*', updateObject.bind(this, object)));
}
});
}
},
destroyed() {
this.$el.removeEventListener('contextMenu', this.showContextMenu);
},
methods: {
showContextMenu(event) {
async showContextMenu(event) {
event.preventDefault();
event.stopPropagation();
let actions = this.openmct.actions.get(this.objectPath);
let objectPath = this.objectPath;
if (this.liteObject && this.beforeInteraction) {
let fullObjectInfo = await this.beforeInteraction();
objectPath = fullObjectInfo.objectPath;
}
let actionsCollection = this.openmct.actions.get(objectPath);
let actions = actionsCollection.getVisibleActions();
let sortedActions = this.openmct.actions._groupAndSortActions(actions);
this.openmct.menus.showMenu(event.clientX, event.clientY, sortedActions);

View File

@@ -23,6 +23,7 @@
<div class="l-preview-window">
<PreviewHeader
:current-view="currentView"
:action-collection="actionCollection"
:domain-object="domainObject"
:views="views"
/>
@@ -52,7 +53,8 @@ export default {
domainObject: domainObject,
viewKey: undefined,
views: [],
currentView: {}
currentView: {},
actionCollection: undefined
};
},
mounted() {
@@ -65,8 +67,7 @@ export default {
});
this.setView(this.views[0]);
},
destroyed() {
this.view.destroy();
beforeDestroy() {
if (this.stopListeningStyles) {
this.stopListeningStyles();
}
@@ -75,6 +76,13 @@ export default {
this.styleRuleManager.destroy();
delete this.styleRuleManager;
}
if (this.actionCollection) {
this.actionCollection.destroy();
}
},
destroyed() {
this.view.destroy();
},
methods: {
clear() {
@@ -97,11 +105,19 @@ export default {
this.viewContainer = document.createElement('div');
this.viewContainer.classList.add('l-angular-ov-wrapper');
this.$refs.objectView.append(this.viewContainer);
this.view = this.currentView.view(this.domainObject, this.objectPath);
this.getActionsCollection();
this.view.show(this.viewContainer, false);
this.initObjectStyles();
},
getActionsCollection() {
if (this.actionCollection) {
this.actionCollection.destroy();
}
this.actionCollection = this.openmct.actions._get(this.objectPath, this.view);
},
initObjectStyles() {
if (!this.styleRuleManager) {
this.styleRuleManager = new StyleRuleManager((this.domainObject.configuration && this.domainObject.configuration.objectStyles), this.openmct, this.updateStyle.bind(this));
@@ -124,8 +140,9 @@ export default {
}
let keys = Object.keys(styleObj);
let firstChild = this.$refs.objectView.querySelector(':first-child');
keys.forEach(key => {
let firstChild = this.$refs.objectView.querySelector(':first-child');
if (firstChild) {
if ((typeof styleObj[key] === 'string') && (styleObj[key].indexOf('__no_value') > -1)) {
if (firstChild.style[key]) {

View File

@@ -13,12 +13,25 @@
</div>
</div>
<div class="l-browse-bar__end">
<view-switcher
:v-if="!hideViewSwitcher"
:views="views"
:current-view="currentView"
/>
<div class="l-browse-bar__actions">
<view-switcher
:v-if="!hideViewSwitcher"
:views="views"
:current-view="currentView"
/>
<button
v-for="(item, index) in statusBarItems"
:key="index"
class="c-button"
:class="item.cssClass"
@click="item.callBack"
>
</button>
<button
class="l-browse-bar__actions c-icon-button icon-3-dots"
title="More options"
@click.prevent.stop="showMenuItems($event)"
></button>
</div>
</div>
</div>
@@ -26,6 +39,11 @@
<script>
import ViewSwitcher from '../../ui/layout/ViewSwitcher.vue';
const HIDDEN_ACTIONS = [
'remove',
'move',
'preview'
];
export default {
inject: [
@@ -58,16 +76,53 @@ export default {
default: () => {
return [];
}
},
actionCollection: {
type: Object,
default: () => {
return undefined;
}
}
},
data() {
return {
type: this.openmct.types.get(this.domainObject.type)
type: this.openmct.types.get(this.domainObject.type),
statusBarItems: [],
menuActionItems: []
};
},
watch: {
actionCollection(actionCollection) {
if (this.actionCollection) {
this.unlistenToActionCollection();
}
this.actionCollection.on('update', this.updateActionItems);
this.updateActionItems(this.actionCollection.getActionsObject());
}
},
mounted() {
if (this.actionCollection) {
this.actionCollection.on('update', this.updateActionItems);
this.updateActionItems(this.actionCollection.getActionsObject());
}
},
methods: {
setView(view) {
this.$emit('setView', view);
},
unlistenToActionCollection() {
this.actionCollection.off('update', this.updateActionItems);
delete this.actionCollection;
},
updateActionItems() {
this.actionCollection.hide(HIDDEN_ACTIONS);
this.statusBarItems = this.actionCollection.getStatusBarActions();
this.menuActionItems = this.actionCollection.getVisibleActions();
},
showMenuItems(event) {
let sortedActions = this.openmct.actions._groupAndSortActions(this.menuActionItems);
this.openmct.menus.showMenu(event.x, event.y, sortedActions);
}
}
};

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