diff --git a/e2e/tests/functional/plugins/operatorStatus/operatorStatus.e2e.spec.js b/e2e/tests/functional/plugins/operatorStatus/operatorStatus.e2e.spec.js index b40dc2a67a..1fa5437bd8 100644 --- a/e2e/tests/functional/plugins/operatorStatus/operatorStatus.e2e.spec.js +++ b/e2e/tests/functional/plugins/operatorStatus/operatorStatus.e2e.spec.js @@ -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 diff --git a/example/exampleUser/ExampleUserProvider.js b/example/exampleUser/ExampleUserProvider.js index 683ddb1d4b..c92d780125 100644 --- a/example/exampleUser/ExampleUserProvider.js +++ b/example/exampleUser/ExampleUserProvider.js @@ -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(); diff --git a/example/exampleUser/plugin.js b/example/exampleUser/plugin.js index 25c00b7dc0..ffbcae08ba 100644 --- a/example/exampleUser/plugin.js +++ b/example/exampleUser/plugin.js @@ -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) { diff --git a/src/api/overlays/OverlayAPI.js b/src/api/overlays/OverlayAPI.js index f7033f6b52..05811159bd 100644 --- a/src/api/overlays/OverlayAPI.js +++ b/src/api/overlays/OverlayAPI.js @@ -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; diff --git a/src/api/overlays/Selection.js b/src/api/overlays/Selection.js new file mode 100644 index 0000000000..15c8365a69 --- /dev/null +++ b/src/api/overlays/Selection.js @@ -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: '' + }).$mount(); + + super({ + element: component.$el, + size: 'fit', + dismissable: false, + onChange, + currentSelection, + ...options + }); + + this.once('destroy', () => { + component.$destroy(); + }); + } +} + +export default Selection; diff --git a/src/api/overlays/components/SelectionComponent.vue b/src/api/overlays/components/SelectionComponent.vue new file mode 100644 index 0000000000..45ca72384c --- /dev/null +++ b/src/api/overlays/components/SelectionComponent.vue @@ -0,0 +1,34 @@ + + + diff --git a/src/api/user/ActiveRoleSynchronizer.js b/src/api/user/ActiveRoleSynchronizer.js new file mode 100644 index 0000000000..1ecbf28627 --- /dev/null +++ b/src/api/user/ActiveRoleSynchronizer.js @@ -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; diff --git a/src/api/user/StatusAPI.js b/src/api/user/StatusAPI.js index 159dcbb13b..30495e280d 100644 --- a/src/api/user/StatusAPI.js +++ b/src/api/user/StatusAPI.js @@ -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} 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} 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} 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; } /** diff --git a/src/api/user/StatusUserProvider.js b/src/api/user/StatusUserProvider.js index 85d132b52c..6389c36350 100644 --- a/src/api/user/StatusUserProvider.js +++ b/src/api/user/StatusUserProvider.js @@ -77,5 +77,4 @@ export default class StatusUserProvider extends UserProvider { /** * @returns {Promise} the active status role for the currently logged in user */ - async getStatusRoleForCurrentUser() {} } diff --git a/src/api/user/StoragePersistance.js b/src/api/user/StoragePersistance.js new file mode 100644 index 0000000000..c9c53e6629 --- /dev/null +++ b/src/api/user/StoragePersistance.js @@ -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(); diff --git a/src/api/user/UserAPI.js b/src/api/user/UserAPI.js index 98f4322ac7..5e588825c3 100644 --- a/src/api/user/UserAPI.js +++ b/src/api/user/UserAPI.js @@ -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 diff --git a/src/api/user/UserAPISpec.js b/src/api/user/UserAPISpec.js index 0c5e0db947..8a849c9fa4 100644 --- a/src/api/user/UserAPISpec.js +++ b/src/api/user/UserAPISpec.js @@ -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; diff --git a/src/api/user/constants.js b/src/api/user/constants.js index 94d1203629..c2e4ed02fb 100644 --- a/src/api/user/constants.js +++ b/src/api/user/constants.js @@ -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'; diff --git a/src/plugins/operatorStatus/operatorStatus/OperatorStatus.vue b/src/plugins/operatorStatus/operatorStatus/OperatorStatus.vue index 26650005a1..19f754fbdf 100644 --- a/src/plugins/operatorStatus/operatorStatus/OperatorStatus.vue +++ b/src/plugins/operatorStatus/operatorStatus/OperatorStatus.vue @@ -48,7 +48,6 @@