Role selection for operator status roles (#6706)

* Add additional test roles to example user

* Add session storage and role to user indicator

* Update example user provider

* Added selection dialog to overlays and implemented for operator status

* Display role in user indicator

* Updates to broadcast channel lifecycle

* Update comment

* Comment width

* UserAPI role updates and UserIndicator improvement

* Moved prompt to UserIndicator

* Reconnect channel on error and UserIndicator updates

* Updates to status api canPRovideStatusForRole

* Cleanup

* Store status roles in an array instead of a singular value

* Added success notification and cleanup

* Lint

* Removed unused role param from status api call

* Remove default status role from example user plugin

* Removed status.getStatusRoleForCurrentUser

* Cleanup

* Cleanup

* Moved roleChannel to private field

* Separated input value from active role value

* More flight like status role names and parameter names

* Update statusRole parameter name

* Update default selection for roles if input is not chosen

* Update OperatorStatusIndicator install to hide if an observer

* console.log

* Return null instead of undefined

* Remove unneccesary filter on allRoles

* refactor: format with prettier

* Undid merge error

* Merge conflict extra line

* Copyright statement

* RoleChannelProvider to RoleChannel

* Throw error on no provider

* Change RoleChannel to ActiveRoleSynchronizer and update method calls to match

* iconClass to alert

* Add role selection step to beforeEach

* example-role to flight

* Dismiss overlay from exampleUser plugin which affected menu api positioning

---------

Co-authored-by: Scott Bell <scott@traclabs.com>
This commit is contained in:
Michael Rogers 2023-07-14 14:10:58 -05:00 committed by GitHub
parent 92329b3d8e
commit 32529ff6b2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 434 additions and 70 deletions

View File

@ -47,6 +47,11 @@ test.describe('Operator Status', () => {
path: path.join(__dirname, '../../../../helper/', 'addInitOperatorStatus.js')
});
await page.goto('./', { waitUntil: 'domcontentloaded' });
await expect(page.getByText('Select Role')).toBeVisible();
// set role
await page.getByRole('button', { name: 'Select' }).click();
// dismiss role confirmation popup
await page.getByRole('button', { name: 'Dismiss' }).click();
});
// verify that operator status is visible

View File

@ -63,16 +63,24 @@ const STATUSES = [
* @implements {StatusUserProvider}
*/
export default class ExampleUserProvider extends EventEmitter {
constructor(openmct, { defaultStatusRole } = { defaultStatusRole: undefined }) {
constructor(
openmct,
{ statusRoles } = {
statusRoles: []
}
) {
super();
this.openmct = openmct;
this.user = undefined;
this.loggedIn = false;
this.autoLoginUser = undefined;
this.status = STATUSES[0];
this.statusRoleValues = statusRoles.map((role) => ({
role: role,
status: STATUSES[0]
}));
this.pollQuestion = undefined;
this.defaultStatusRole = defaultStatusRole;
this.statusRoles = statusRoles;
this.ExampleUser = createExampleUser(this.openmct.user.User);
this.loginPromise = undefined;
@ -94,14 +102,13 @@ export default class ExampleUserProvider extends EventEmitter {
return this.loginPromise;
}
canProvideStatusForRole() {
return Promise.resolve(true);
canProvideStatusForRole(role) {
return this.statusRoles.includes(role);
}
canSetPollQuestion() {
return Promise.resolve(true);
}
hasRole(roleId) {
if (!this.loggedIn) {
Promise.resolve(undefined);
@ -110,16 +117,18 @@ export default class ExampleUserProvider extends EventEmitter {
return Promise.resolve(this.user.getRoles().includes(roleId));
}
getStatusRoleForCurrentUser() {
return Promise.resolve(this.defaultStatusRole);
getPossibleRoles() {
return this.user.getRoles();
}
getAllStatusRoles() {
return Promise.resolve([this.defaultStatusRole]);
return Promise.resolve(this.statusRoles);
}
getStatusForRole(role) {
return Promise.resolve(this.status);
const statusForRole = this.statusRoleValues.find((statusRole) => statusRole.role === role);
return Promise.resolve(statusForRole?.status);
}
async getDefaultStatusForRole(role) {
@ -130,7 +139,8 @@ export default class ExampleUserProvider extends EventEmitter {
setStatusForRole(role, status) {
status.timestamp = Date.now();
this.status = status;
const matchingIndex = this.statusRoleValues.findIndex((statusRole) => statusRole.role === role);
this.statusRoleValues[matchingIndex].status = status;
this.emit('statusChange', {
role,
status
@ -175,7 +185,7 @@ export default class ExampleUserProvider extends EventEmitter {
// for testing purposes, this will skip the form, this wouldn't be used in
// a normal authentication process
if (this.autoLoginUser) {
this.user = new this.ExampleUser(id, this.autoLoginUser, ['example-role']);
this.user = new this.ExampleUser(id, this.autoLoginUser, ['flight', 'driver', 'observer']);
this.loggedIn = true;
return Promise.resolve();

View File

@ -21,16 +21,18 @@
*****************************************************************************/
import ExampleUserProvider from './ExampleUserProvider';
const AUTO_LOGIN_USER = 'mct-user';
const STATUS_ROLES = ['flight', 'driver'];
export default function ExampleUserPlugin(
{ autoLoginUser, defaultStatusRole } = {
autoLoginUser: 'guest',
defaultStatusRole: 'test-role'
{ autoLoginUser, statusRoles } = {
autoLoginUser: AUTO_LOGIN_USER,
statusRoles: STATUS_ROLES
}
) {
return function install(openmct) {
const userProvider = new ExampleUserProvider(openmct, {
defaultStatusRole
statusRoles
});
if (autoLoginUser !== undefined) {

View File

@ -1,6 +1,29 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, 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 Overlay from './Overlay';
import Dialog from './Dialog';
import ProgressDialog from './ProgressDialog';
import Selection from './Selection';
/**
* The OverlayAPI is responsible for pre-pending templates to
@ -130,6 +153,13 @@ class OverlayAPI {
return progressDialog;
}
selection(options) {
let selection = new Selection(options);
this.showOverlay(selection);
return selection;
}
}
export default OverlayAPI;

View File

@ -0,0 +1,67 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, 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 SelectionComponent from './components/SelectionComponent.vue';
import Overlay from './Overlay';
import Vue from 'vue';
class Selection extends Overlay {
constructor({
iconClass,
title,
message,
selectionOptions,
onChange,
currentSelection,
...options
}) {
let component = new Vue({
components: {
SelectionComponent: SelectionComponent
},
provide: {
iconClass,
title,
message,
selectionOptions,
onChange,
currentSelection
},
template: '<selection-component></selection-component>'
}).$mount();
super({
element: component.$el,
size: 'fit',
dismissable: false,
onChange,
currentSelection,
...options
});
this.once('destroy', () => {
component.$destroy();
});
}
}
export default Selection;

View File

@ -0,0 +1,34 @@
<template>
<div class="c-message">
<!--Uses flex-row -->
<div class="c-message__icon" :class="['u-icon-bg-color-' + iconClass]"></div>
<div class="c-message__text">
<!-- Uses flex-column -->
<div v-if="title" class="c-message__title">
{{ title }}
</div>
<div v-if="message" class="c-message__action-text">
{{ message }}
</div>
<select @change="onChange">
<option
v-for="option in selectionOptions"
:key="option.key"
:value="option.key"
:selected="option.key === currentSelection"
>
{{ option.name }}
</option>
</select>
<slot></slot>
</div>
</div>
</template>
<script>
export default {
inject: ['iconClass', 'title', 'message', 'selectionOptions', 'currentSelection', 'onChange']
};
</script>

View File

@ -0,0 +1,37 @@
import { ACTIVE_ROLE_BROADCAST_CHANNEL_NAME } from './constants';
class ActiveRoleSynchronizer {
#roleChannel;
constructor(openmct) {
this.openmct = openmct;
this.#roleChannel = new BroadcastChannel(ACTIVE_ROLE_BROADCAST_CHANNEL_NAME);
this.setActiveRoleFromChannelMessage = this.setActiveRoleFromChannelMessage.bind(this);
this.subscribeToRoleChanges(this.setActiveRoleFromChannelMessage);
}
subscribeToRoleChanges(callback) {
this.#roleChannel.addEventListener('message', callback);
}
unsubscribeFromRoleChanges(callback) {
this.#roleChannel.removeEventListener('message', callback);
}
setActiveRoleFromChannelMessage(event) {
const role = event.data;
this.openmct.user.setActiveRole(role);
}
broadcastNewRole(role) {
if (!this.#roleChannel.name) {
return false;
}
this.#roleChannel.postMessage(role);
}
destroy() {
this.unsubscribeFromRoleChanges(this.setActiveRoleFromChannelMessage);
this.#roleChannel.close();
}
}
export default ActiveRoleSynchronizer;

View File

@ -140,9 +140,9 @@ export default class StatusAPI extends EventEmitter {
const provider = this.#userAPI.getProvider();
if (provider.canProvideStatusForRole) {
return provider.canProvideStatusForRole(role);
return Promise.resolve(provider.canProvideStatusForRole(role));
} else {
return false;
return Promise.resolve(false);
}
}
@ -151,11 +151,16 @@ export default class StatusAPI extends EventEmitter {
* @param {Status} status The status to set for the provided role
* @returns {Promise<Boolean>} true if operation was successful, otherwise false.
*/
setStatusForRole(role, status) {
setStatusForRole(status) {
const provider = this.#userAPI.getProvider();
if (provider.setStatusForRole) {
return provider.setStatusForRole(role, status);
const activeRole = this.#userAPI.getActiveRole();
if (!provider.canProvideStatusForRole(activeRole)) {
return false;
}
return provider.setStatusForRole(activeRole, status);
} else {
this.#userAPI.error('User provider does not support setting role status');
}
@ -216,21 +221,6 @@ export default class StatusAPI extends EventEmitter {
}
}
/**
* The status role of the current user. A user may have multiple roles, but will only have one role
* that provides status at any time.
* @returns {Promise<import("./UserAPI").Role>} the role for which the current user can provide status.
*/
getStatusRoleForCurrentUser() {
const provider = this.#userAPI.getProvider();
if (provider.getStatusRoleForCurrentUser) {
return provider.getStatusRoleForCurrentUser();
} else {
this.#userAPI.error('User provider cannot provide role status for this user');
}
}
/**
* @returns {Promise<Boolean>} true if the configured UserProvider can provide status for the currently logged in user, false otherwise.
* @see StatusUserProvider
@ -238,14 +228,13 @@ export default class StatusAPI extends EventEmitter {
async canProvideStatusForCurrentUser() {
const provider = this.#userAPI.getProvider();
if (provider.getStatusRoleForCurrentUser) {
const activeStatusRole = await this.#userAPI.getProvider().getStatusRoleForCurrentUser();
const canProvideStatus = await this.canProvideStatusForRole(activeStatusRole);
return canProvideStatus;
} else {
if (!provider) {
return false;
}
const activeStatusRole = await this.#userAPI.getActiveRole();
const canProvideStatus = await this.canProvideStatusForRole(activeStatusRole);
return canProvideStatus;
}
/**

View File

@ -77,5 +77,4 @@ export default class StatusUserProvider extends UserProvider {
/**
* @returns {Promise<import("./UserAPI").Role>} the active status role for the currently logged in user
*/
async getStatusRoleForCurrentUser() {}
}

View File

@ -0,0 +1,37 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2023, 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 { ACTIVE_ROLE_LOCAL_STORAGE_KEY } from './constants';
class StoragePersistance {
getActiveRole() {
return localStorage.getItem(ACTIVE_ROLE_LOCAL_STORAGE_KEY);
}
setActiveRole(role) {
return localStorage.setItem(ACTIVE_ROLE_LOCAL_STORAGE_KEY, role);
}
clearActiveRole() {
return localStorage.removeItem(ACTIVE_ROLE_LOCAL_STORAGE_KEY);
}
}
export default new StoragePersistance();

View File

@ -24,6 +24,7 @@ import EventEmitter from 'EventEmitter';
import { MULTIPLE_PROVIDER_ERROR, NO_PROVIDER_ERROR } from './constants';
import StatusAPI from './StatusAPI';
import User from './User';
import StoragePersistance from './StoragePersistance';
class UserAPI extends EventEmitter {
/**
@ -86,6 +87,58 @@ class UserAPI extends EventEmitter {
return this._provider.getCurrentUser();
}
}
/**
* If a user provider is set, it will return an array of possible roles
* that can be selected by the current user
* @memberof module:openmct.UserAPI#
* @returns {Array}
* @throws Will throw an error if no user provider is set
*/
getPossibleRoles() {
if (!this.hasProvider()) {
this.error(NO_PROVIDER_ERROR);
}
return this._provider.getPossibleRoles();
}
/**
* If a user provider is set, it will return the active role or null
* @memberof module:openmct.UserAPI#
* @returns {string|null}
*/
getActiveRole() {
if (!this.hasProvider()) {
return null;
}
// get from session storage
const sessionStorageValue = StoragePersistance.getActiveRole();
return sessionStorageValue;
}
/**
* Set the active role in session storage
* @memberof module:openmct.UserAPI#
* @returns {undefined}
*/
setActiveRole(role) {
StoragePersistance.setActiveRole(role);
this.emit('roleChanged', role);
}
/**
* Will return if a role can provide a operator status response
* @memberof module:openmct.UserApi#
* @returns {Boolean}
*/
canProvideStatusForRole() {
if (!this.hasProvider()) {
return null;
}
const activeRole = this.getActiveRole();
return this._provider.canProvideStatusForRole?.(activeRole);
}
/**
* If a user provider is set, it will return the user provider's

View File

@ -25,7 +25,7 @@ import { MULTIPLE_PROVIDER_ERROR } from './constants';
import ExampleUserProvider from '../../../example/exampleUser/ExampleUserProvider';
const USERNAME = 'Test User';
const EXAMPLE_ROLE = 'example-role';
const EXAMPLE_ROLE = 'flight';
describe('The User API', () => {
let openmct;

View File

@ -22,3 +22,6 @@
export const MULTIPLE_PROVIDER_ERROR = 'Only one user provider may be set at a time.';
export const NO_PROVIDER_ERROR = 'No user provider has been set.';
export const ACTIVE_ROLE_LOCAL_STORAGE_KEY = 'ACTIVE_USER_ROLE';
export const ACTIVE_ROLE_BROADCAST_CHANNEL_NAME = 'ActiveRoleChannel';

View File

@ -48,7 +48,6 @@
<script>
const DEFAULT_POLL_QUESTION = 'NO POLL QUESTION';
export default {
inject: ['openmct', 'indicator', 'configuration'],
props: {
@ -63,7 +62,6 @@ export default {
},
data() {
return {
allRoles: [],
role: '--',
pollQuestionUpdated: '--',
currentPollQuestion: DEFAULT_POLL_QUESTION,
@ -78,26 +76,27 @@ export default {
left: `${this.positionX}px`,
top: `${this.positionY}px`
};
},
canProvideStatusForRole() {
return this.openmct.user.canProvideStatusForRole(this.role);
}
},
beforeDestroy() {
this.openmct.user.status.off('statusChange', this.setStatus);
this.openmct.user.status.off('pollQuestionChange', this.setPollQuestion);
this.openmct.user.off('roleChanged', this.fetchMyStatus);
},
async mounted() {
this.unsubscribe = [];
await this.fetchUser();
await this.findFirstApplicableRole();
this.fetchPossibleStatusesForUser();
this.fetchCurrentPoll();
this.fetchMyStatus();
await this.fetchMyStatus();
this.subscribeToMyStatus();
this.subscribeToPollQuestion();
this.subscribeToRoleChange();
},
methods: {
async findFirstApplicableRole() {
this.role = await this.openmct.user.status.getStatusRoleForCurrentUser();
},
async fetchUser() {
this.user = await this.openmct.user.getCurrentUser();
},
@ -117,9 +116,22 @@ export default {
this.indicator.text(pollQuestion?.question || '');
},
async fetchMyStatus() {
const activeStatusRole = await this.openmct.user.status.getStatusRoleForCurrentUser();
const status = await this.openmct.user.status.getStatusForRole(activeStatusRole);
// hide indicator for observer
const isStatusCapable = await this.openmct.user.canProvideStatusForRole();
if (!isStatusCapable) {
this.indicator.text('');
this.indicator.statusClass('hidden');
return;
}
const activeRole = await this.openmct.user.getActiveRole();
if (!activeRole) {
return;
}
this.role = activeRole;
const status = await this.openmct.user.status.getStatusForRole(activeRole);
if (status !== undefined) {
this.setStatus({ status });
}
@ -130,7 +142,10 @@ export default {
subscribeToPollQuestion() {
this.openmct.user.status.on('pollQuestionChange', this.setPollQuestion);
},
setStatus({ role, status }) {
subscribeToRoleChange() {
this.openmct.user.on('roleChanged', this.fetchMyStatus);
},
setStatus({ status }) {
status = this.applyStyling(status);
this.selectedStatus = status.key;
this.indicator.iconClass(status.iconClassPoll);
@ -148,11 +163,16 @@ export default {
return this.allStatuses.find((possibleMatch) => possibleMatch.key === statusKey);
},
async changeStatus() {
if (!this.openmct.user.canProvideStatusForRole()) {
this.openmct.notifications.error('Selected role is ineligible to provide operator status');
return;
}
if (this.selectedStatus !== undefined) {
const statusObject = this.findStatusByKey(this.selectedStatus);
const result = await this.openmct.user.status.setStatusForRole(this.role, statusObject);
const result = await this.openmct.user.status.setStatusForRole(statusObject);
if (result === true) {
this.openmct.notifications.info('Successfully set operator status');
} else {

View File

@ -29,13 +29,8 @@ import PollQuestionIndicator from './pollQuestion/PollQuestionIndicator';
export default function operatorStatusPlugin(configuration) {
return function install(openmct) {
if (openmct.user.hasProvider()) {
openmct.user.status.canProvideStatusForCurrentUser().then((canProvideStatus) => {
if (canProvideStatus) {
const operatorStatusIndicator = new OperatorStatusIndicator(openmct, configuration);
operatorStatusIndicator.install();
}
});
const operatorStatusIndicator = new OperatorStatusIndicator(openmct, configuration);
operatorStatusIndicator.install();
openmct.user.status.canSetPollQuestion().then((canSetPollQuestion) => {
if (canSetPollQuestion) {

View File

@ -23,30 +23,107 @@
<template>
<div class="c-indicator icon-person c-indicator--clickable">
<span class="label c-indicator__label">
{{ userName }}
{{ role ? `${userName}: ${role}` : userName }}
<button @click="promptForRoleSelection">Change Role</button>
</span>
</div>
</template>
<script>
import ActiveRoleSynchronizer from '../../../api/user/ActiveRoleSynchronizer';
export default {
inject: ['openmct'],
data() {
return {
userName: undefined,
loggedIn: false
role: undefined,
loggedIn: false,
roleChannel: undefined,
inputRoleSelection: undefined,
roleSelectionDialog: undefined
};
},
mounted() {
this.getUserInfo();
async mounted() {
await this.getUserInfo();
this.roleChannel = new ActiveRoleSynchronizer(this.openmct);
this.roleChannel.subscribeToRoleChanges(this.onRoleChange);
await this.fetchOrPromptForRole();
},
beforeDestroy() {
this.roleChannel.unsubscribeFromRoleChanges(this.onRoleChange);
},
methods: {
getUserInfo() {
this.openmct.user.getCurrentUser().then((user) => {
this.userName = user.getName();
this.loggedIn = this.openmct.user.isLoggedIn();
async getUserInfo() {
const user = await this.openmct.user.getCurrentUser();
this.userName = user.getName();
this.role = this.openmct.user.getActiveRole();
this.loggedIn = this.openmct.user.isLoggedIn();
},
async fetchOrPromptForRole() {
const UserAPI = this.openmct.user;
const activeRole = UserAPI.getActiveRole();
this.role = activeRole;
if (!activeRole) {
this.promptForRoleSelection();
} else {
// only notify the user if they have more than one role available
const allRoles = await this.openmct.user.getPossibleRoles();
if (allRoles.length > 1) {
this.openmct.notifications.info(`You're logged in as role ${activeRole}`);
}
}
},
async promptForRoleSelection() {
const allRoles = await this.openmct.user.getPossibleRoles();
const selectionOptions = allRoles.map((role) => ({
key: role,
name: role
}));
// automatically select only role option
if (selectionOptions.length === 1) {
this.updateRole(selectionOptions[0].key);
return;
}
this.roleSelectionDialog = this.openmct.overlays.selection({
selectionOptions,
iconClass: 'alert',
title: 'Select Role',
message: 'Please select your role for operator status.',
currentSelection: this.role,
onChange: (event) => {
this.inputRoleSelection = event.target.value;
},
buttons: [
{
label: 'Select',
emphasis: true,
callback: () => {
this.roleSelectionDialog.dismiss();
this.roleSelectionDialog = undefined;
const inputValueOrDefault = this.inputRoleSelection || selectionOptions[0].key;
this.updateRole(inputValueOrDefault);
this.openmct.notifications.info(`Successfully set new role to ${this.role}`);
}
}
]
});
},
onRoleChange(event) {
const role = event.data;
this.roleSelectionDialog?.dismiss();
this.setRoleSelection(role);
},
setRoleSelection(role) {
this.role = role;
},
updateRole(role) {
this.setRoleSelection(role);
this.openmct.user.setActiveRole(role);
// update other tabs through broadcast channel
this.roleChannel.broadcastNewRole(role);
}
}
};

View File

@ -113,6 +113,10 @@ export default {
mounted() {
this.openmct.notifications.on('notification', this.showNotification);
this.openmct.notifications.on('dismiss-all', this.clearModel);
if (this.openmct.notifications.activeNotification) {
activeNotification = this.openmct.notifications.activeNotification;
this.showNotification(activeNotification);
}
},
methods: {
showNotification(notification) {

View File

@ -25,6 +25,7 @@ import MCT from 'MCT';
let nativeFunctions = [];
let mockObjects = setMockObjects();
const EXAMPLE_ROLE = 'flight';
const DEFAULT_TIME_OPTIONS = {
timeSystemKey: 'utc',
bounds: {
@ -38,6 +39,7 @@ export function createOpenMct(timeSystemOptions = DEFAULT_TIME_OPTIONS) {
openmct.install(openmct.plugins.LocalStorage());
openmct.install(openmct.plugins.UTCTimeSystem());
openmct.setAssetPath('/base');
openmct.user.setActiveRole(EXAMPLE_ROLE);
const timeSystemKey = timeSystemOptions.timeSystemKey;
const start = timeSystemOptions.bounds.start;