Compare commits

..

21 Commits

Author SHA1 Message Date
Joshi
0a4ff80f96 Version 1.4.0 2020-11-09 10:42:42 -08:00
Charles Hacskaylo
77b720d00d Fix Imagery for VERVE #266 (#3507)
* Fairly extensive refactoring to fix layout in Safari for VERVE #266

- VERY WIP at this time!
- Many instances of `height: 100%` converted or amended to include
`flex: 1 1 auto`;
- Some high-use containers like `c-so-view__object-view` converted to use
flex layout;
- Views fixed generally for sub-object view, and specifically for
Conditionals, Folder grid view and Imagery;
- Imagery background image holder converted to use absolute positioning;
- TODO: Notebook has a problem where the side nav pane isn't overlaying
in Safari - it's a JS thing, c-drawer--push isn't be replaced with
c-drawer--overlays as it should;

* CSS and markup refactoring to support addition of 'suspect' telemetry

- Remove commented code;
2020-11-09 09:33:25 -08:00
Deep Tailor
ba982671b2 Quick idea on a splash screen that will not increase load time (#3376)
* New splash screen

Co-authored-by: charlesh88 <charlesh88@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2020-11-06 13:58:57 -08:00
Jamie V
5df7d92d64 [Navigation Tree] Fix tree loading issue (#3500)
* added resize observer for FIRST load of mainTree

* new Promise driven height calculation test

* cleaning up code, sticking with promise height caclcuations

* more cleanup

* returning from the initialize function
2020-11-03 12:06:49 -08:00
David Tsay
a8228406de [Inspector] Allow styles (including font and font size) to be saved and reused (#3432)
* working proto for font size

* wip

* Font styling

 - Base classes for font-size and font;
 - WIP!

* working data attribute for fontsize

* Font styling

 - Add `js-style-receiver` to markup, refine style targeting JS for
 better application of styles;
 - Refinements to font and size CSS;
 - WIP!

* Font styling

 - Redo CSS to use `data-*` attributes;
 - New `u-style-receiver` class for use as font-size and font-family CSS
 selector target;
 - New `js-style-receiver` class for use as JS target by ObjectView.vue;
 - New classes added to markup in all Open MCT views;
 - Changed font-size values from 'is-font-size--*' to just the number;
 - Some refinement to individual views to account for font-sizing
 capability;
 - Removed automatic font-size 13px being set by SubobjectView.vue;
 - WIP!

* working mixed styles

* Font styling

 - Added `u-style-receiver` to TelemetryView.vue;
 - Added `icon-font-size` to Font Size dropdown button;
 - TODO: better font-size icon;

* working font-family

* Font styling

 - Art for `icon-font-size` glyph updated;
 - Redefined glyph usage in some Layout toolbar buttons;
 - Updated font-size and font dropdown menus options text;

* Font styling

 - Refined font-size and font dropdown values;
 - Fixed toolbar-select-menu.vue to remove 'px' from non-specific option
  return;

* dont allow font styling on layouts that contain other layouts

* fix lint warning

* add sizing row

* fix bug with column width sizing

* fix bug with header style

* add saved styles inspector view

* WIP

* add vue component for selector

* WIP styles manager to communicate between vue components

* WIP saving and persisting styles

* no duplicate styles prevention

* fix props syntax

* WIP can apply conditional styles

* static styles do not work yet

* display border color in saved styles swatch

* allow deleting styles except default style

* WIP apply static style works but also to layout...

* prevent additional StylesView from being created

* delete style message

* change save order

* move applystyle to selector component

* rename for consistency

* naming refactor

* add style description

* update style properties only if they exist and do not erase properties

* refactor singleton usage

refactor save method

* show save and delete only on hover

* do not show delete icon if not in edit mode

* normalize styles before saving

prevent apply style if conditional and static styles are simultaneously selected

* remove default style

tweak selector display

* allow conditional and static styles to have saved style applied

limit saved styles to 20

* refactor styles manager

remove openmct dependency

use provide/inject

* resolve merge conflicts

* lint fix

* reorganize styles

* add font style editor to styles view

* save and display border correctly in saved styles view

* WIP add font styling controls to inspector styles view

* add font constants

* WIP refactor to provide reactive props

fix locked for edit

* WIP display consolidated font styles for selection in editor

* WIP font styles saved to layout

* WIP persisting font styles from inspector works

* fix styleable check

* move logic up to stylesview because save is two part

* apply font style to thumb

* there can be only one

* show font style for native views

* linting fix

* push stylesManager work to StylesView

* move method to computed

* move constant definition outside of function call

* Styling for saved styles functionality WIP

- Simplified and removed unnecessary markup;
- Standardized style applied to saved style element and toolbar control;
- Removed saved style expand arrow and description, replaced with item
title / tooltip approach;
- Standardized width of `c-style-thumb` element;
- Moved font size and style controls to the designed location;

* Styling for saved styles functionality WIP

- Layout and CSS normalization between style editor control and saved
style preview element;
- Control alignment refined;
- Moved font size and style controls to the designed location;

* Styling for saved styles functionality WIP

- Update font size icon art to normalize size;
- Sanding, tweaking, alignin and layout in style controls area of
Inspector;

* Styling for saved styles functionality WIP

- Hide the font size and style menu buttons unless the user is editing;

* remove font controls from toolbar

* turn styles tab into multipane element

* lint fix

* no font style should not be viewed as non-specific

* delete saved style by index not style

* cleanup

* view and inspector view updates on initial font change

* revert computed back to method

* set initial height

* fix test after removing 2 buttons from toolbar

* fix hidden lint error

* fix lint

Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
Co-authored-by: charlesh88 <charlesh88@gmail.com>
2020-11-02 12:35:43 -08:00
Shefali Joshi
2401473012 [#3465] Intercept drag start event for imagery controls (#3485) 2020-11-02 11:26:33 -08:00
Charles Hacskaylo
e502fb88fa Fix Imagery brightness and contrast controls (#3473)
* Fix imagery #3467

- Move location of imagery controls in markup;
- Refine vertical placement;

* Fix imagery #3467

- Fix Firefox-related slider problems: bring over slider fixes and
markup from branch `imagery-view-layers`;

* Fix imagery #3467

- Fix linting problem;

Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
2020-11-02 08:38:13 -08:00
Charles Hacskaylo
37a52cb011 Notebook fixes for NT10 'click-to-edit entry' (#3475)
* Notebook fixes for NT10 'click-to-edit entry'

- Hovering over entries now displays a subtle background change, and
only displays the 'inline input' look when clicked into;
- Changed default styling and behavior to not apply default text
content: new entries now start with a blank entry, and do not include
'placeholder' formatting;
- Refactored styles associated with `c-input-inline`, `c-ne__input` and
`reactive-input` mixin;
- New mixin `inlineInput`;
- Removed unused CSS classes, general cleanups;

* fixed defaultText as blank issue and some cleanup

* Update _mixins.scss

- Remove commented code;

Co-authored-by: Nikhil Mandlik <nikhil.k.mandlik@nasa.gov>
2020-10-30 16:47:29 -07:00
Nikhil
04fb4e8a82 [Tables] Object names should appear in tables (#3466)
* [Tables] Object names should appear in tables #3312

* updated tests to include name header.

* fixed lint issue.

* Removed Name from data.

* renamed 'addColunmName'  to 'addNameColumn'.

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2020-10-30 15:10:31 -07:00
Jamie V
5646a252f7 [Navigation Tree] Simplify logic (#3474)
* added new navigation method for tracking, lots of optimizations

* updated indicator logic, tweaked objectPath/navigationPath, removed old code

* added temporary ancestors variable to be used while building new tree ui during navigation

* removed observer for ancestors, all handled in composition watch now

* updates from PR comments

* fixing testing errors

* checking for older format of saved path, update if old
2020-10-29 11:58:45 -07:00
Jamie V
0e6ce7f58b [Time Conductor] Realtime presets and history tracking (#3270)
Time conductor realtime preset/history updates

Co-authored-by: Nikhil <nikhil.k.mandlik@nasa.gov>
Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2020-10-28 17:46:28 -07:00
Nikhil
8cd6a4c6a3 [Notebook] Link to snapshot should not be a fully qualified url #3445 (#3460)
Co-authored-by: Andrew Henry <akhenry@gmail.com>
2020-10-28 16:46:54 -07:00
Shefali Joshi
02fc162197 Save subobject styles to container/layout if the object cannot be persisted (#3471)
* styles for Subobjects that can't be persisted should be saved on the container/layout

* Add tests for suboject styles that should be saved on the display layout
2020-10-26 15:58:42 -07:00
David Tsay
84d21a3695 [Display Layout] User should be able to set outer dimensions (#3333)
* Display Layout grid toggle and dimensions

- Added toggle grid button;
- Added Layout 'size' properties;
- Very WIP!

* Display Layout grid toggle and dimensions

- Cleanup toolbar;

* new configuration layoutDimensions

* add outer dimensions

* content dimensions not needed

* show/hide layout dimensions based on selection

* push non-dynamic styles to class definition

* remove grid code for other display layout feature

* reorder to match master

* layoutDimensionsStyle computed prop should return an object

* Styling for Display Layout dimensions box

- Mods to markup and SCSS;
- New ``$editDimensionsColor` theme constant;

* Styling for Display Layout dimensions box

- Refined styling;
- Fixed selector for nested sub-layouts;

* Styling for Display Layout dimensions box

- Added v-if that now only displays the dimensions indicator if both
width and height are greater than 0;

* fix lint issues

* fix merge issues

* fix display layout dimensions logic

* fix display layout dimensions check

Co-authored-by: charlesh88 <charlesh88@gmail.com>
Co-authored-by: Shefali Joshi <simplyrender@gmail.com>
Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
2020-10-23 12:19:16 -07:00
David Tsay
1a6369c2b9 [Display Layout] Grid lines should show and hide appropriately for nested layouts (#3330)
* change selector from sibling to same element

* hide gridlines for selected layout if is multi selection
2020-10-23 10:02:18 -07:00
David Tsay
463c44679d [Display Layout] User should be able to toggle grid lines (#3331)
* Display Layout grid toggle and dimensions

- Added toggle grid button;
- Added Layout 'size' properties;
- Very WIP!

* Display Layout grid toggle and dimensions

- Cleanup toolbar;

* new configuration layoutDimensions

* extract display layout grid to own vue component

* split toolbar structure into two structures

* allow toggling grid when editing display layout

* toggle grid icon show/hide state on click

* grid be shown on starting edit mode

* remove dimensions code for other display layout feature

* toggle icon after method completes

* change icon names

* update spec to include new action and separator

Co-authored-by: charlesh88 <charlesh88@gmail.com>
Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
2020-10-23 09:32:35 -07:00
Sanchit Singhal
c1f3ea4e61 fixed windows scss load time issues (#3361)
Co-authored-by: MUDKIP-9560\sanch <sanchit.singhal@mandsconsulting.com>
Co-authored-by: Nikhil <nikhil.k.mandlik@nasa.gov>
2020-10-22 16:17:41 -07:00
Nikhil
142b767470 [Notebook] new notebook entry causes console error #3440 (#3443)
* [Notebook] new notebook entry causes console error #3440

* using 'makeKeyString' to compare notebook identifiers

Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
2020-10-19 17:57:57 -07:00
Deep Tailor
184b716b53 [Telemetry Table] Row counts (#3428)
* add marked rows and total rows in tables

* Styling for table row counts addition

- Main styles for new `.c-table-indicator` and elements;
- Refined main layout spacing;
- Layout for table footer elements;
- Hover behavior for footer when table in Display Layout;

* Styling for table row counts addition

- Refined `.c-filter-indication` styles;
- Refined `.c-table-indicator` styles;
- Added dynamic tooltips for total and marked rows count elements;

* fix lint issues

Co-authored-by: charlesh88 <charlesh88@gmail.com>
2020-10-19 11:48:10 -07:00
Shefali Joshi
e53399495b Legacy and new object providers work together (#3461)
* Strip mct namespace from ids when getting models from cache

* Revert PersistenceCapability to use legacy code
Enforce empty namespace for LegacyPersistenceAdapter for new object providers

* Reverts change to caching provider

* CouchObject provider is registered with the mct space.
When saving objects via the persistence capability use the mct space to find the couchdb object provider
2020-10-19 10:17:18 -07:00
David Tsay
d27f73579b [Plots] Toggle grid lines (#3313)
* add toggle button

* enable toggle grid lines in plots

* fix merge issue

* change to new glyphs

Co-authored-by: Deep Tailor <deep.j.tailor@nasa.gov>
2020-10-19 10:07:51 -07:00
153 changed files with 3339 additions and 2569 deletions

View File

@@ -76,6 +76,7 @@ define([
workerRequest[prop] = Number(workerRequest[prop]);
});
workerRequest.name = domainObject.name;
return workerRequest;

View File

@@ -108,7 +108,6 @@
for (; nextStep < end && data.length < 5000; nextStep += step) {
data.push({
name: request.name,
utc: nextStep,
yesterday: nextStep - 60 * 60 * 24 * 1000,
sin: sin(nextStep, period, amplitude, offset, phase, randomness),

View File

@@ -30,12 +30,50 @@
<link rel="icon" type="image/png" href="dist/favicons/favicon-96x96.png" sizes="96x96" type="image/x-icon">
<link rel="icon" type="image/png" href="dist/favicons/favicon-32x32.png" sizes="32x32" type="image/x-icon">
<link rel="icon" type="image/png" href="dist/favicons/favicon-16x16.png" sizes="16x16" type="image/x-icon">
<style type="text/css">
@keyframes splash-spinner {
0% {
transform: translate(-50%, -50%) rotate(0deg); }
100% {
transform: translate(-50%, -50%) rotate(360deg); } }
#splash-screen {
background-color: black;
position: absolute;
top: 0; right: 0; bottom: 0; left: 0;
z-index: 10000;
}
#splash-screen:before {
animation-name: splash-spinner;
animation-duration: 0.5s;
animation-iteration-count: infinite;
animation-timing-function: linear;
border-radius: 50%;
border-color: rgba(255,255,255,0.25);
border-top-color: white;
border-style: solid;
border-width: 10px;
content: '';
display: block;
opacity: 0.25;
position: absolute;
left: 50%; top: 50%;
height: 100px; width: 100px;
}
</style>
</head>
<body>
</body>
<script>
const THIRTY_SECONDS = 30 * 1000;
const THIRTY_MINUTES = THIRTY_SECONDS * 60;
const ONE_MINUTE = THIRTY_SECONDS * 2;
const FIVE_MINUTES = ONE_MINUTE * 5;
const FIFTEEN_MINUTES = FIVE_MINUTES * 3;
const THIRTY_MINUTES = FIFTEEN_MINUTES * 2;
const ONE_HOUR = THIRTY_MINUTES * 2;
const TWO_HOURS = ONE_HOUR * 2;
const ONE_DAY = ONE_HOUR * 24;
[
'example/eventGenerator'
@@ -73,21 +111,21 @@
{
label: 'Last Day',
bounds: {
start: () => Date.now() - 1000 * 60 * 60 * 24,
start: () => Date.now() - ONE_DAY,
end: () => Date.now()
}
},
{
label: 'Last 2 hours',
bounds: {
start: () => Date.now() - 1000 * 60 * 60 * 2,
start: () => Date.now() - TWO_HOURS,
end: () => Date.now()
}
},
{
label: 'Last hour',
bounds: {
start: () => Date.now() - 1000 * 60 * 60,
start: () => Date.now() - ONE_HOUR,
end: () => Date.now()
}
}
@@ -96,7 +134,7 @@
records: 10,
// maximum duration between start and end bounds
// for utc-based time systems this is in milliseconds
limit: 1000 * 60 * 60 * 24
limit: ONE_DAY
},
{
name: "Realtime",
@@ -105,7 +143,44 @@
clockOffsets: {
start: - THIRTY_MINUTES,
end: THIRTY_SECONDS
}
},
presets: [
{
label: '1 Hour',
bounds: {
start: - ONE_HOUR,
end: THIRTY_SECONDS
}
},
{
label: '30 Minutes',
bounds: {
start: - THIRTY_MINUTES,
end: THIRTY_SECONDS
}
},
{
label: '15 Minutes',
bounds: {
start: - FIFTEEN_MINUTES,
end: THIRTY_SECONDS
}
},
{
label: '5 Minutes',
bounds: {
start: - FIVE_MINUTES,
end: THIRTY_SECONDS
}
},
{
label: '1 Minute',
bounds: {
start: - ONE_MINUTE,
end: THIRTY_SECONDS
}
}
]
}
]
}));

View File

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

View File

@@ -143,8 +143,8 @@ define([
"$window"
],
"group": "windowing",
"priority": 10,
"cssClass": "icon-new-window"
"cssClass": "icon-new-window",
"priority": "preferred"
}
],
"runs": [

View File

@@ -139,9 +139,7 @@ define([
],
"description": "Edit",
"category": "view-control",
"cssClass": "major icon-pencil",
"group": "action",
"priority": 10
"cssClass": "major icon-pencil"
},
{
"key": "properties",
@@ -152,8 +150,6 @@ define([
"implementation": PropertiesAction,
"cssClass": "major icon-pencil",
"name": "Edit Properties...",
"group": "action",
"priority": 10,
"description": "Edit properties of this object.",
"depends": [
"dialogService"

View File

@@ -114,7 +114,12 @@ define(["objectUtils"],
var self = this,
domainObject = this.domainObject;
let newStyleObject = objectUtils.toNewFormat(domainObject.getModel(), domainObject.getId());
const identifier = {
namespace: this.getSpace(),
key: this.getKey()
};
let newStyleObject = objectUtils.toNewFormat(domainObject.getModel(), identifier);
return this.openmct.objects
.save(newStyleObject)
@@ -146,6 +151,7 @@ define(["objectUtils"],
return domainObject.useCapability("mutation", function () {
return model;
}, modified);
}
}
@@ -153,11 +159,10 @@ define(["objectUtils"],
return this.$q.when(true);
}
return this.openmct.objects.get(domainObject.getId()).then((newStyleObject) => {
let oldStyleObject = this.openmct.legacyObject(newStyleObject);
return updateModel(oldStyleObject.getModel());
});
return this.persistenceService.readObject(
this.getSpace(),
this.getKey()
).then(updateModel);
};
/**

View File

@@ -37,7 +37,6 @@ define(
key = "persistence key",
id = "object identifier",
model,
refreshModel,
SPACE = "some space",
persistence,
mockOpenMCT,
@@ -61,7 +60,6 @@ define(
someKey: "some value",
name: "domain object"
};
refreshModel = {someOtherKey: "some other value"};
mockPersistenceService = jasmine.createSpyObj(
"persistenceService",
@@ -101,8 +99,8 @@ define(
mockNewStyleDomainObject = Object.assign({}, model);
mockNewStyleDomainObject.identifier = {
namespace: "",
key: id
namespace: SPACE,
key: key
};
// Simulate mutation capability
@@ -112,16 +110,8 @@ define(
}
});
mockOpenMCT = {
legacyObject: function (object) {
return {
getModel: function () {
return object;
}
};
}
};
mockOpenMCT.objects = jasmine.createSpyObj('Object API', ['save', 'get']);
mockOpenMCT = {};
mockOpenMCT.objects = jasmine.createSpyObj('Object API', ['save']);
mockIdentifierService.parse.and.returnValue(mockIdentifier);
mockIdentifier.getSpace.and.returnValue(SPACE);
@@ -141,7 +131,6 @@ define(
describe("successful persistence", function () {
beforeEach(function () {
mockOpenMCT.objects.save.and.returnValue(Promise.resolve(true));
mockOpenMCT.objects.get.and.returnValue(Promise.resolve(refreshModel));
});
it("creates unpersisted objects with the persistence service", function () {
// Verify precondition; no call made during constructor
@@ -157,10 +146,11 @@ define(
});
it("refreshes the domain object model from persistence", function () {
var refreshModel = {someOtherKey: "some other value"};
model.persisted = 1;
persistence.refresh().then(() => {
expect(model).toEqual(refreshModel);
});
mockPersistenceService.readObject.and.returnValue(asPromise(refreshModel));
persistence.refresh();
expect(model).toEqual(refreshModel);
});
it("does not trigger error notification on successful"

View File

@@ -66,8 +66,6 @@ define([
"description": "Move object to another location.",
"cssClass": "icon-move",
"category": "contextual",
"group": "action",
"priority": 9,
"implementation": MoveAction,
"depends": [
"policyService",
@@ -81,8 +79,6 @@ define([
"description": "Duplicate object to another location.",
"cssClass": "icon-duplicate",
"category": "contextual",
"group": "action",
"priority": 8,
"implementation": CopyAction,
"depends": [
"$log",
@@ -99,8 +95,6 @@ define([
"description": "Create Link to object in another location.",
"cssClass": "icon-link",
"category": "contextual",
"group": "action",
"priority": 7,
"implementation": LinkAction,
"depends": [
"policyService",

View File

@@ -19,7 +19,7 @@
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<div class="c-clock l-time-display" ng-controller="ClockController as clock">
<div class="c-clock l-time-display u-style-receiver js-style-receiver" ng-controller="ClockController as clock">
<div class="c-clock__timezone">
{{clock.zone()}}
</div>

View File

@@ -19,7 +19,7 @@
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<div class="c-timer is-{{timer.timerState}}" ng-controller="TimerController as timer">
<div class="c-timer u-style-receiver js-style-receiver is-{{timer.timerState}}" ng-controller="TimerController as timer">
<div class="c-timer__controls">
<button ng-click="timer.clickStopButton()"
ng-hide="timer.timerState == 'stopped'"

View File

@@ -47,8 +47,6 @@ define([
"implementation": ExportAsJSONAction,
"category": "contextual",
"cssClass": "icon-export",
"group": "json",
"priority": 2,
"depends": [
"openmct",
"exportService",
@@ -63,8 +61,6 @@ define([
"implementation": ImportAsJSONAction,
"category": "contextual",
"cssClass": "icon-import",
"group": "json",
"priority": 2,
"depends": [
"exportService",
"identifierService",

View File

@@ -242,9 +242,7 @@ define([
this.overlays = new OverlayAPI.default();
this.menus = new api.MenuAPI(this);
this.actions = new api.ActionsAPI(this);
this.contextMenu = new api.ContextMenuRegistry();
this.router = new ApplicationRouter();
@@ -273,7 +271,6 @@ define([
this.install(this.plugins.URLTimeSettingsSynchronizer());
this.install(this.plugins.NotificationIndicator());
this.install(this.plugins.NewFolderAction());
this.install(this.plugins.ViewDatumAction());
}
MCT.prototype = Object.create(EventEmitter.prototype);

View File

@@ -35,5 +35,5 @@ export default function LegacyActionAdapter(openmct, legacyActions) {
legacyActions.filter(contextualCategoryOnly)
.map(LegacyAction => new LegacyContextMenuAction(openmct, LegacyAction))
.forEach(openmct.actions.register);
.forEach(openmct.contextMenu.registerAction);
}

View File

@@ -31,8 +31,6 @@ export default class LegacyContextMenuAction {
this.description = LegacyAction.definition.description;
this.cssClass = LegacyAction.definition.cssClass;
this.LegacyAction = LegacyAction;
this.group = LegacyAction.definition.group;
this.priority = LegacyAction.definition.priority;
}
invoke(objectPath) {

View File

@@ -129,13 +129,6 @@ define([
ObjectServiceProvider.prototype.get = function (key) {
let keyString = utils.makeKeyString(key);
const space = this.getSpace(keyString);
let identifier = utils.parseKeyString(keyString);
// We assign to the space for legacy persistence providers since they register themselves using a defaultSpace.
// This is the way to make everyone happy.
identifier.namespace = space;
keyString = utils.makeKeyString(identifier);
return this.objectService.getObjects([keyString])
.then(function (results) {

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

View File

@@ -1,145 +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.
*****************************************************************************/
import EventEmitter from 'EventEmitter';
import ActionCollection from './ActionCollection';
import _ from 'lodash';
class ActionsAPI extends EventEmitter {
constructor(openmct) {
super();
this._allActions = {};
this._actionCollections = new WeakMap();
this._openmct = openmct;
this._groupOrder = ['windowing', 'undefined', 'view', 'action', 'json'];
this.register = this.register.bind(this);
this.get = this.get.bind(this);
this._applicableActions = this._applicableActions.bind(this);
this._updateCachedActionCollections = this._updateCachedActionCollections.bind(this);
}
register(actionDefinition) {
this._allActions[actionDefinition.key] = actionDefinition;
}
get(objectPath, view) {
let viewContext = view && view.getViewContext && view.getViewContext() || {};
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;
}
} 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;
}
}
updateGroupOrder(groupArray) {
this._groupOrder = groupArray;
}
_updateCachedActionCollections(key) {
if (this._actionCollections.has(key)) {
let actionCollection = this._actionCollections.get(key);
actionCollection.off('destroy', this._updateCachedActionCollections);
this._actionCollections.delete(key);
}
}
_applicableActions(objectPath, view) {
let actionsObject = {};
let keys = Object.keys(this._allActions).filter(key => {
let actionDefinition = this._allActions[key];
if (actionDefinition.appliesTo === undefined) {
return true;
} else {
return actionDefinition.appliesTo(objectPath, view);
}
});
keys.forEach(key => {
let action = _.clone(this._allActions[key]);
actionsObject[key] = action;
});
return actionsObject;
}
_groupAndSortActions(actionsArray) {
if (!Array.isArray(actionsArray) && typeof actionsArray === 'object') {
actionsArray = Object.keys(actionsArray).map(key => actionsArray[key]);
}
let actionsObject = {};
let groupedSortedActionsArray = [];
function sortDescending(a, b) {
return b.priority - a.priority;
}
actionsArray.forEach(action => {
if (actionsObject[action.group] === undefined) {
actionsObject[action.group] = [action];
} else {
actionsObject[action.group].push(action);
}
});
this._groupOrder.forEach(group => {
let groupArray = actionsObject[group];
if (groupArray) {
groupedSortedActionsArray.push(groupArray.sort(sortDescending));
}
});
return groupedSortedActionsArray;
}
}
export default ActionsAPI;

View File

@@ -28,9 +28,8 @@ define([
'./telemetry/TelemetryAPI',
'./indicators/IndicatorAPI',
'./notifications/NotificationAPI',
'./Editor',
'./menu/MenuAPI',
'./actions/ActionsAPI'
'./contextMenu/ContextMenuAPI',
'./Editor'
], function (
TimeAPI,
@@ -40,9 +39,8 @@ define([
TelemetryAPI,
IndicatorAPI,
NotificationAPI,
EditorAPI,
MenuAPI,
ActionsAPI
ContextMenuAPI,
EditorAPI
) {
return {
TimeAPI: TimeAPI,
@@ -53,7 +51,6 @@ define([
IndicatorAPI: IndicatorAPI,
NotificationAPI: NotificationAPI.default,
EditorAPI: EditorAPI,
MenuAPI: MenuAPI.default,
ActionsAPI: ActionsAPI.default
ContextMenuRegistry: ContextMenuAPI.default
};
});

View File

@@ -0,0 +1,24 @@
<template>
<div class="c-menu">
<ul>
<li
v-for="action in actions"
:key="action.name"
:class="action.cssClass"
:title="action.description"
@click="action.invoke(objectPath)"
>
{{ action.name }}
</li>
<li v-if="actions.length === 0">
No actions defined.
</li>
</ul>
</div>
</template>
<script>
export default {
inject: ['actions', 'objectPath']
};
</script>

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 ContextMenuComponent from './ContextMenu.vue';
import Vue from 'vue';
/**
* The ContextMenuAPI allows the addition of new context menu actions, and for the context menu to be launched from
* custom HTML elements.
* @interface ContextMenuAPI
* @memberof module:openmct
*/
class ContextMenuAPI {
constructor() {
this._allActions = [];
this._activeContextMenu = undefined;
this._hideActiveContextMenu = this._hideActiveContextMenu.bind(this);
this.registerAction = this.registerAction.bind(this);
}
/**
* Defines an item to be added to context menus. Allows specification of text, appearance, and behavior when
* selected. Applicabilioty can be restricted by specification of an `appliesTo` function.
*
* @interface ContextMenuAction
* @memberof module:openmct
* @property {string} name the human-readable name of this view
* @property {string} description a longer-form description (typically
* a single sentence or short paragraph) of this kind of view
* @property {string} cssClass the CSS class to apply to labels for this
* view (to add icons, for instance)
* @property {string} key unique key to identify the context menu action
* (used in custom context menu eg table rows, to identify which actions to include)
* @property {boolean} hideInDefaultMenu optional flag to hide action from showing in the default context menu (tree item)
*/
/**
* @method appliesTo
* @memberof module:openmct.ContextMenuAction#
* @param {DomainObject[]} objectPath the path of the object that the context menu has been invoked on.
* @returns {boolean} true if the action applies to the objects specified in the 'objectPath', otherwise false.
*/
/**
* Code to be executed when the action is selected from a context menu
* @method invoke
* @memberof module:openmct.ContextMenuAction#
* @param {DomainObject[]} objectPath the path of the object to invoke the action on.
*/
/**
* @param {ContextMenuAction} actionDefinition
*/
registerAction(actionDefinition) {
this._allActions.push(actionDefinition);
}
/**
* @private
*/
_showContextMenuForObjectPath(objectPath, x, y, actionsToBeIncluded) {
let applicableActions = this._allActions.filter((action) => {
if (actionsToBeIncluded) {
if (action.appliesTo === undefined && actionsToBeIncluded.includes(action.key)) {
return true;
}
return action.appliesTo(objectPath, actionsToBeIncluded) && actionsToBeIncluded.includes(action.key);
} else {
if (action.appliesTo === undefined) {
return true;
}
return action.appliesTo(objectPath) && !action.hideInDefaultMenu;
}
});
if (this._activeContextMenu) {
this._hideActiveContextMenu();
}
this._activeContextMenu = this._createContextMenuForObject(objectPath, applicableActions);
this._activeContextMenu.$mount();
document.body.appendChild(this._activeContextMenu.$el);
let position = this._calculatePopupPosition(x, y, this._activeContextMenu.$el);
this._activeContextMenu.$el.style.left = `${position.x}px`;
this._activeContextMenu.$el.style.top = `${position.y}px`;
document.addEventListener('click', this._hideActiveContextMenu);
}
/**
* @private
*/
_calculatePopupPosition(eventPosX, eventPosY, menuElement) {
let menuDimensions = menuElement.getBoundingClientRect();
let overflowX = (eventPosX + menuDimensions.width) - document.body.clientWidth;
let overflowY = (eventPosY + menuDimensions.height) - document.body.clientHeight;
if (overflowX > 0) {
eventPosX = eventPosX - overflowX;
}
if (overflowY > 0) {
eventPosY = eventPosY - overflowY;
}
return {
x: eventPosX,
y: eventPosY
};
}
/**
* @private
*/
_hideActiveContextMenu() {
document.removeEventListener('click', this._hideActiveContextMenu);
document.body.removeChild(this._activeContextMenu.$el);
this._activeContextMenu.$destroy();
this._activeContextMenu = undefined;
}
/**
* @private
*/
_createContextMenuForObject(objectPath, actions) {
return new Vue({
components: {
ContextMenu: ContextMenuComponent
},
provide: {
actions: actions,
objectPath: objectPath
},
template: '<ContextMenu></ContextMenu>'
});
}
}
export default ContextMenuAPI;

View File

@@ -1,67 +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.
*****************************************************************************/
import Menu from './menu.js';
/**
* The MenuAPI allows the addition of new context menu actions, and for the context menu to be launched from
* custom HTML elements.
* @interface MenuAPI
* @memberof module:openmct
*/
class MenuAPI {
constructor(openmct) {
this.openmct = openmct;
this.showMenu = this.showMenu.bind(this);
this._clearMenuComponent = this._clearMenuComponent.bind(this);
this._showObjectMenu = this._showObjectMenu.bind(this);
}
showMenu(x, y, actions) {
if (this.menuComponent) {
this.menuComponent.dismiss();
}
let options = {
x,
y,
actions
};
this.menuComponent = new Menu(options);
this.menuComponent.once('destroy', this._clearMenuComponent);
}
_clearMenuComponent() {
this.menuComponent = undefined;
delete this.menuComponent;
}
_showObjectMenu(objectPath, x, y, actionsToBeIncluded) {
let applicableActions = this.openmct.actions._groupedAndSortedObjectActions(objectPath, actionsToBeIncluded);
this.showMenu(x, y, applicableActions);
}
}
export default MenuAPI;

View File

@@ -1,52 +0,0 @@
<template>
<div class="c-menu">
<ul v-if="actions.length && actions[0].length">
<template
v-for="(actionGroups, index) in actions"
>
<li
v-for="action in actionGroups"
:key="action.name"
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
:title="action.description"
@click="action.callBack"
>
{{ action.name }}
</li>
<div
v-if="index !== actions.length - 1"
:key="index"
class="c-menu__section-separator"
>
</div>
<li
v-if="actionGroups.length === 0"
:key="index"
>
No actions defined.
</li>
</template>
</ul>
<ul v-else>
<li
v-for="action in actions"
:key="action.name"
:class="action.cssClass"
:title="action.description"
@click="action.callBack"
>
{{ action.name }}
</li>
<li v-if="actions.length === 0">
No actions defined.
</li>
</ul>
</div>
</template>
<script>
export default {
inject: ['actions']
};
</script>

View File

@@ -1,94 +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.
*****************************************************************************/
import EventEmitter from 'EventEmitter';
import MenuComponent from './components/Menu.vue';
import Vue from 'vue';
class Menu extends EventEmitter {
constructor(options) {
super();
this.options = options;
this.component = new Vue({
provide: {
actions: options.actions
},
components: {
MenuComponent
},
template: '<menu-component />'
});
if (options.onDestroy) {
this.once('destroy', options.onDestroy);
}
this.dismiss = this.dismiss.bind(this);
this.show = this.show.bind(this);
this.show();
}
dismiss() {
this.emit('destroy');
document.body.removeChild(this.component.$el);
document.removeEventListener('click', this.dismiss);
this.component.$destroy();
}
show() {
this.component.$mount();
document.body.appendChild(this.component.$el);
let position = this._calculatePopupPosition(this.options.x, this.options.y, this.component.$el);
this.component.$el.style.left = `${position.x}px`;
this.component.$el.style.top = `${position.y}px`;
document.addEventListener('click', this.dismiss);
}
/**
* @private
*/
_calculatePopupPosition(eventPosX, eventPosY, menuElement) {
let menuDimensions = menuElement.getBoundingClientRect();
let overflowX = (eventPosX + menuDimensions.width) - document.body.clientWidth;
let overflowY = (eventPosY + menuDimensions.height) - document.body.clientHeight;
if (overflowX > 0) {
eventPosX = eventPosX - overflowX;
}
if (overflowY > 0) {
eventPosY = eventPosY - overflowY;
}
return {
x: eventPosX,
y: eventPosY
};
}
}
export default Menu;

View File

@@ -22,7 +22,6 @@ class OverlayAPI {
this.dismissLastOverlay();
}
});
}
/**
@@ -128,7 +127,6 @@ class OverlayAPI {
return progressDialog;
}
}
export default OverlayAPI;

View File

@@ -176,10 +176,7 @@ export default {
this.timestampKey = timeSystem.key;
},
showContextMenu(event) {
let allActions = this.openmct.actions.get(this.currentObjectPath, {}, {viewHistoricalData: true});
let applicableActions = CONTEXT_MENU_ACTIONS.map(key => allActions[key]);
this.openmct.menus.showMenu(event.x, event.y, applicableActions);
this.openmct.contextMenu._showContextMenuForObjectPath(this.currentObjectPath, event.x, event.y, CONTEXT_MENU_ACTIONS);
},
resetValues() {
this.value = '---';

View File

@@ -21,7 +21,7 @@
*****************************************************************************/
<template>
<div class="c-lad-table-wrapper">
<div class="c-lad-table-wrapper u-style-receiver js-style-receiver">
<table class="c-table c-lad-table">
<thead>
<tr>

View File

@@ -23,7 +23,6 @@
export default class ClearDataAction {
constructor(openmct, appliesToObjects) {
this.name = 'Clear Data for Object';
this.key = 'clear-data-action';
this.description = 'Clears current data for object, unsubscribes and resubscribes to data';
this.cssClass = 'icon-clear-data';

View File

@@ -53,7 +53,7 @@ define([
openmct.indicators.add(indicator);
}
openmct.actions.register(new ClearDataAction.default(openmct, appliesToObjects));
openmct.contextMenu.registerAction(new ClearDataAction.default(openmct, appliesToObjects));
};
};
});

View File

@@ -26,12 +26,12 @@ import ClearDataAction from '../clearDataAction.js';
describe('When the Clear Data Plugin is installed,', function () {
const mockObjectViews = jasmine.createSpyObj('objectViews', ['emit']);
const mockIndicatorProvider = jasmine.createSpyObj('indicators', ['add']);
const mockActionsProvider = jasmine.createSpyObj('actions', ['register']);
const mockContextMenuProvider = jasmine.createSpyObj('contextMenu', ['registerAction']);
const openmct = {
objectViews: mockObjectViews,
indicators: mockIndicatorProvider,
actions: mockActionsProvider,
contextMenu: mockContextMenuProvider,
install: function (plugin) {
plugin(this);
}
@@ -51,7 +51,7 @@ describe('When the Clear Data Plugin is installed,', function () {
it('Clear Data context menu action is installed', function () {
openmct.install(ClearDataActionPlugin([]));
expect(mockActionsProvider.register).toHaveBeenCalled();
expect(mockContextMenuProvider.registerAction).toHaveBeenCalled();
});
it('clear data action emits a clearData event when invoked', function () {

View File

@@ -50,6 +50,7 @@
.c-cs {
display: flex;
flex-direction: column;
flex: 1 1 auto;
height: 100%;
overflow: hidden;

View File

@@ -21,21 +21,22 @@
*****************************************************************************/
<template>
<div class="c-style">
<span :class="[
{ 'is-style-invisible': styleItem.style.isStyleInvisible },
{ 'c-style-thumb--mixed': mixedStyles.indexOf('backgroundColor') > -1 }
]"
:style="[styleItem.style.imageUrl ? { backgroundImage:'url(' + styleItem.style.imageUrl + ')'} : itemStyle ]"
class="c-style-thumb"
>
<span class="c-style-thumb__text"
:class="{ 'hide-nice': !hasProperty(styleItem.style.color) }"
<div class="c-style has-local-controls c-toolbar">
<div class="c-style__controls">
<div :class="[
{ 'is-style-invisible': styleItem.style && styleItem.style.isStyleInvisible },
{ 'c-style-thumb--mixed': mixedStyles.indexOf('backgroundColor') > -1 }
]"
:style="[styleItem.style.imageUrl ? { backgroundImage:'url(' + styleItem.style.imageUrl + ')'} : itemStyle ]"
class="c-style-thumb"
>
ABC
</span>
</span>
<span class="c-toolbar">
<span class="c-style-thumb__text"
:class="{ 'hide-nice': !hasProperty(styleItem.style.color) }"
>
ABC
</span>
</div>
<toolbar-color-picker v-if="hasProperty(styleItem.style.border)"
class="c-style__toolbar-button--border-color u-menu-to--center"
:options="borderColorOption"
@@ -61,7 +62,14 @@
:options="isStyleInvisibleOption"
@change="updateStyleValue"
/>
</span>
</div>
<!-- Save Styles -->
<toolbar-button v-if="canSaveStyle"
class="c-style__toolbar-button--save c-local-controls--show-on-hover c-icon-button c-icon-button--major"
:options="saveOptions"
@click="saveItemStyle()"
/>
</div>
</template>
@@ -80,12 +88,11 @@ export default {
ToolbarColorPicker,
ToolbarToggleButton
},
inject: [
'openmct'
],
inject: ['openmct'],
props: {
isEditing: {
type: Boolean
type: Boolean,
required: true
},
mixedStyles: {
type: Array,
@@ -93,6 +100,10 @@ export default {
return [];
}
},
nonSpecificFontProperties: {
type: Array,
required: true
},
styleItem: {
type: Object,
required: true
@@ -182,7 +193,16 @@ export default {
}
]
};
},
saveOptions() {
return {
icon: 'icon-save',
title: 'Save style',
isEditing: this.isEditing
};
},
canSaveStyle() {
return this.isEditing && !this.mixedStyles.length && !this.nonSpecificFontProperties.length;
}
},
methods: {
@@ -216,6 +236,9 @@ export default {
}
this.$emit('persist', this.styleItem, item.property);
},
saveItemStyle() {
this.$emit('save-style', this.itemStyle);
}
}
};

View File

@@ -31,6 +31,11 @@
<div class="c-inspect-styles__header">
Object Style
</div>
<FontStyleEditor
v-if="canStyleFont"
:font-style="consolidatedFontStyle"
@set-font-property="setFontProperty"
/>
<div class="c-inspect-styles__content">
<div v-if="staticStyle"
class="c-inspect-styles__style"
@@ -39,7 +44,9 @@
:style-item="staticStyle"
:is-editing="allowEditing"
:mixed-styles="mixedStyles"
:non-specific-font-properties="nonSpecificFontProperties"
@persist="updateStaticStyle"
@save-style="saveStyle"
/>
</div>
<button
@@ -58,10 +65,11 @@
</div>
<div class="c-inspect-styles__content c-inspect-styles__condition-set">
<a v-if="conditionSetDomainObject"
class="c-object-label icon-conditional"
class="c-object-label"
:href="navigateToPath"
@click="navigateOrPreview"
>
<span class="c-object-label__type-icon icon-conditional"></span>
<span class="c-object-label__name">{{ conditionSetDomainObject.name }}</span>
</a>
<template v-if="allowEditing">
@@ -80,6 +88,12 @@
</template>
</div>
<FontStyleEditor
v-if="canStyleFont"
:font-style="consolidatedFontStyle"
@set-font-property="setFontProperty"
/>
<div v-if="conditionsLoaded"
class="c-inspect-styles__conditions"
>
@@ -97,8 +111,10 @@
/>
<style-editor class="c-inspect-styles__editor"
:style-item="conditionStyle"
:non-specific-font-properties="nonSpecificFontProperties"
:is-editing="allowEditing"
@persist="updateConditionalStyle"
@save-style="saveStyle"
/>
</div>
</div>
@@ -108,6 +124,7 @@
<script>
import FontStyleEditor from '@/ui/inspector/styles/FontStyleEditor.vue';
import StyleEditor from "./StyleEditor.vue";
import PreviewAction from "@/ui/preview/PreviewAction.js";
import { getApplicableStylesForItem, getConsolidatedStyleValues, getConditionSetIdentifierForItem } from "@/plugins/condition/utils/styleUtils";
@@ -116,16 +133,30 @@ import ConditionError from "@/plugins/condition/components/ConditionError.vue";
import ConditionDescription from "@/plugins/condition/components/ConditionDescription.vue";
import Vue from 'vue';
const NON_SPECIFIC = '??';
const NON_STYLEABLE_CONTAINER_TYPES = [
'layout',
'flexible-layout',
'tabs'
];
const NON_STYLEABLE_LAYOUT_ITEM_TYPES = [
'line-view',
'box-view',
'image-view'
];
export default {
name: 'StylesView',
components: {
FontStyleEditor,
StyleEditor,
ConditionError,
ConditionDescription
},
inject: [
'openmct',
'selection'
'selection',
'stylesManager'
],
data() {
return {
@@ -139,19 +170,80 @@ export default {
conditionsLoaded: false,
navigateToPath: '',
selectedConditionId: '',
locked: false
items: [],
domainObject: undefined,
consolidatedFontStyle: {}
};
},
computed: {
locked() {
return this.selection.some(selectionPath => {
const self = selectionPath[0].context.item;
const parent = selectionPath.length > 1 ? selectionPath[1].context.item : undefined;
return (self && self.locked) || (parent && parent.locked);
});
},
allowEditing() {
return this.isEditing && !this.locked;
},
styleableFontItems() {
return this.selection.filter(selectionPath => {
const item = selectionPath[0].context.item;
const itemType = item && item.type;
const layoutItem = selectionPath[0].context.layoutItem;
const layoutItemType = layoutItem && layoutItem.type;
if (itemType && NON_STYLEABLE_CONTAINER_TYPES.includes(itemType)) {
return false;
}
if (layoutItemType && NON_STYLEABLE_LAYOUT_ITEM_TYPES.includes(layoutItemType)) {
return false;
}
return true;
});
},
computedconsolidatedFontStyle() {
let consolidatedFontStyle;
const styles = [];
this.styleableFontItems.forEach(styleable => {
const fontStyle = this.getFontStyle(styleable[0]);
styles.push(fontStyle);
});
if (styles.length) {
const hasConsolidatedFontSize = styles.length && styles.every((fontStyle, i, arr) => fontStyle.fontSize === arr[0].fontSize);
const hasConsolidatedFont = styles.length && styles.every((fontStyle, i, arr) => fontStyle.font === arr[0].font);
consolidatedFontStyle = {
fontSize: hasConsolidatedFontSize ? styles[0].fontSize : NON_SPECIFIC,
font: hasConsolidatedFont ? styles[0].font : NON_SPECIFIC
};
}
return consolidatedFontStyle;
},
nonSpecificFontProperties() {
if (!this.consolidatedFontStyle) {
return [];
}
return Object.keys(this.consolidatedFontStyle).filter(property => this.consolidatedFontStyle[property] === NON_SPECIFIC);
},
canStyleFont() {
return this.styleableFontItems.length && this.allowEditing;
}
},
destroyed() {
this.removeListeners();
this.openmct.editor.off('isEditing', this.setEditState);
this.stylesManager.off('styleSelected', this.applyStyleToSelection);
},
mounted() {
this.items = [];
this.previewAction = new PreviewAction(this.openmct);
this.isMultipleSelection = this.selection.length > 1;
this.getObjectsAndItemsFromSelection();
@@ -166,7 +258,10 @@ export default {
this.initializeStaticStyle();
}
this.setConsolidatedFontStyle();
this.openmct.editor.on('isEditing', this.setEditState);
this.stylesManager.on('styleSelected', this.applyStyleToSelection);
},
methods: {
getObjectStyles() {
@@ -178,10 +273,10 @@ export default {
}
} else if (this.items.length) {
const itemId = this.items[0].id;
if (this.domainObject.configuration && this.domainObject.configuration.objectStyles && this.domainObject.configuration.objectStyles[itemId]) {
if (this.domainObject && this.domainObject.configuration && this.domainObject.configuration.objectStyles && this.domainObject.configuration.objectStyles[itemId]) {
objectStyles = this.domainObject.configuration.objectStyles[itemId];
}
} else if (this.domainObject.configuration && this.domainObject.configuration.objectStyles) {
} else if (this.domainObject && this.domainObject.configuration && this.domainObject.configuration.objectStyles) {
objectStyles = this.domainObject.configuration.objectStyles;
}
@@ -219,6 +314,18 @@ export default {
isItemType(type, item) {
return item && (item.type === type);
},
canPersistObject(item) {
// for now the only way to tell if an object can be persisted is if it is creatable.
let creatable = false;
if (item) {
const type = this.openmct.types.get(item.type);
if (type && type.definition) {
creatable = (type.definition.creatable === true);
}
}
return creatable;
},
hasConditionalStyle(domainObject, layoutItem) {
const id = layoutItem ? layoutItem.id : undefined;
@@ -235,13 +342,8 @@ export default {
this.selection.forEach((selectionItem) => {
const item = selectionItem[0].context.item;
const layoutItem = selectionItem[0].context.layoutItem;
const layoutDomainObject = selectionItem[0].context.item;
const isChildItem = selectionItem.length > 1;
if (layoutDomainObject && layoutDomainObject.locked) {
this.locked = true;
}
if (!isChildItem) {
domainObject = item;
itemStyle = getApplicableStylesForItem(item);
@@ -251,7 +353,7 @@ export default {
} else {
this.canHide = true;
domainObject = selectionItem[1].context.item;
if (item && !layoutItem || this.isItemType('subobject-view', layoutItem)) {
if (item && !layoutItem || (this.isItemType('subobject-view', layoutItem) && this.canPersistObject(item))) {
subObjects.push(item);
itemStyle = getApplicableStylesForItem(item);
if (this.hasConditionalStyle(item)) {
@@ -275,7 +377,7 @@ export default {
const {styles, mixedStyles} = getConsolidatedStyleValues(itemInitialStyles);
this.initialStyles = styles;
this.mixedStyles = mixedStyles;
// main layout
this.domainObject = domainObject;
this.removeListeners();
if (this.domainObject) {
@@ -298,6 +400,7 @@ export default {
isKeyItemId(key) {
return (key !== 'styles')
&& (key !== 'staticStyle')
&& (key !== 'fontStyle')
&& (key !== 'defaultConditionId')
&& (key !== 'selectedConditionId')
&& (key !== 'conditionSetIdentifier');
@@ -637,6 +740,124 @@ export default {
},
persist(domainObject, style) {
this.openmct.objects.mutate(domainObject, 'configuration.objectStyles', style);
},
applyStyleToSelection(style) {
if (!this.allowEditing) {
return;
}
this.updateSelectionFontStyle(style);
this.updateSelectionStyle(style);
},
updateSelectionFontStyle(style) {
const fontSizeProperty = {
fontSize: style.fontSize
};
const fontProperty = {
font: style.font
};
this.setFontProperty(fontSizeProperty);
this.setFontProperty(fontProperty);
},
updateSelectionStyle(style) {
const foundStyle = this.findStyleByConditionId(this.selectedConditionId);
if (foundStyle && !this.isStaticAndConditionalStyles) {
Object.entries(style).forEach(([property, value]) => {
if (foundStyle.style[property] !== undefined && foundStyle.style[property] !== value) {
foundStyle.style[property] = value;
}
});
this.getAndPersistStyles();
} else {
this.removeConditionSet();
Object.entries(style).forEach(([property, value]) => {
if (this.staticStyle.style[property] !== undefined && this.staticStyle.style[property] !== value) {
this.staticStyle.style[property] = value;
this.getAndPersistStyles(property);
}
});
}
},
saveStyle(style) {
const styleToSave = {
...style,
...this.consolidatedFontStyle
};
this.stylesManager.save(styleToSave);
},
setConsolidatedFontStyle() {
const styles = [];
this.styleableFontItems.forEach(styleable => {
const fontStyle = this.getFontStyle(styleable[0]);
styles.push(fontStyle);
});
if (styles.length) {
const hasConsolidatedFontSize = styles.length && styles.every((fontStyle, i, arr) => fontStyle.fontSize === arr[0].fontSize);
const hasConsolidatedFont = styles.length && styles.every((fontStyle, i, arr) => fontStyle.font === arr[0].font);
const fontSize = hasConsolidatedFontSize ? styles[0].fontSize : NON_SPECIFIC;
const font = hasConsolidatedFont ? styles[0].font : NON_SPECIFIC;
this.$set(this.consolidatedFontStyle, 'fontSize', fontSize);
this.$set(this.consolidatedFontStyle, 'font', font);
}
},
getFontStyle(selectionPath) {
const item = selectionPath.context.item;
const layoutItem = selectionPath.context.layoutItem;
let fontStyle = item && item.configuration && item.configuration.fontStyle;
// support for legacy where font styling in layouts only
if (!fontStyle) {
fontStyle = {
fontSize: layoutItem && layoutItem.fontSize || 'default',
font: layoutItem && layoutItem.font || 'default'
};
}
return fontStyle;
},
setFontProperty(fontStyleObject) {
let layoutDomainObject;
const [property, value] = Object.entries(fontStyleObject)[0];
this.styleableFontItems.forEach(styleable => {
if (!this.isLayoutObject(styleable)) {
const fontStyle = this.getFontStyle(styleable[0]);
fontStyle[property] = value;
this.openmct.objects.mutate(styleable[0].context.item, 'configuration.fontStyle', fontStyle);
} else {
// all layoutItems in this context will share same parent layout
if (!layoutDomainObject) {
layoutDomainObject = styleable[1].context.item;
}
// save layout item font style to parent layout configuration
const layoutItemIndex = styleable[0].context.index;
const layoutItemConfiguration = layoutDomainObject.configuration.items[layoutItemIndex];
layoutItemConfiguration[property] = value;
}
});
if (layoutDomainObject) {
this.openmct.objects.mutate(layoutDomainObject, 'configuration.items', layoutDomainObject.configuration.items);
}
// sync vue component on font update
this.$set(this.consolidatedFontStyle, property, value);
},
isLayoutObject(selectionPath) {
const layoutItemType = selectionPath[0].context.layoutItem && selectionPath[0].context.layoutItem.type;
return layoutItemType && layoutItemType !== 'subobject-view';
}
}
};

View File

@@ -40,9 +40,11 @@
}
&__condition-set {
align-items: baseline;
border-bottom: 1px solid $colorInteriorBorder;
display: flex;
flex-direction: row;
align-items: center;
padding-bottom: $interiorMargin;
.c-object-label {
flex: 1 1 auto;
@@ -53,7 +55,10 @@
}
}
&__style,
&__style {
padding-bottom: $interiorMargin;
}
&__condition {
padding: $interiorMargin;
}

View File

@@ -146,6 +146,8 @@ describe('the plugin', function () {
let displayLayoutItem;
let lineLayoutItem;
let boxLayoutItem;
let notCreatableObjectItem;
let notCreatableObject;
let selection;
let component;
let styleViewComponentObject;
@@ -264,6 +266,19 @@ describe('the plugin', function () {
"stroke": "#717171",
"type": "line-view",
"id": "57d49a28-7863-43bd-9593-6570758916f0"
},
{
"width": 32,
"height": 18,
"x": 36,
"y": 8,
"identifier": {
"key": "~TEST~image",
"namespace": "test-space"
},
"hasFrame": true,
"type": "subobject-view",
"id": "6d9fe81b-a3ce-4e59-b404-a4a0be1a5d85"
}
],
"layoutGrid": [
@@ -297,6 +312,52 @@ describe('the plugin', function () {
"type": "box-view",
"id": "89b88746-d325-487b-aec4-11b79afff9e8"
};
notCreatableObjectItem = {
"width": 32,
"height": 18,
"x": 36,
"y": 8,
"identifier": {
"key": "~TEST~image",
"namespace": "test-space"
},
"hasFrame": true,
"type": "subobject-view",
"id": "6d9fe81b-a3ce-4e59-b404-a4a0be1a5d85"
};
notCreatableObject = {
"identifier": {
"key": "~TEST~image",
"namespace": "test-space"
},
"name": "test~image",
"location": "test-space:~TEST",
"type": "test.image",
"telemetry": {
"values": [
{
"key": "value",
"name": "Value",
"hints": {
"image": 1,
"priority": 0
},
"format": "image",
"source": "value"
},
{
"key": "utc",
"source": "timestamp",
"name": "Timestamp",
"format": "iso",
"hints": {
"domain": 1,
"priority": 1
}
}
]
}
};
selection = [
[{
context: {
@@ -316,6 +377,19 @@ describe('the plugin', function () {
"index": 0
}
},
{
context: {
item: displayLayoutItem,
"supportsMultiSelect": true
}
}],
[{
context: {
"item": notCreatableObject,
"layoutItem": notCreatableObjectItem,
"index": 2
}
},
{
context: {
item: displayLayoutItem,
@@ -344,7 +418,7 @@ describe('the plugin', function () {
});
it('initializes the items in the view', () => {
expect(styleViewComponentObject.items.length).toBe(2);
expect(styleViewComponentObject.items.length).toBe(3);
});
it('initializes conditional styles', () => {
@@ -363,7 +437,7 @@ describe('the plugin', function () {
return Vue.nextTick().then(() => {
expect(styleViewComponentObject.domainObject.configuration.objectStyles).toBeDefined();
[boxLayoutItem, lineLayoutItem].forEach((item) => {
[boxLayoutItem, lineLayoutItem, notCreatableObjectItem].forEach((item) => {
const itemStyles = styleViewComponentObject.domainObject.configuration.objectStyles[item.id].styles;
expect(itemStyles.length).toBe(2);
const foundStyle = itemStyles.find((style) => {
@@ -385,7 +459,7 @@ describe('the plugin', function () {
return Vue.nextTick().then(() => {
expect(styleViewComponentObject.domainObject.configuration.objectStyles).toBeDefined();
[boxLayoutItem, lineLayoutItem].forEach((item) => {
[boxLayoutItem, lineLayoutItem, notCreatableObjectItem].forEach((item) => {
const itemStyle = styleViewComponentObject.domainObject.configuration.objectStyles[item.id].staticStyle;
expect(itemStyle).toBeDefined();
const applicableStyles = getApplicableStylesForItem(styleViewComponentObject.domainObject, item);

View File

@@ -22,7 +22,7 @@
<template>
<component :is="urlDefined ? 'a' : 'span'"
class="c-condition-widget"
class="c-condition-widget u-style-receiver js-style-receiver"
:href="urlDefined ? internalDomainObject.url : null"
>
<div class="c-condition-widget__label">

View File

@@ -64,16 +64,9 @@ define([
components: {
AlphanumericFormatView: AlphanumericFormatView.default
},
template: '<alphanumeric-format-view ref="alphanumericFormatView"></alphanumeric-format-view>'
template: '<alphanumeric-format-view></alphanumeric-format-view>'
});
},
getViewContext() {
if (component) {
return component.$refs.alphanumericFormatView.getViewContext();
} else {
return {};
}
},
destroy: function () {
component.$destroy();
component = undefined;

View File

@@ -73,7 +73,6 @@ define(['lodash'], function (_) {
]
}
};
const VIEW_TYPES = {
'telemetry-view': {
value: 'telemetry-view',
@@ -96,7 +95,6 @@ define(['lodash'], function (_) {
class: 'icon-tabular-realtime'
}
};
const APPLICABLE_VIEWS = {
'telemetry-view': [
VIEW_TYPES['telemetry.plot.overlay'],
@@ -390,29 +388,6 @@ define(['lodash'], function (_) {
}
}
function getTextSizeMenu(selectedParent, selection) {
const TEXT_SIZE = [8, 9, 10, 11, 12, 13, 14, 15, 16, 20, 24, 30, 36, 48, 72, 96, 128];
return {
control: "select-menu",
domainObject: selectedParent,
applicableSelectedItems: selection.filter(selectionPath => {
let type = selectionPath[0].context.layoutItem.type;
return type === 'text-view' || type === 'telemetry-view';
}),
property: function (selectionPath) {
return getPath(selectionPath) + ".size";
},
title: "Set text size",
options: TEXT_SIZE.map(size => {
return {
value: size + "px"
};
})
};
}
function getTextButton(selectedParent, selection) {
return {
control: "button",
@@ -423,7 +398,7 @@ define(['lodash'], function (_) {
property: function (selectionPath) {
return getPath(selectionPath);
},
icon: "icon-font",
icon: "icon-pencil",
title: "Edit text properties",
dialog: DIALOG_FORM.text
};
@@ -623,6 +598,33 @@ define(['lodash'], function (_) {
}
}
function getToggleGridButton(selection, selectionPath) {
const ICON_GRID_SHOW = 'icon-grid-on';
const ICON_GRID_HIDE = 'icon-grid-off';
let displayLayoutContext;
if (selection.length === 1 && selectionPath === undefined) {
displayLayoutContext = selection[0][0].context;
} else {
displayLayoutContext = selectionPath[1].context;
}
return {
control: "button",
domainObject: displayLayoutContext.item,
icon: ICON_GRID_SHOW,
method: function () {
displayLayoutContext.toggleGrid();
this.icon = this.icon === ICON_GRID_SHOW
? ICON_GRID_HIDE
: ICON_GRID_SHOW;
},
secondary: true
};
}
function getSeparator() {
return {
control: "separator"
@@ -637,7 +639,9 @@ define(['lodash'], function (_) {
}
if (isMainLayoutSelected(selectedObjects[0])) {
return [getAddButton(selectedObjects)];
return [
getToggleGridButton(selectedObjects),
getAddButton(selectedObjects)];
}
let toolbar = {
@@ -649,11 +653,11 @@ define(['lodash'], function (_) {
'display-mode': [],
'telemetry-value': [],
'style': [],
'text-style': [],
'position': [],
'duplicate': [],
'unit-toggle': [],
'remove': []
'remove': [],
'toggle-grid': []
};
selectedObjects.forEach(selectionPath => {
@@ -699,12 +703,6 @@ define(['lodash'], function (_) {
toolbar['telemetry-value'] = [getTelemetryValueMenu(selectionPath, selectedObjects)];
}
if (toolbar['text-style'].length === 0) {
toolbar['text-style'] = [
getTextSizeMenu(selectedParent, selectedObjects)
];
}
if (toolbar.position.length === 0) {
toolbar.position = [
getStackOrder(selectedParent, selectionPath),
@@ -730,12 +728,6 @@ define(['lodash'], function (_) {
}
}
} else if (layoutItem.type === 'text-view') {
if (toolbar['text-style'].length === 0) {
toolbar['text-style'] = [
getTextSizeMenu(selectedParent, selectedObjects)
];
}
if (toolbar.position.length === 0) {
toolbar.position = [
getStackOrder(selectedParent, selectionPath),
@@ -800,6 +792,10 @@ define(['lodash'], function (_) {
if (toolbar.duplicate.length === 0) {
toolbar.duplicate = [getDuplicateButton(selectedParent, selectionPath, selectedObjects)];
}
if (toolbar['toggle-grid'].length === 0) {
toolbar['toggle-grid'] = [getToggleGridButton(selectedObjects, selectionPath)];
}
});
let toolbarArray = Object.values(toolbar);

View File

@@ -56,6 +56,28 @@ define(function () {
1
],
required: true
},
{
name: "Horizontal size (px)",
control: "numberfield",
cssClass: "l-input-sm l-numeric",
property: [
"configuration",
"layoutDimensions",
0
],
required: false
},
{
name: "Vertical size (px)",
control: "numberfield",
cssClass: "l-input-sm l-numeric",
property: [
"configuration",
"layoutDimensions",
1
],
required: false
}
]
};

View File

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

View File

@@ -29,7 +29,7 @@
@endMove="() => $emit('endMove')"
>
<div
class="c-box-view"
class="c-box-view u-style-receiver js-style-receiver"
:class="[styleClass]"
:style="style"
></div>

View File

@@ -22,7 +22,7 @@
<template>
<div
class="l-layout"
class="l-layout u-style-receiver js-style-receiver"
:class="{
'is-multi-selected': selectedLayoutItems.length > 1,
'allow-editing': isEditing
@@ -31,21 +31,19 @@
@click.capture="bypassSelection"
@drop="handleDrop"
>
<!-- Background grid -->
<div
<display-layout-grid
v-if="isEditing"
class="l-layout__grid-holder c-grid"
:grid-size="gridSize"
:show-grid="showGrid"
/>
<div
v-if="shouldDisplayLayoutDimensions"
class="l-layout__dimensions"
:style="layoutDimensionsStyle"
>
<div
v-if="gridSize[0] >= 3"
class="c-grid__x l-grid l-grid-x"
:style="[{ backgroundSize: gridSize[0] + 'px 100%' }]"
></div>
<div
v-if="gridSize[1] >= 3"
class="c-grid__y l-grid l-grid-y"
:style="[{ backgroundSize: '100%' + gridSize[1] + 'px' }]"
></div>
<div class="l-layout__dimensions-vals">
{{ layoutDimensions[0] }},{{ layoutDimensions[1] }}
</div>
</div>
<component
:is="item.type"
@@ -81,6 +79,7 @@ import TextView from './TextView.vue';
import LineView from './LineView.vue';
import ImageView from './ImageView.vue';
import EditMarquee from './EditMarquee.vue';
import DisplayLayoutGrid from './DisplayLayoutGrid.vue';
import _ from 'lodash';
const TELEMETRY_IDENTIFIER_FUNCTIONS = {
@@ -127,6 +126,7 @@ const DUPLICATE_OFFSET = 3;
let components = ITEM_TYPE_VIEW_MAP;
components['edit-marquee'] = EditMarquee;
components['display-layout-grid'] = DisplayLayoutGrid;
function getItemDefinition(itemType, ...options) {
let itemView = ITEM_TYPE_VIEW_MAP[itemType];
@@ -140,6 +140,7 @@ function getItemDefinition(itemType, ...options) {
export default {
components: components,
inject: ['openmct', 'options', 'objectPath'],
props: {
domainObject: {
type: Object,
@@ -156,7 +157,8 @@ export default {
return {
internalDomainObject: domainObject,
initSelectIndex: undefined,
selection: []
selection: [],
showGrid: true
};
},
computed: {
@@ -171,6 +173,23 @@ export default {
return this.itemIsInCurrentSelection(item);
});
},
layoutDimensions() {
return this.internalDomainObject.configuration.layoutDimensions;
},
shouldDisplayLayoutDimensions() {
return this.layoutDimensions
&& this.layoutDimensions[0] > 0
&& this.layoutDimensions[1] > 0;
},
layoutDimensionsStyle() {
const width = `${this.layoutDimensions[0]}px`;
const height = `${this.layoutDimensions[1]}px`;
return {
width,
height
};
},
showMarquee() {
let selectionPath = this.selection[0];
let singleSelectedLine = this.selection.length === 1
@@ -179,7 +198,13 @@ export default {
return this.isEditing && selectionPath && selectionPath.length > 1 && !singleSelectedLine;
}
},
inject: ['openmct', 'options', 'objectPath'],
watch: {
isEditing(value) {
if (value) {
this.showGrid = value;
}
}
},
mounted() {
this.unlisten = this.openmct.objects.observe(this.internalDomainObject, '*', function (obj) {
this.internalDomainObject = JSON.parse(JSON.stringify(obj));
@@ -798,6 +823,9 @@ export default {
this.removeItem(selection);
this.initSelectIndex = this.layoutItems.length - 1; //restore selection
},
toggleGrid() {
this.showGrid = !this.showGrid;
}
}
};

View File

@@ -0,0 +1,34 @@
<template>
<div
class="l-layout__grid-holder"
:class="{ 'c-grid': showGrid }"
>
<div
v-if="gridSize[0] >= 3"
class="c-grid__x l-grid l-grid-x"
:style="[{ backgroundSize: gridSize[0] + 'px 100%' }]"
></div>
<div
v-if="gridSize[1] >= 3"
class="c-grid__y l-grid l-grid-y"
:style="[{ backgroundSize: '100%' + gridSize[1] + 'px' }]"
></div>
</div>
</template>
<script>
export default {
props: {
gridSize: {
type: Array,
required: true,
validator: (arr) => arr && arr.length === 2
&& arr.every(el => typeof el === 'number')
},
showGrid: {
type: Boolean,
required: true
}
}
};
</script>

View File

@@ -81,6 +81,7 @@ export default {
style() {
let backgroundImage = 'url(' + this.item.url + ')';
let border = '1px solid ' + this.item.stroke;
if (this.itemStyle) {
if (this.itemStyle.imageUrl !== undefined) {
backgroundImage = 'url(' + this.itemStyle.imageUrl + ')';

View File

@@ -35,6 +35,8 @@
:object-path="currentObjectPath"
:has-frame="item.hasFrame"
:show-edit-view="false"
:layout-font-size="item.fontSize"
:layout-font="item.font"
/>
</layout-frame>
</template>
@@ -73,6 +75,8 @@ export default {
y: position[1],
identifier: domainObject.identifier,
hasFrame: hasFrameByDefault(domainObject.type),
fontSize: 'default',
font: 'default',
viewKey
};
},
@@ -138,18 +142,14 @@ export default {
this.domainObject = domainObject;
this.currentObjectPath = [this.domainObject].concat(this.objectPath.slice());
this.$nextTick(() => {
let reference = this.$refs.objectFrame;
if (reference) {
let childContext = this.$refs.objectFrame.getSelectionContext();
childContext.item = domainObject;
childContext.layoutItem = this.item;
childContext.index = this.index;
this.context = childContext;
this.removeSelectable = this.openmct.selection.selectable(
this.$el, this.context, this.immediatelySelect || this.initSelect);
delete this.immediatelySelect;
}
let childContext = this.$refs.objectFrame.getSelectionContext();
childContext.item = domainObject;
childContext.layoutItem = this.item;
childContext.index = this.index;
this.context = childContext;
this.removeSelectable = this.openmct.selection.selectable(
this.$el, this.context, this.immediatelySelect || this.initSelect);
delete this.immediatelySelect;
});
}
}

View File

@@ -30,13 +30,15 @@
>
<div
v-if="domainObject"
class="c-telemetry-view"
class="u-style-receiver c-telemetry-view"
:class="{
styleClass,
'is-missing': domainObject.status === 'missing'
}"
:style="styleObject"
@contextmenu.prevent.stop="showContextMenu"
:data-font-size="item.fontSize"
:data-font="item.font"
@contextmenu.prevent="showContextMenu"
>
<div class="is-missing__indicator"
title="This item is missing"
@@ -74,11 +76,10 @@
import LayoutFrame from './LayoutFrame.vue';
import printj from 'printj';
import conditionalStylesMixin from "../mixins/objectStyles-mixin";
import { getDefaultNotebook } from '@/plugins/notebook/utils/notebook-storage.js';
const DEFAULT_TELEMETRY_DIMENSIONS = [10, 5];
const DEFAULT_POSITION = [1, 1];
const CONTEXT_MENU_ACTIONS = ['copyToClipboard', 'copyToNotebook', 'viewHistoricalData'];
const CONTEXT_MENU_ACTIONS = ['viewHistoricalData'];
export default {
makeDefinition(openmct, gridSize, domainObject, position) {
@@ -96,7 +97,8 @@ export default {
stroke: "",
fill: "",
color: "",
size: "13px"
fontSize: 'default',
font: 'default'
};
},
inject: ['openmct', 'objectPath'],
@@ -127,11 +129,10 @@ export default {
},
data() {
return {
currentObjectPath: undefined,
datum: undefined,
domainObject: undefined,
formats: undefined,
viewKey: `alphanumeric-format-${Math.random()}`
domainObject: undefined,
currentObjectPath: undefined
};
},
computed: {
@@ -152,10 +153,15 @@ export default {
return unit;
},
styleObject() {
return Object.assign({}, {
fontSize: this.item.size
}, this.itemStyle);
let size;
//for legacy size support
if (!this.item.fontSize) {
size = this.item.size;
}
return Object.assign({}, {
size
}, this.itemStyle);
},
fieldName() {
return this.valueMetadata && this.valueMetadata.name;
@@ -218,18 +224,6 @@ 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];
return `At ${timeFormatter.format(this.datum)} ${this.domainObject.name} had a value of ${this.telemetryValue} ${this.unit}`;
},
requestHistoricalData() {
let bounds = this.openmct.time.bounds();
let options = {
@@ -267,16 +261,6 @@ export default {
this.requestHistoricalData(this.domainObject);
}
},
getView() {
return {
getViewContext() {
return {
viewHistoricalData: true,
skipCache: true
};
}
};
},
setObject(domainObject) {
this.domainObject = domainObject;
this.keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
@@ -300,37 +284,12 @@ 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.$emit('formatChanged', this.item, format);
},
async getContextMenuActions() {
const defaultNotebook = getDefaultNotebook();
const domainObject = defaultNotebook && await this.openmct.objects.get(defaultNotebook.notebookMeta.identifier);
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}`;
}
return CONTEXT_MENU_ACTIONS.includes(actionsObject[key].key);
});
return applicableActionKeys.map(key => actionsObject[key]);
},
async showContextMenu(event) {
const contextMenuActions = await this.getContextMenuActions();
this.openmct.menus.showMenu(event.x, event.y, contextMenuActions);
showContextMenu(event) {
this.openmct.contextMenu._showContextMenuForObjectPath(this.currentObjectPath, event.x, event.y, CONTEXT_MENU_ACTIONS);
}
}
};

View File

@@ -29,7 +29,9 @@
@endMove="() => $emit('endMove')"
>
<div
class="c-text-view"
class="c-text-view u-style-receiver js-style-receiver"
:data-font-size="item.fontSize"
:data-font="item.font"
:class="[styleClass]"
:style="style"
>
@@ -47,13 +49,14 @@ export default {
return {
fill: '',
stroke: '',
size: '13px',
color: '',
x: 1,
y: 1,
width: 10,
height: 5,
text: element.text
text: element.text,
fontSize: 'default',
font: 'default'
};
},
inject: ['openmct'],
@@ -84,8 +87,14 @@ export default {
},
computed: {
style() {
let size;
//legacy size support
if (!this.item.fontSize) {
size = this.item.size;
}
return Object.assign({
fontSize: this.item.size
size
}, this.itemStyle);
}
},

View File

@@ -17,10 +17,29 @@
flex-direction: column;
overflow: auto;
&__grid-holder {
&__grid-holder,
&__dimensions {
display: none;
}
&__dimensions {
$b: 1px dashed $editDimensionsColor;
border-right: $b;
border-bottom: $b;
pointer-events: none;
position: absolute;
&-vals {
$p: 2px;
color: $editDimensionsColor;
display: inline-block;
font-style: italic;
position: absolute;
bottom: $p; right: $p;
opacity: 0.7;
}
}
&__frame {
position: absolute;
}
@@ -34,6 +53,10 @@
> .l-layout {
background: $editUIGridColorBg;
> [class*="__dimensions"] {
display: block;
}
> [class*="__grid-holder"] {
display: block;
}
@@ -42,12 +65,16 @@
}
.l-layout__frame {
&[s-selected],
&[s-selected]:not([multi-select="true"]),
&[s-selected-parent] {
// Display grid and allow edit marquee to display in nested layouts when editing
> * > * > .l-layout + .allow-editing {
> * > * > .l-layout.allow-editing {
box-shadow: inset $editUIGridColorFg 0 0 2px 1px;
> [class*="__dimensions"] {
display: block;
}
> [class*='grid-holder'] {
display: block;
}

View File

@@ -27,6 +27,7 @@ export default {
inject: ['openmct'],
data() {
return {
objectStyle: undefined,
itemStyle: undefined,
styleClass: ''
};

View File

@@ -26,12 +26,9 @@ import objectUtils from 'objectUtils';
import DisplayLayoutType from './DisplayLayoutType.js';
import DisplayLayoutToolbar from './DisplayLayoutToolbar.js';
import AlphaNumericFormatViewProvider from './AlphanumericFormatViewProvider.js';
import CopyToClipboardAction from './actions/CopyToClipboardAction';
export default function DisplayLayoutPlugin(options) {
return function (openmct) {
openmct.actions.register(new CopyToClipboardAction(openmct));
openmct.objectViews.addProvider({
key: 'layout.view',
canView: function (domainObject) {
@@ -75,7 +72,8 @@ export default function DisplayLayoutPlugin(options) {
duplicateItem: component && component.$refs.displayLayout.duplicateItem,
switchViewType: component && component.$refs.displayLayout.switchViewType,
mergeMultipleTelemetryViews: component && component.$refs.displayLayout.mergeMultipleTelemetryViews,
mergeMultipleOverlayPlots: component && component.$refs.displayLayout.mergeMultipleOverlayPlots
mergeMultipleOverlayPlots: component && component.$refs.displayLayout.mergeMultipleOverlayPlots,
toggleGrid: component && component.$refs.displayLayout.toggleGrid
};
},
onEditModeChange: function (isEditing) {

View File

@@ -340,6 +340,7 @@ describe('the plugin', function () {
it('provides controls including separators', () => {
const displayLayoutToolbar = openmct.toolbars.get(selection);
expect(displayLayoutToolbar.length).toBe(9);
});
});

View File

@@ -3,20 +3,26 @@
@include userSelectNone();
background: $colorFilterBg;
color: $colorFilterFg;
display: flex;
align-items: center;
font-size: 0.9em;
margin-top: $interiorMarginSm;
padding: 2px;
text-transform: uppercase;
&:before {
font-family: symbolsfont-12px;
content: $glyph-icon-filter;
display: block;
font-size: 12px;
margin-right: $interiorMarginSm;
}
&--mixed {
.c-filter-indication__mixed {
font-style: italic;
}
}
&__label {
+ .c-filter-indication__label {
&:before {
content: ', ';
}
}
}
}
.c-filter-tree-item {

View File

@@ -12,14 +12,14 @@
:href="objectLink"
>
<div
class="c-object-label__type-icon c-list-item__name__type-icon"
class="c-object-label__type-icon c-list-item__type-icon"
:class="item.type.cssClass"
>
<span class="is-missing__indicator"
title="This item is missing"
></span>
</div>
<div class="c-object-label__name c-list-item__name__name">{{ item.model.name }}</div>
<div class="c-object-label__name c-list-item__name">{{ item.model.name }}</div>
</a>
</td>
<td class="c-list-item__type">

View File

@@ -11,6 +11,8 @@
body.desktop & {
flex-flow: row wrap;
align-content: flex-start;
&__item {
height: $gridItemDesk;
width: $gridItemDesk;

View File

@@ -1,19 +1,11 @@
/******************************* LIST ITEM */
.c-list-item {
&__name__type-icon {
&__type-icon {
color: $colorItemTreeIcon;
}
&__name__name {
&__name {
@include ellipsize();
a & {
color: $colorItemFg;
}
}
&:not(.c-list-item__name) {
color: $colorItemFgDetails;
}
&.is-alias {

View File

@@ -28,5 +28,9 @@
padding-top: $p;
padding-bottom: $p;
width: 25%;
&:not(.c-list-item__name) {
color: $colorItemFgDetails;
}
}
}

View File

@@ -25,8 +25,6 @@ export default class GoToOriginalAction {
this.name = 'Go To Original';
this.key = 'goToOriginal';
this.description = 'Go to the original unlinked instance of this object';
this.group = 'action';
this.priority = 4;
this._openmct = openmct;
}

View File

@@ -23,6 +23,6 @@ import GoToOriginalAction from './goToOriginalAction';
export default function () {
return function (openmct) {
openmct.actions.register(new GoToOriginalAction(openmct));
openmct.contextMenu.registerAction(new GoToOriginalAction(openmct));
};
}

View File

@@ -7,58 +7,67 @@
@mouseover="focusElement"
>
<div class="c-imagery__main-image-wrapper has-local-controls">
<div class="h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover l-flex-row c-imagery__lc">
<span class="holder flex-elem grows c-imagery__lc__sliders">
<input v-model="filters.brightness"
class="icon-brightness"
type="range"
min="0"
max="500"
>
<input v-model="filters.contrast"
class="icon-contrast"
type="range"
min="0"
max="500"
>
<div class="h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover c-image-controls__controls">
<span class="c-image-controls__sliders"
draggable="true"
@dragstart="startDrag"
>
<div class="c-image-controls__slider-wrapper icon-brightness">
<input v-model="filters.brightness"
type="range"
min="0"
max="500"
>
</div>
<div class="c-image-controls__slider-wrapper icon-contrast">
<input v-model="filters.contrast"
type="range"
min="0"
max="500"
>
</div>
</span>
<span class="holder flex-elem t-reset-btn-holder c-imagery__lc__reset-btn">
<span class="t-reset-btn-holder c-imagery__lc__reset-btn c-image-controls__btn-reset">
<a class="s-icon-button icon-reset t-btn-reset"
@click="filters={brightness: 100, contrast: 100}"
></a>
</span>
</div>
<div class="main-image s-image-main c-imagery__main-image has-local-controls"
<div class="c-imagery__main-image__bg"
:class="{'paused unnsynced': isPaused,'stale':false }"
:style="{'background-image': imageUrl ? `url(${imageUrl})` : 'none',
'filter': `brightness(${filters.brightness}%) contrast(${filters.contrast}%)`}"
:data-openmct-image-timestamp="time"
:data-openmct-object-keystring="keyString"
>
<div class="c-local-controls c-local-controls--show-on-hover c-imagery__prev-next-buttons">
<button class="c-nav c-nav--prev"
title="Previous image"
:disabled="isPrevDisabled"
@click="prevImage()"
></button>
<button class="c-nav c-nav--next"
title="Next image"
:disabled="isNextDisabled"
@click="nextImage()"
></button>
</div>
<div class="c-imagery__main-image__image"
:style="{
'background-image': imageUrl ? `url(${imageUrl})` : 'none',
'filter': `brightness(${filters.brightness}%) contrast(${filters.contrast}%)`
}"
:data-openmct-image-timestamp="time"
:data-openmct-object-keystring="keyString"
></div>
</div>
<div class="c-local-controls c-local-controls--show-on-hover c-imagery__prev-next-buttons">
<button class="c-nav c-nav--prev"
title="Previous image"
:disabled="isPrevDisabled"
@click="prevImage()"
></button>
<button class="c-nav c-nav--next"
title="Next image"
:disabled="isNextDisabled"
@click="nextImage()"
></button>
</div>
<div class="c-imagery__control-bar">
<div class="c-imagery__time">
<div class="c-imagery__timestamp">{{ time }}</div>
<div class="c-imagery__timestamp u-style-receiver js-style-receiver">{{ time }}</div>
<div
v-if="canTrackDuration"
:class="{'c-imagery--new': isImageNew && !refreshCSS}"
class="c-imagery__age icon-timer"
>{{ formattedDuration }}</div>
</div>
<div class="h-local-controls flex-elem">
<div class="h-local-controls">
<button
class="c-button icon-pause pause-play"
:class="{'is-paused': isPaused}"
@@ -446,6 +455,10 @@ export default {
this.setFocusedImage(--index, THUMBNAIL_CLICKED);
}
},
startDrag(e) {
e.preventDefault();
e.stopPropagation();
},
arrowDownHandler(event) {
let key = event.keyCode;

View File

@@ -1,8 +1,8 @@
.c-imagery {
display: flex;
flex-direction: column;
flex: 1 1 auto;
overflow: hidden;
height: 100%;
&:focus {
outline: none;
@@ -19,13 +19,21 @@
}
&__main-image {
background-position: center;
background-repeat: no-repeat;
background-size: contain;
height: 100%;
&__bg {
background-color: $colorPlotBg;
border: 1px solid transparent;
flex: 1 1 auto;
&.unnsynced{
@include sUnsynced();
&.unnsynced{
@include sUnsynced();
}
}
&__image {
@include abs(); // Safari fix
background-position: center;
background-repeat: no-repeat;
background-size: contain;
}
}
@@ -138,11 +146,6 @@
}
}
.s-image-main {
background-color: $colorPlotBg;
border: 1px solid transparent;
}
/*************************************** IMAGERY LOCAL CONTROLS*/
.c-imagery {
.h-local-controls--overlay-content {
@@ -152,7 +155,7 @@
background: $colorLocalControlOvrBg;
border-radius: $basicCr;
max-width: 200px;
min-width: 100px;
min-width: 70px;
width: 35%;
align-items: center;
padding: $interiorMargin $interiorMarginLg;
@@ -173,6 +176,7 @@
&__lc {
&__reset-btn {
$bc: $scrollbarTrackColorBg;
&:before,
&:after {
border-right: 1px solid $bc;
@@ -195,6 +199,46 @@
}
}
.c-image-controls {
// Brightness/contrast
&__controls {
// Sliders and reset element
display: flex;
align-items: center;
margin-right: $interiorMargin; // Need some extra space due to proximity to close button
}
&__sliders {
display: flex;
flex: 1 1 auto;
flex-direction: column;
> * + * {
margin-top: 11px;
}
}
&__slider-wrapper {
// A wrapper is needed to add the type icon to left of each range input
display: flex;
align-items: center;
&:before {
color: rgba($colorMenuFg, 0.5);
margin-right: $interiorMarginSm;
}
input[type='range'] {
width: 100px;
}
}
&__btn-reset {
flex: 0 0 auto;
}
}
/*************************************** BUTTONS */
.c-button.pause-play {
// Pause icon set by default in markup
@@ -211,14 +255,13 @@
}
.c-imagery__prev-next-buttons {
//background: rgba(deeppink, 0.2);
display: flex;
width: 100%;
justify-content: space-between;
pointer-events: none;
position: absolute;
top: 50%;
transform: translateY(-50%);
transform: translateY(-75%);
.c-nav {
pointer-events: all;

View File

@@ -28,8 +28,6 @@ export default class NewFolderAction {
this.key = 'newFolder';
this.description = 'Create a new folder';
this.cssClass = 'icon-folder-new';
this.group = "action";
this.priority = 9;
this._openmct = openmct;
this._dialogForm = {

View File

@@ -23,6 +23,6 @@ import NewFolderAction from './newFolderAction';
export default function () {
return function (openmct) {
openmct.actions.register(new NewFolderAction(openmct));
openmct.contextMenu.registerAction(new NewFolderAction(openmct));
};
}

View File

@@ -40,7 +40,9 @@ describe("the plugin", () => {
openmct.on('start', done);
openmct.startHeadless();
newFolderAction = openmct.actions._allActions.newFolder;
newFolderAction = openmct.contextMenu._allActions.filter(action => {
return action.key === 'newFolder';
})[0];
});
afterEach(() => {

View File

@@ -1,39 +0,0 @@
import { getDefaultNotebook } from '../utils/notebook-storage';
import { addNotebookEntry } from '../utils/notebook-entries';
export default class CopyToNotebookAction {
constructor(openmct) {
this.openmct = openmct;
this.cssClass = 'icon-duplicate';
this.description = 'Copy to Notebook action';
this.group = "action";
this.key = 'copyToNotebook';
this.name = 'Copy to Notebook';
this.priority = 9;
}
copyToNotebook(entryText) {
const notebookStorage = getDefaultNotebook();
this.openmct.objects.get(notebookStorage.notebookMeta.identifier)
.then(domainObject => {
addNotebookEntry(this.openmct, domainObject, notebookStorage, null, entryText);
const defaultPath = `${domainObject.name} - ${notebookStorage.section.name} - ${notebookStorage.page.name}`;
const msg = `Saved to Notebook ${defaultPath}`;
this.openmct.notifications.info(msg);
});
}
invoke(objectPath, viewContext) {
this.copyToNotebook(viewContext.formattedValueForCopy());
}
appliesTo(objectPath, viewContext) {
if (viewContext && viewContext.getViewKey) {
return viewContext.getViewKey().includes('alphanumeric-format');
}
return false;
}
}

View File

@@ -112,6 +112,8 @@ import SearchResults from './SearchResults.vue';
import Sidebar from './Sidebar.vue';
import { clearDefaultNotebook, getDefaultNotebook, setDefaultNotebook, setDefaultNotebookSection, setDefaultNotebookPage } from '../utils/notebook-storage';
import { DEFAULT_CLASS, addNotebookEntry, createNewEmbed, getNotebookEntries } from '../utils/notebook-entries';
import objectUtils from 'objectUtils';
import { throttle } from 'lodash';
export default {
@@ -431,7 +433,9 @@ export default {
},
async updateDefaultNotebook(notebookStorage) {
const defaultNotebookObject = await this.getDefaultNotebookObject();
if (defaultNotebookObject.identifier.key !== notebookStorage.notebookMeta.identifier.key) {
if (!defaultNotebookObject) {
setDefaultNotebook(this.openmct, notebookStorage);
} else if (objectUtils.makeKeyString(defaultNotebookObject.identifier) !== objectUtils.makeKeyString(notebookStorage.notebookMeta.identifier)) {
this.removeDefaultClass(defaultNotebookObject);
setDefaultNotebook(this.openmct, notebookStorage);
}

View File

@@ -143,7 +143,8 @@ export default {
this.openmct.notifications.alert(message);
}
window.location.href = link;
const url = new URL(link);
window.location.href = url.hash;
},
formatTime(unixTime, timeFormat) {
return Moment.utc(unixTime).format(timeFormat);

View File

@@ -12,12 +12,11 @@
<div class="c-ne__content">
<div :id="entry.id"
class="c-ne__text"
:class="{'c-input-inline' : !readOnly }"
:class="{'c-ne__input' : !readOnly }"
:contenteditable="!readOnly"
:style="!entry.text.length ? defaultEntryStyle : ''"
@blur="updateEntryValue($event, entry.id)"
@focus="updateCurrentEntryValue($event, entry.id)"
>{{ entry.text.length ? entry.text : defaultText }}</div>
>{{ entry.text }}</div>
<div class="c-snapshots c-ne__embeds">
<NotebookEmbed v-for="embed in entry.embeds"
:key="embed.id"
@@ -106,12 +105,7 @@ export default {
},
data() {
return {
currentEntryValue: '',
defaultEntryStyle: {
fontStyle: 'italic',
color: '#6e6e6e'
},
defaultText: 'add description'
currentEntryValue: ''
};
},
computed: {
@@ -235,24 +229,13 @@ export default {
this.entry.embeds.splice(embedPosition, 1);
this.updateEntry(this.entry);
},
selectTextInsideElement(element) {
const range = document.createRange();
range.selectNodeContents(element);
let selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
},
updateCurrentEntryValue($event) {
if (this.readOnly) {
return;
}
const target = $event.target;
this.currentEntryValue = target ? target.innerText : '';
if (!this.entry.text.length) {
this.selectTextInsideElement(target);
}
this.currentEntryValue = target ? target.textContent : '';
},
updateEmbed(newEmbed) {
this.entry.embeds.some(e => {
@@ -292,6 +275,8 @@ export default {
const entryPos = this.entryPosById(entryId);
const value = target.textContent.trim();
if (this.currentEntryValue !== value) {
target.textContent = value;
const entries = getNotebookEntries(this.domainObject, this.selectedSection, this.selectedPage);
entries[entryPos].text = value;

View File

@@ -1,17 +1,29 @@
<template>
<div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left">
<button
class="c-icon-button c-button--menu icon-camera"
class="c-button--menu icon-notebook"
title="Take a Notebook Snapshot"
@click.stop.prevent="showMenu"
@click="setNotebookTypes"
@click.stop="toggleMenu"
>
<span
title="Take Notebook Snapshot"
class="c-icon-button__label"
>
Snapshot
</span>
<span class="c-button__label"></span>
</button>
<div
v-show="showMenu"
class="c-menu"
>
<ul>
<li
v-for="(type, index) in notebookTypes"
:key="index"
:class="type.cssClass"
:title="type.name"
@click="snapshot(type)"
>
{{ type.name }}
</li>
</ul>
</div>
</div>
</template>
@@ -45,19 +57,22 @@ export default {
data() {
return {
notebookSnapshot: null,
notebookTypes: []
notebookTypes: [],
showMenu: false
};
},
mounted() {
this.notebookSnapshot = new Snapshot(this.openmct);
document.addEventListener('click', this.hideMenu);
},
destroyed() {
document.removeEventListener('click', this.hideMenu);
},
methods: {
showMenu(event) {
setNotebookTypes() {
const notebookTypes = [];
const defaultNotebook = getDefaultNotebook();
const elementBoundingClientRect = this.$el.getBoundingClientRect();
const x = elementBoundingClientRect.x;
const y = elementBoundingClientRect.y + elementBoundingClientRect.height;
if (defaultNotebook) {
const domainObject = defaultNotebook.domainObject;
@@ -68,31 +83,35 @@ export default {
notebookTypes.push({
cssClass: 'icon-notebook',
name: `Save to Notebook ${defaultPath}`,
callBack: () => {
return this.snapshot(NOTEBOOK_DEFAULT);
}
type: NOTEBOOK_DEFAULT
});
}
}
notebookTypes.push({
cssClass: 'icon-camera',
cssClass: 'icon-notebook',
name: 'Save to Notebook Snapshots',
callBack: () => {
return this.snapshot(NOTEBOOK_SNAPSHOT);
}
type: NOTEBOOK_SNAPSHOT
});
this.openmct.menus.showMenu(x, y, notebookTypes);
this.notebookTypes = notebookTypes;
},
toggleMenu() {
this.showMenu = !this.showMenu;
},
hideMenu() {
this.showMenu = false;
},
snapshot(notebook) {
this.hideMenu();
this.$nextTick(() => {
const element = document.querySelector('.c-overlay__contents')
|| document.getElementsByClassName('l-shell__main-container')[0];
const bounds = this.openmct.time.bounds();
const link = !this.ignoreLink
? window.location.href
? window.location.hash
: null;
const objectPath = this.objectPath || this.openmct.router.path;

View File

@@ -4,7 +4,7 @@
<div class="l-browse-bar__start">
<div class="l-browse-bar__object-name--w">
<div class="l-browse-bar__object-name c-object-label">
<div class="c-object-label__type-icon icon-camera"></div>
<div class="c-object-label__type-icon icon-notebook"></div>
<div class="c-object-label__name">
Notebook Snapshots
<span v-if="snapshots.length"

View File

@@ -1,5 +1,5 @@
<template>
<div class="c-indicator c-indicator--clickable icon-camera"
<div class="c-indicator c-indicator--clickable icon-notebook"
:class="[
{ 's-status-off': snapshotCount === 0 },
{ 's-status-on': snapshotCount > 0 },

View File

@@ -1,4 +1,3 @@
import CopyToNotebookAction from './actions/CopyToNotebookAction';
import Notebook from './components/Notebook.vue';
import NotebookSnapshotIndicator from './components/NotebookSnapshotIndicator.vue';
import SnapshotContainer from './snapshot-container';
@@ -14,8 +13,6 @@ export default function NotebookPlugin() {
installed = true;
openmct.actions.register(new CopyToNotebookAction(openmct));
const notebookType = {
name: 'Notebook',
description: 'Create and save timestamped notes with embedded object snapshots.',

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,90 @@ describe("Notebook plugin:", () => {
expect(hasMajorElements).toBe(true);
});
});
describe("Notebook Snapshots view:", () => {
let snapshotIndicator;
let drawerElement;
function clickSnapshotIndicator() {
const indicator = element.querySelector('.icon-notebook');
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();
if (drawerElement) {
drawerElement.remove();
}
});
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');
const success = isExpandedBefore === false
&& isExpandedAfterFirstClick === true;
expect(success).toBe(true);
});
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');
const success = isExpandedBefore === false
&& isExpandedAfterFirstClick === true
&& isExpandedAfterSecondClick === false;
expect(success).toBe(true);
});
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

@@ -49,7 +49,7 @@ export default class Snapshot {
.then(domainObject => {
addNotebookEntry(this.openmct, domainObject, notebookStorage, embed);
const defaultPath = `${domainObject.name} - ${notebookStorage.section.name} - ${notebookStorage.page.name}`;
const defaultPath = `${domainObject.name} > ${notebookStorage.section.name} > ${notebookStorage.page.name}`;
const msg = `Saved to Notebook ${defaultPath}`;
this._showNotification(msg);
});

View File

@@ -103,7 +103,7 @@ export function createNewEmbed(snapshotMeta, snapshot = '') {
};
}
export function addNotebookEntry(openmct, domainObject, notebookStorage, embed = null, entryText = '') {
export function addNotebookEntry(openmct, domainObject, notebookStorage, embed = null) {
if (!openmct || !domainObject || !notebookStorage) {
return;
}
@@ -125,7 +125,7 @@ export function addNotebookEntry(openmct, domainObject, notebookStorage, embed =
defaultEntries.push({
id,
createdOn: date,
text: entryText,
text: '',
embeds
});

View File

@@ -86,7 +86,10 @@ export default class CouchObjectProvider {
this.objectQueue[key] = new CouchObjectQueue(undefined, response[REV]);
}
this.objectQueue[key].updateRevision(response[REV]);
//Sometimes CouchDB returns the old rev which fetching the object if there is a document update in progress
if (!this.objectQueue[key].pending) {
this.objectQueue[key].updateRevision(response[REV]);
}
return object;
} else {

View File

@@ -22,7 +22,7 @@
import CouchObjectProvider from './CouchObjectProvider';
const NAMESPACE = '';
const PERSISTENCE_SPACE = '';
const PERSISTENCE_SPACE = 'mct';
export default function CouchPlugin(url) {
return function install(openmct) {

View File

@@ -31,19 +31,18 @@ describe('the plugin', () => {
let element;
let child;
let provider;
let testSpace = 'testSpace';
let testPath = '/test/db';
let mockDomainObject;
beforeEach((done) => {
mockDomainObject = {
identifier: {
namespace: '',
namespace: 'mct',
key: 'some-value'
}
};
openmct = createOpenMct(false);
openmct.install(new CouchPlugin(testSpace, testPath));
openmct.install(new CouchPlugin(testPath));
element = document.createElement('div');
child = document.createElement('div');

View File

@@ -188,15 +188,19 @@
ng-style="{
right: (100 * (max - tick.value) / interval) + '%',
height: '100%'
}">
</div>
}"
ng-show="plot.gridLines"
>
</div>
</mct-ticks>
<mct-ticks axis="yAxis">
<div class="gl-plot-hash hash-h"
<div class="gl-plot-hash hash-h"
ng-repeat="tick in ticks track by tick.value"
ng-style="{ bottom: (100 * (tick.value - min) / interval) + '%', width: '100%' }">
</div>
ng-style="{ bottom: (100 * (tick.value - min) / interval) + '%', width: '100%' }"
ng-show="plot.gridLines"
>
</div>
</mct-ticks>
<mct-chart config="config"

View File

@@ -22,16 +22,16 @@
<div ng-controller="PlotController as controller"
class="c-plot holder holder-plot has-control-bar">
<div class="c-control-bar" ng-show="!controller.hideExportButtons">
<span class="c-button-set c-button-set--strip-h">
<span class="c-button-set c-button-set--strip-h">
<button class="c-button icon-download"
ng-click="controller.exportPNG()"
title="Export This View's Data as PNG">
<span class="c-button__label">PNG</span>
ng-click="controller.exportPNG()"
title="Export This View's Data as PNG">
<span class="c-button__label">PNG</span>
</button>
<button class="c-button"
ng-click="controller.exportJPG()"
title="Export This View's Data as JPG">
<span class="c-button__label">JPG</span>
ng-click="controller.exportJPG()"
title="Export This View's Data as JPG">
<span class="c-button__label">JPG</span>
</button>
</span>
<button class="c-button icon-crosshair"
@@ -39,9 +39,14 @@
ng-click="controller.toggleCursorGuide($event)"
title="Toggle cursor guides">
</button>
<button class="c-button"
ng-class="{ 'icon-grid-on': controller.gridLines, 'icon-grid-off': !controller.gridLines }"
ng-click="controller.toggleGridLines($event)"
title="Toggle grid lines">
</button>
</div>
<div class="l-view-section">
<div class="l-view-section u-style-receiver js-style-receiver">
<div class="c-loading--overlay loading"
ng-show="!!pending"></div>
<mct-plot config="controller.config"

View File

@@ -22,25 +22,30 @@
<div ng-controller="StackedPlotController as stackedPlot"
class="c-plot c-plot--stacked holder holder-plot has-control-bar">
<div class="c-control-bar" ng-show="!stackedPlot.hideExportButtons">
<span class="c-button-set c-button-set--strip-h">
<button class="c-button icon-download"
ng-click="stackedPlot.exportPNG()"
title="Export This View's Data as PNG">
<span class="c-button__label">PNG</span>
</button>
<button class="c-button"
ng-click="stackedPlot.exportJPG()"
title="Export This View's Data as JPG">
<span class="c-button__label">JPG</span>
</button>
<span class="c-button-set c-button-set--strip-h">
<button class="c-button icon-download"
ng-click="stackedPlot.exportPNG()"
title="Export This View's Data as PNG">
<span class="c-button__label">PNG</span>
</button>
<button class="c-button"
ng-click="stackedPlot.exportJPG()"
title="Export This View's Data as JPG">
<span class="c-button__label">JPG</span>
</button>
</span>
<button class="c-button icon-crosshair"
ng-class="{ 'is-active': stackedPlot.cursorGuide }"
ng-click="stackedPlot.toggleCursorGuide($event)"
title="Toggle cursor guides">
</button>
<button class="c-button"
ng-class="{ 'icon-grid-on': stackedPlot.gridLines, 'icon-grid-off': !stackedPlot.gridLines }"
ng-click="stackedPlot.toggleGridLines($event)"
title="Toggle grid lines">
</button>
</div>
<div class="l-view-section">
<div class="l-view-section u-style-receiver js-style-receiver">
<div class="c-loading--overlay loading"
ng-show="!!currentRequest.pending"></div>
<div class="gl-plot child-frame u-inspectable"

View File

@@ -96,7 +96,10 @@ define([
this.cursorGuideHorizontal = this.$element[0].querySelector('.js-cursor-guide--h');
this.cursorGuide = false;
this.gridLines = true;
this.listenTo(this.$scope, 'cursorguide', this.toggleCursorGuide, this);
this.listenTo(this.$scope, 'toggleGridLines', this.toggleGridLines, this);
this.listenTo(this.$scope, '$destroy', this.destroy, this);
this.listenTo(this.$scope, 'plot:tickWidth', this.onTickWidthChange, this);
@@ -554,6 +557,10 @@ define([
this.cursorGuide = !this.cursorGuide;
};
MCTPlotController.prototype.toggleGridLines = function ($event) {
this.gridLines = !this.gridLines;
};
MCTPlotController.prototype.getXKeyOption = function (key) {
return this.$scope.xKeyOptions.find(option => option.key === key);
};

View File

@@ -60,6 +60,7 @@ define([
this.objectService = objectService;
this.exportImageService = exportImageService;
this.cursorGuide = false;
this.gridLines = true;
$scope.pending = 0;
@@ -331,6 +332,11 @@ define([
this.$scope.$broadcast('cursorguide', $event);
};
PlotController.prototype.toggleGridLines = function ($event) {
this.gridLines = !this.gridLines;
this.$scope.$broadcast('toggleGridLines', $event);
};
return PlotController;
});

View File

@@ -160,5 +160,10 @@ define([], function () {
this.$scope.$broadcast('cursorguide', $event);
};
StackedPlotController.prototype.toggleGridLines = function ($event) {
this.gridLines = !this.gridLines;
this.$scope.$broadcast('toggleGridLines', $event);
};
return StackedPlotController;
});

View File

@@ -58,8 +58,7 @@ define([
'./newFolderAction/plugin',
'./persistence/couch/plugin',
'./defaultRootName/plugin',
'./timeline/plugin',
'./viewDatumAction/plugin'
'./timeline/plugin'
], function (
_,
UTCTimeSystem,
@@ -98,8 +97,7 @@ define([
NewFolderAction,
CouchDBPlugin,
DefaultRootName,
Timeline,
ViewDatumAction
Timeline
) {
const bundleMap = {
LocalStorage: 'platform/persistence/local',
@@ -193,7 +191,6 @@ define([
plugins.ISOTimeFormat = ISOTimeFormat.default;
plugins.DefaultRootName = DefaultRootName.default;
plugins.Timeline = Timeline.default;
plugins.ViewDatumAction = ViewDatumAction.default;
return plugins;
});

View File

@@ -25,8 +25,6 @@ export default class RemoveAction {
this.key = 'remove';
this.description = 'Remove this object from its containing object.';
this.cssClass = "icon-trash";
this.group = "action";
this.priority = 1;
this.openmct = openmct;
}
@@ -105,16 +103,6 @@ export default class RemoveAction {
let parentType = parent && this.openmct.types.get(parent.type);
let child = objectPath[0];
let locked = child.locked ? child.locked : parent && parent.locked;
let isEditing = this.openmct.editor.isEditing();
if (isEditing) {
let currentItemInView = this.openmct.router.path[0];
let domainObject = objectPath[0];
if (this.openmct.objects.areIdsEqual(currentItemInView.identifier, domainObject.identifier)) {
return false;
}
}
if (locked) {
return false;

View File

@@ -23,6 +23,6 @@ import RemoveAction from "./RemoveAction";
export default function () {
return function (openmct) {
openmct.actions.register(new RemoveAction(openmct));
openmct.contextMenu.registerAction(new RemoveAction(openmct));
};
}

View File

@@ -25,6 +25,7 @@ define([
'lodash',
'./collections/BoundedTableRowCollection',
'./collections/FilteredTableRowCollection',
'./TelemetryTableNameColumn',
'./TelemetryTableRow',
'./TelemetryTableColumn',
'./TelemetryTableUnitColumn',
@@ -34,6 +35,7 @@ define([
_,
BoundedTableRowCollection,
FilteredTableRowCollection,
TelemetryTableNameColumn,
TelemetryTableRow,
TelemetryTableColumn,
TelemetryTableUnitColumn,
@@ -71,6 +73,24 @@ define([
openmct.time.on('timeSystem', this.refreshData);
}
/**
* @private
*/
addNameColumn(telemetryObject, metadataValues) {
let metadatum = metadataValues.find(m => m.key === 'name');
if (!metadatum) {
metadatum = {
format: 'string',
key: 'name',
name: 'Name'
};
}
const column = new TelemetryTableNameColumn(this.openmct, telemetryObject, metadatum);
this.configuration.addSingleColumnForObject(telemetryObject, column);
}
initialize() {
if (this.domainObject.type === 'table') {
this.filterObserver = this.openmct.objects.observe(this.domainObject, 'configuration.filters', this.updateFilters);
@@ -160,6 +180,7 @@ define([
processHistoricalData(telemetryData, columnMap, keyString, limitEvaluator) {
let telemetryRows = telemetryData.map(datum => new TelemetryTableRow(datum, columnMap, keyString, limitEvaluator));
this.boundedRows.add(telemetryRows);
this.emit('historical-rows-processed');
}
/**
@@ -211,7 +232,13 @@ define([
addColumnsForObject(telemetryObject) {
let metadataValues = this.openmct.telemetry.getMetadata(telemetryObject).values();
this.addNameColumn(telemetryObject, metadataValues);
metadataValues.forEach(metadatum => {
if (metadatum.key === 'name') {
return;
}
let column = this.createColumn(metadatum);
this.configuration.addSingleColumnForObject(telemetryObject, column);
// add units column if available

View File

@@ -1,5 +1,5 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2020, United States Government
* Open MCT, Copyright (c) 2014-2018, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
@@ -19,11 +19,26 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define([
'./TelemetryTableColumn.js'
], function (
TelemetryTableColumn
) {
class TelemetryTableNameColumn extends TelemetryTableColumn {
constructor(openmct, telemetryObject, metadatum) {
super(openmct, metadatum);
import ViewDatumAction from './ViewDatumAction.js';
this.telemetryObject = telemetryObject;
}
export default function plugin() {
return function install(openmct) {
openmct.actions.register(new ViewDatumAction(openmct));
};
}
getRawValue() {
return this.telemetryObject.name;
}
getFormattedValue() {
return this.telemetryObject.name;
}
}
return TelemetryTableNameColumn;
});

View File

@@ -26,7 +26,6 @@ define([], function () {
this.columns = columns;
this.datum = createNormalizedDatum(datum, columns);
this.fullDatum = datum;
this.limitEvaluator = limitEvaluator;
this.objectKeyString = objectKeyString;
}
@@ -88,7 +87,7 @@ define([], function () {
}
getContextMenuActions() {
return ['viewDatumAction'];
return [];
}
}

View File

@@ -54,13 +54,15 @@ define([
view(domainObject, objectPath) {
let table = new TelemetryTable(domainObject, openmct);
let component;
let markingProp = {
enable: true,
useAlternateControlBar: false,
rowName: '',
rowNamePlural: ''
};
const view = {
return {
show: function (element, editMode) {
component = new Vue({
el: element,
@@ -76,10 +78,9 @@ define([
provide: {
openmct,
table,
objectPath,
view
objectPath
},
template: '<table-component ref="tableComponent" :isEditing="isEditing" :marking="markingProp"/>'
template: '<table-component :isEditing="isEditing" :marking="markingProp"/>'
});
},
onEditModeChange(editMode) {
@@ -88,22 +89,11 @@ define([
onClearData() {
table.clearData();
},
getViewContext() {
if (component) {
return component.$refs.tableComponent.getViewContext();
} else {
return {
type: 'telemetry-table'
};
}
},
destroy: function (element) {
component.$destroy();
component = undefined;
}
};
return view;
},
priority() {
return 1;

View File

@@ -1,123 +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.
*****************************************************************************/
let exportCSV = {
name: 'Export Table Data',
key: 'export-csv-all',
description: "Export this view's data",
cssClass: 'icon-download labeled',
invoke: (objectPath, viewProvider) => {
viewProvider.getViewContext().exportAllDataAsCSV();
},
group: 'view'
};
let exportMarkedRows = {
name: 'Export Marked Rows',
key: 'export-csv-marked',
description: "Export marked rows as CSV",
cssClass: 'icon-download labeled',
invoke: (objectPath, viewProvider) => {
viewProvider.getViewContext().exportMarkedDataAsCSV();
},
group: 'view'
};
let unmarkAllRows = {
name: 'Unmark All Rows',
key: 'unmark-all-rows',
description: 'Unmark all rows',
cssClass: 'icon-x labeled',
invoke: (objectPath, viewProvider) => {
viewProvider.getViewContext().unmarkAllRows();
},
showInStatusBar: true,
group: 'view'
};
let pause = {
name: 'Pause',
key: 'pause-data',
description: 'Pause real-time data flow',
cssClass: 'icon-pause',
invoke: (objectPath, viewProvider) => {
viewProvider.getViewContext().togglePauseByButton();
},
showInStatusBar: true,
group: 'view'
};
let play = {
name: 'Play',
key: 'play-data',
description: 'Continue real-time data flow',
cssClass: 'c-button pause-play is-paused',
invoke: (objectPath, viewProvider) => {
viewProvider.getViewContext().togglePauseByButton();
},
showInStatusBar: true,
group: 'view'
};
let expandColumns = {
name: 'Expand Columns',
key: 'expand-columns',
description: "Increase column widths to fit currently available data.",
cssClass: 'icon-arrows-right-left labeled',
invoke: (objectPath, viewProvider) => {
viewProvider.getViewContext().expandColumns();
},
showInStatusBar: true,
group: 'view'
};
let autosizeColumns = {
name: 'Autosize Columns',
key: 'autosize-columns',
description: "Automatically size columns to fit the table into the available space.",
cssClass: 'icon-expand labeled',
invoke: (objectPath, viewProvider) => {
viewProvider.getViewContext().autosizeColumns();
},
showInStatusBar: true,
group: 'view'
};
let viewActions = [
exportCSV,
exportMarkedRows,
unmarkAllRows,
pause,
play,
expandColumns,
autosizeColumns
];
viewActions.forEach(action => {
action.appliesTo = (objectPath, viewProvider = {}) => {
let viewContext = viewProvider.getViewContext && viewProvider.getViewContext();
if (viewContext) {
let type = viewContext.type;
return type === 'telemetry-table';
}
return false;
};
});
export default viewActions;

View File

@@ -0,0 +1,54 @@
<template>
<tr class="c-telemetry-table__sizing-tr"><td>SIZING ROW</td></tr>
</template>
<script>
export default {
props: {
isEditing: {
type: Boolean,
default: false
}
},
watch: {
isEditing: function (isEditing) {
if (isEditing) {
this.pollForRowHeight();
} else {
this.clearPoll();
}
}
},
mounted() {
this.$nextTick().then(() => {
this.height = this.$el.offsetHeight;
this.$emit('change-height', this.height);
});
if (this.isEditing) {
this.pollForRowHeight();
}
},
destroyed() {
this.clearPoll();
},
methods: {
pollForRowHeight() {
this.clearPoll();
this.pollID = window.setInterval(this.heightPoll, 300);
},
clearPoll() {
if (this.pollID) {
window.clearInterval(this.pollID);
this.pollID = undefined;
}
},
heightPoll() {
let height = this.$el.offsetHeight;
if (height !== this.height) {
this.$emit('change-height', height);
this.height = height;
}
}
}
};
</script>

View File

@@ -0,0 +1,29 @@
.c-table-indicator {
display: flex;
align-items: center;
font-size: 0.9em;
overflow: hidden;
&__elem {
@include ellipsize();
flex: 0 1 auto;
padding: 2px;
text-transform: uppercase;
> * {
//display: contents;
}
}
&__counts {
//background: rgba(deeppink, 0.1);
display: flex;
flex: 1 1 auto;
justify-content: flex-end;
overflow: hidden;
> * {
margin-left: $interiorMargin;
}
}
}

View File

@@ -1,18 +1,41 @@
<template>
<div
v-if="filterNames.length > 0"
:title="title"
class="c-filter-indication"
:class="{ 'c-filter-indication--mixed': hasMixedFilters }"
class="c-table-indicator"
:class="{ 'is-filtering': filterNames.length > 0 }"
>
<span class="c-filter-indication__mixed">{{ label }}</span>
<span
v-for="(name, index) in filterNames"
:key="index"
class="c-filter-indication__label"
<div
v-if="filterNames.length > 0"
class="c-table-indicator__filter c-table-indicator__elem c-filter-indication"
:class="{ 'c-filter-indication--mixed': hasMixedFilters }"
:title="title"
>
{{ name }}
</span>
<span class="c-filter-indication__mixed">{{ label }}</span>
<span
v-for="(name, index) in filterNames"
:key="index"
class="c-filter-indication__label"
>
{{ name }}
</span>
</div>
<div class="c-table-indicator__counts">
<span
:title="totalRows + ' rows visible after any filtering'"
class="c-table-indicator__elem c-table-indicator__row-count"
>
{{ totalRows }} Rows
</span>
<span
v-if="markedRows"
class="c-table-indicator__elem c-table-indicator__marked-count"
:title="markedRows + ' rows selected'"
>
{{ markedRows }} Marked
</span>
</div>
</div>
</template>
@@ -27,6 +50,16 @@ const USE_GLOBAL = 'useGlobal';
export default {
inject: ['openmct', 'table'],
props: {
markedRows: {
type: Number,
default: 0
},
totalRows: {
type: Number,
default: 0
}
},
data() {
return {
filterNames: [],

View File

@@ -102,17 +102,7 @@ export default {
selectable[columnKeys] = this.row.columns[columnKeys].selectable;
return selectable;
}, {}),
actionsViewContext: {
getViewContext: () => {
return {
viewHistoricalData: true,
viewDatumAction: true,
getDatum: this.getDatum,
skipCache: true
};
}
}
}, {})
};
},
computed: {
@@ -180,24 +170,14 @@ export default {
event.stopPropagation();
}
},
getDatum() {
return this.row.fullDatum;
},
showContextMenu: function (event) {
event.preventDefault();
this.markRow(event);
this.row.getContextualDomainObject(this.openmct, this.row.objectKeyString).then(domainObject => {
let contextualObjectPath = this.objectPath.slice();
contextualObjectPath.unshift(domainObject);
let allActions = this.openmct.actions.get(contextualObjectPath, this.actionsViewContext);
let applicableActions = this.row.getContextMenuActions().map(key => allActions[key]);
if (applicableActions.length) {
this.openmct.menus.showMenu(event.x, event.y, applicableActions);
}
this.openmct.contextMenu._showContextMenuForObjectPath(contextualObjectPath, event.x, event.y, this.row.getContextMenuActions());
});
}
}

View File

@@ -9,6 +9,9 @@
.c-telemetry-table {
// Table that displays telemetry in a scrolling body area
@include fontAndSize();
display: flex;
flex-flow: column nowrap;
justify-content: flex-start;
@@ -108,7 +111,7 @@
display: flex; // flex-flow defaults to row nowrap (which is what we want) so no need to define
align-items: stretch;
position: absolute;
height: 18px; // Needed when a row has empty values in its cells
min-height: 18px; // Needed when a row has empty values in its cells
.is-editing .l-layout__frame & {
pointer-events: none;
@@ -150,6 +153,41 @@
white-space: nowrap;
}
}
&__sizing-tr {
// A row element used to determine sizing of rows based on font size
visibility: hidden;
pointer-events: none;
}
&__footer {
$pt: 2px;
border-top: 1px solid $colorInteriorBorder;
margin-top: $interiorMargin;
padding: $pt 0;
overflow: hidden;
transition: all 250ms;
&:not(.is-filtering) {
.c-frame & {
height: 0;
padding: 0;
visibility: hidden;
}
}
}
.c-frame & {
// target .c-frame .c-telemetry-table {}
$pt: 2px;
&:hover {
.c-telemetry-table__footer:not(.is-filtering) {
height: $pt + 16px;
padding: initial;
visibility: visible;
}
}
}
}
/******************************* SPECIFIC CASE WRAPPERS */

View File

@@ -23,6 +23,79 @@
<div class="c-table-wrapper"
:class="{ 'is-paused': paused }"
>
<!-- main contolbar start-->
<div v-if="!marking.useAlternateControlBar"
class="c-table-control-bar c-control-bar"
>
<button
v-if="allowExport"
v-show="!markedRows.length"
class="c-button icon-download labeled"
title="Export this view's data"
@click="exportAllDataAsCSV()"
>
<span class="c-button__label">Export Table Data</span>
</button>
<button
v-if="allowExport"
v-show="markedRows.length"
class="c-button icon-download labeled"
title="Export marked rows as CSV"
@click="exportMarkedDataAsCSV()"
>
<span class="c-button__label">Export Marked Rows</span>
</button>
<button
v-show="markedRows.length"
class="c-button icon-x labeled"
title="Unmark all rows"
@click="unmarkAllRows()"
>
<span class="c-button__label">Unmark All Rows</span>
</button>
<div
v-if="marking.enable"
class="c-separator"
></div>
<button
v-if="marking.enable"
class="c-button icon-pause pause-play labeled"
:class=" paused ? 'icon-play is-paused' : 'icon-pause'"
:title="paused ? 'Continue real-time data flow' : 'Pause real-time data flow'"
@click="togglePauseByButton()"
>
<span class="c-button__label">
{{ paused ? 'Play' : 'Pause' }}
</span>
</button>
<template v-if="!isEditing">
<div
class="c-separator"
>
</div>
<button
v-if="isAutosizeEnabled"
class="c-button icon-arrows-right-left labeled"
title="Increase column widths to fit currently available data."
@click="recalculateColumnWidths"
>
<span class="c-button__label">Expand Columns</span>
</button>
<button
v-else
class="c-button icon-expand labeled"
title="Automatically size columns to fit the table into the available space."
@click="autosizeColumns"
>
<span class="c-button__label">Autosize Columns</span>
</button>
</template>
<slot name="buttons"></slot>
</div>
<!-- main controlbar end -->
<!-- alternate controlbar start -->
<div v-if="marking.useAlternateControlBar"
class="c-table-control-bar c-control-bar"
@@ -40,11 +113,11 @@
<button
:class="{'hide-nice': !markedRows.length}"
class="c-icon-button icon-x labeled"
class="c-button icon-x labeled"
title="Deselect All"
@click="unmarkAllRows()"
>
<span class="c-icon-button__label">{{ `Deselect ${marking.disableMultiSelect ? '' : 'All'}` }} </span>
<span class="c-button__label">{{ `Deselect ${marking.disableMultiSelect ? '' : 'All'}` }} </span>
</button>
<slot name="buttons"></slot>
@@ -52,7 +125,7 @@
<!-- alternate controlbar end -->
<div
class="c-table c-telemetry-table c-table--filterable c-table--sortable has-control-bar"
class="c-table c-telemetry-table c-table--filterable c-table--sortable has-control-bar u-style-receiver js-style-receiver"
:class="{
'loading': loading,
'is-paused' : paused
@@ -161,6 +234,10 @@
class="c-telemetry-table__sizing js-telemetry-table__sizing"
:style="sizingTableWidth"
>
<sizing-row
:is-editing="isEditing"
@change-height="setRowHeight"
/>
<tr>
<template v-for="(title, key) in headers">
<th
@@ -180,7 +257,11 @@
:object-path="objectPath"
/>
</table>
<telemetry-filter-indicator />
<table-footer-indicator
class="c-telemetry-table__footer"
:marked-rows="markedRows.length"
:total-rows="totalNumberOfRows"
/>
</div>
</div><!-- closes c-table-wrapper -->
</template>
@@ -189,10 +270,11 @@
import TelemetryTableRow from './table-row.vue';
import search from '../../../ui/components/search.vue';
import TableColumnHeader from './table-column-header.vue';
import TelemetryFilterIndicator from './TelemetryFilterIndicator.vue';
import TableFooterIndicator from './table-footer-indicator.vue';
import CSVExporter from '../../../exporters/CSVExporter.js';
import _ from 'lodash';
import ToggleSwitch from '../../../ui/components/ToggleSwitch.vue';
import SizingRow from './sizing-row.vue';
const VISIBLE_ROW_COUNT = 100;
const ROW_HEIGHT = 17;
@@ -204,10 +286,11 @@ export default {
TelemetryTableRow,
TableColumnHeader,
search,
TelemetryFilterIndicator,
ToggleSwitch
TableFooterIndicator,
ToggleSwitch,
SizingRow
},
inject: ['table', 'openmct', 'objectPath', 'view'],
inject: ['table', 'openmct', 'objectPath'],
props: {
isEditing: {
type: Boolean,
@@ -269,7 +352,8 @@ export default {
paused: false,
markedRows: [],
isShowingMarkedRowsOnly: false,
hideHeaders: configuration.hideHeaders
hideHeaders: configuration.hideHeaders,
totalNumberOfRows: 0
};
},
computed: {
@@ -310,34 +394,6 @@ export default {
markedRows: {
handler(newVal, oldVal) {
this.$emit('marked-rows-updated', newVal, oldVal);
if (newVal.length > 0) {
this.viewActionsCollection.enable(['export-csv-marked', 'unmark-all-rows']);
} else if (newVal.length === 0) {
this.viewActionsCollection.disable(['export-csv-marked', 'unmark-all-rows']);
}
}
},
paused: {
handler(newVal) {
if (newVal) {
this.viewActionsCollection.hide(['pause-data']);
this.viewActionsCollection.show(['play-data']);
} else {
this.viewActionsCollection.hide(['play-data']);
this.viewActionsCollection.show(['pause-data']);
}
}
},
isAutosizeEnabled: {
handler(newVal) {
if (newVal) {
this.viewActionsCollection.show(['expand-columns']);
this.viewActionsCollection.hide(['autosize-columns']);
} else {
this.viewActionsCollection.show(['autosize-columns']);
this.viewActionsCollection.hide(['expand-columns']);
}
}
}
},
@@ -350,9 +406,6 @@ export default {
this.rowsRemoved = _.throttle(this.rowsRemoved, 200);
this.scroll = _.throttle(this.scroll, 100);
this.viewActionsCollection = this.openmct.actions.get(this.objectPath, this.view);
this.initializeViewActions();
this.table.on('object-added', this.addObject);
this.table.on('object-removed', this.removeObject);
this.table.on('outstanding-requests', this.outstandingRequests);
@@ -409,6 +462,8 @@ export default {
let filteredRows = this.table.filteredRows.getRows();
let filteredRowsLength = filteredRows.length;
this.totalNumberOfRows = filteredRowsLength;
if (filteredRowsLength < VISIBLE_ROW_COUNT) {
end = filteredRowsLength;
} else {
@@ -457,7 +512,7 @@ export default {
let columnWidths = {};
let totalWidth = 0;
let headerKeys = Object.keys(this.headers);
let sizingTableRow = this.sizingTable.children[0];
let sizingTableRow = this.sizingTable.children[1];
let sizingCells = sizingTableRow.children;
headerKeys.forEach((headerKey, headerIndex, array) => {
@@ -791,7 +846,7 @@ export default {
for (let i = firstRowIndex; i <= lastRowIndex; i++) {
let row = allRows[i];
this.$set(row, 'marked', true);
row.marked = true;
if (row !== baseRow) {
this.markedRows.push(row);
@@ -853,39 +908,11 @@ export default {
this.$nextTick().then(this.calculateColumnWidths);
},
getViewContext() {
return {
type: 'telemetry-table',
exportAllDataAsCSV: this.exportAllDataAsCSV,
exportMarkedRows: this.exportMarkedRows,
unmarkAllRows: this.unmarkAllRows,
togglePauseByButton: this.togglePauseByButton,
expandColumns: this.recalculateColumnWidths,
autosizeColumns: this.autosizeColumns
};
},
initializeViewActions() {
if (this.markedRows.length > 0) {
this.viewActionsCollection.enable(['export-csv-marked', 'unmark-all-rows']);
} else if (this.markedRows.length === 0) {
this.viewActionsCollection.disable(['export-csv-marked', 'unmark-all-rows']);
}
if (this.paused) {
this.viewActionsCollection.hide(['pause-data']);
this.viewActionsCollection.show(['play-data']);
} else {
this.viewActionsCollection.hide(['play-data']);
this.viewActionsCollection.show(['pause-data']);
}
if (this.isAutosizeEnabled) {
this.viewActionsCollection.show(['expand-columns']);
this.viewActionsCollection.hide(['autosize-columns']);
} else {
this.viewActionsCollection.show(['autosize-columns']);
this.viewActionsCollection.hide(['expand-columns']);
}
setRowHeight(height) {
this.rowHeight = height;
this.setHeight();
this.calculateTableSize();
this.clearRowsAndRerender();
}
}
};

View File

@@ -1,37 +0,0 @@
.c-filter-indication {
@include userSelectNone();
background: $colorFilterBg;
color: $colorFilterFg;
display: flex;
align-items: center;
font-size: 0.9em;
margin-top: $interiorMarginSm;
padding: 2px;
text-transform: uppercase;
&:before {
font-family: symbolsfont-12px;
content: $glyph-icon-filter;
display: block;
font-size: 12px;
margin-right: $interiorMarginSm;
}
&__mixed {
margin-right: $interiorMarginSm;
}
&--mixed {
.c-filter-indication__mixed {
font-style: italic;
}
}
&__label {
+ .c-filter-indication__label {
&:before {
content: ',';
}
}
}
}

View File

@@ -23,13 +23,11 @@
define([
'./TelemetryTableViewProvider',
'./TableConfigurationViewProvider',
'./TelemetryTableType',
'./ViewActions'
'./TelemetryTableType'
], function (
TelemetryTableViewProvider,
TableConfigurationViewProvider,
TelemetryTableType,
TelemetryTableViewActions
TelemetryTableType
) {
return function plugin() {
return function install(openmct) {
@@ -43,10 +41,6 @@ define([
return true;
}
});
TelemetryTableViewActions.default.forEach(action => {
openmct.actions.register(action);
});
};
};
});

View File

@@ -168,8 +168,6 @@ describe("the plugin", () => {
return telemetryPromise;
});
openmct.router.path = [testTelemetryObject];
applicableViews = openmct.objectViews.get(testTelemetryObject);
tableViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'table');
tableView = tableViewProvider.view(testTelemetryObject, [testTelemetryObject]);
@@ -185,10 +183,11 @@ describe("the plugin", () => {
it("Renders a column for every item in telemetry metadata", () => {
let headers = element.querySelectorAll('span.c-telemetry-table__headers__label');
expect(headers.length).toBe(3);
expect(headers[0].innerText).toBe('Time');
expect(headers[1].innerText).toBe('Some attribute');
expect(headers[2].innerText).toBe('Another attribute');
expect(headers.length).toBe(4);
expect(headers[0].innerText).toBe('Name');
expect(headers[1].innerText).toBe('Time');
expect(headers[2].innerText).toBe('Some attribute');
expect(headers[3].innerText).toBe('Another attribute');
});
it("Supports column reordering via drag and drop", () => {

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