Compare commits

...

21 Commits

Author SHA1 Message Date
15bab81447 Merge branch 'master' into mct6555-v2 2023-06-16 20:03:03 -05:00
cf8db0a21d Cleanup 2023-06-16 18:19:48 -05:00
bda652b3bb Removed status.getStatusRoleForCurrentUser 2023-06-16 18:18:30 -05:00
e7f2f4b5f6 Remove default status role from example user plugin 2023-06-15 11:53:45 -05:00
18b5ee1340 Removed unused role param from status api call 2023-06-15 11:50:35 -05:00
9006c13eb4 Lint 2023-06-14 16:20:18 -05:00
d7b16a5a2c Added success notification and cleanup 2023-06-14 16:17:02 -05:00
a76701c23a Store status roles in an array instead of a singular value 2023-06-14 11:45:40 -05:00
bb52291ab6 Cleanup 2023-06-14 11:02:40 -05:00
87db357b5a Updates to status api canPRovideStatusForRole 2023-06-14 11:00:01 -05:00
587c27464b Reconnect channel on error and UserIndicator updates 2023-06-07 17:36:53 -05:00
e0bad2620e Moved prompt to UserIndicator 2023-06-07 11:20:48 -05:00
0222b77941 UserAPI role updates and UserIndicator improvement 2023-06-01 17:24:13 -05:00
e05d812219 Comment width 2023-06-01 10:04:45 -05:00
d51498752f Update comment 2023-06-01 10:03:16 -05:00
a3b1fa34e4 Updates to broadcast channel lifecycle 2023-06-01 09:57:16 -05:00
598ebddd29 Display role in user indicator 2023-05-31 17:30:30 -05:00
010e86bf83 Added selection dialog to overlays and implemented for operator status 2023-05-31 17:17:49 -05:00
e8ed10db78 Update example user provider 2023-05-26 16:54:25 -05:00
fd20d392c2 Add session storage and role to user indicator 2023-05-26 16:52:16 -05:00
841c999d16 Add additional test roles to example user 2023-05-08 14:08:37 -05:00
14 changed files with 1005 additions and 622 deletions

View File

@ -63,16 +63,21 @@ const STATUSES = [
* @implements {StatusUserProvider}
*/
export default class ExampleUserProvider extends EventEmitter {
constructor(openmct, { defaultStatusRole } = { defaultStatusRole: undefined }) {
super();
constructor(openmct, {statusRoles} = {
statusRoles: []
}) {
super();
this.openmct = openmct;
this.user = undefined;
this.loggedIn = false;
this.autoLoginUser = undefined;
this.status = STATUSES[0];
this.pollQuestion = undefined;
this.defaultStatusRole = defaultStatusRole;
this.openmct = openmct;
this.user = undefined;
this.loggedIn = false;
this.autoLoginUser = undefined;
this.statusRoleValues = statusRoles.map(x => ({
role: x,
status: STATUSES[0]
}));
this.pollQuestion = undefined;
this.statusRoles = statusRoles;
this.ExampleUser = createExampleUser(this.openmct.user.User);
this.loginPromise = undefined;
@ -169,41 +174,126 @@ export default class ExampleUserProvider extends EventEmitter {
return Promise.resolve(STATUSES);
}
_login() {
const id = uuid();
// 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.loggedIn = true;
canProvideStatusForRole(role) {
return this.statusRoles.includes(role);
}
return Promise.resolve();
}
const formStructure = {
title: 'Login',
sections: [
{
rows: [
{
key: 'username',
control: 'textfield',
name: 'Username',
pattern: '\\S+',
required: true,
cssClass: 'l-input-lg',
value: ''
}
]
}
],
buttons: {
submit: {
label: 'Login'
}
canSetPollQuestion() {
return Promise.resolve(true);
}
hasRole(roleId) {
if (!this.loggedIn) {
Promise.resolve(undefined);
}
};
return Promise.resolve(this.user.getRoles().includes(roleId));
}
getPossibleRoles() {
return this.user.getRoles();
}
getStatusRoleForCurrentUser(role) {
const matchedRole = this.statusRoleValues.find(x => x.role === role);
return Promise.resolve(matchedRole?.status);
}
getAllStatusRoles() {
return Promise.resolve(this.statusRoles);
}
getStatusForRole(role) {
const statusForRole = this.statusRoleValues.find(x => x.role === role);
return Promise.resolve(statusForRole?.status);
}
async getDefaultStatusForRole(role) {
const allRoles = await this.getPossibleStatuses();
return allRoles?.[0];
}
setStatusForRole(role, status) {
status.timestamp = Date.now();
const matchingIndex = this.statusRoleValues.findIndex(x => x.role === role);
this.statusRoleValues[matchingIndex].status = status;
this.emit('statusChange', {
role,
status
});
return true;
}
// eslint-disable-next-line require-await
async getPollQuestion() {
if (this.pollQuestion) {
return this.pollQuestion;
} else {
return undefined;
}
}
setPollQuestion(pollQuestion) {
if (!pollQuestion) {
// If the poll question is undefined, set it to a blank string.
// This behavior better reflects how other telemetry systems
// deal with undefined poll questions.
pollQuestion = '';
}
this.pollQuestion = {
question: pollQuestion,
timestamp: Date.now()
};
this.emit("pollQuestionChange", this.pollQuestion);
return true;
}
getPossibleStatuses() {
return Promise.resolve(STATUSES);
}
_login() {
const id = uuid();
// 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, ['test-role-1', 'test-role-2', 'test-role-3', 'test-role-4']);
this.loggedIn = true;
return Promise.resolve();
}
const formStructure = {
title: "Login",
sections: [
{
rows: [
{
key: "username",
control: "textfield",
name: "Username",
pattern: "\\S+",
required: true,
cssClass: "l-input-lg",
value: ''
}
]
}
],
buttons: {
submit: {
label: 'Login'
}
}
};
return this.openmct.forms.showForm(formStructure).then(
(info) => {

View File

@ -21,17 +21,17 @@
*****************************************************************************/
import ExampleUserProvider from './ExampleUserProvider';
const AUTO_LOGIN_USER = 'guest';
const STATUS_ROLES = ['test-role-1', 'test-role-2', 'test-role-3'];
export default function ExampleUserPlugin(
{ autoLoginUser, defaultStatusRole } = {
autoLoginUser: 'guest',
defaultStatusRole: 'test-role'
}
) {
return function install(openmct) {
const userProvider = new ExampleUserProvider(openmct, {
defaultStatusRole
});
export default function ExampleUserPlugin({autoLoginUser, statusRoles} = {
autoLoginUser: AUTO_LOGIN_USER,
statusRoles: STATUS_ROLES
}) {
return function install(openmct) {
const userProvider = new ExampleUserProvider(openmct, {
statusRoles
});
if (autoLoginUser !== undefined) {
userProvider.autoLogin(autoLoginUser);

View File

@ -1,6 +1,7 @@
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
@ -127,9 +128,15 @@ class OverlayAPI {
let progressDialog = new ProgressDialog(options);
this.showOverlay(progressDialog);
return progressDialog;
}
selection(options) {
let selection = new Selection(options);
this.showOverlay(selection);
return selection;
}
}
export default OverlayAPI;

View File

@ -0,0 +1,38 @@
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,45 @@
<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,52 @@
import { BROADCAST_CHANNEL_NAME } from './constants';
class RoleChannel {
constructor(openmct, channelName = BROADCAST_CHANNEL_NAME) {
this.openmct = openmct;
this.channelName = channelName;
this.roleChannel = undefined;
}
createRoleChannel() {
this.roleChannel = new BroadcastChannel(this.channelName);
}
subscribeToRole(cb) {
this.roleChannel.onmessage = (event => {
const role = event.data;
this.openmct.user.setActiveRole(role);
if (cb) {
cb(role);
}
});
}
unsubscribeToRole() {
this.roleChannel.close();
}
reconnect() {
this.roleChannel.close();
this.createRoleChannel();
}
broadcastNewRole(role) {
if (!this.roleChannel.name) {
return false;
}
try {
this.roleChannel.postMessage(role);
} catch (e) {
this.reconnect();
this.broadcastNewRole(role);
/** FIXME: there doesn't seem to be a reliable way to test for open/closed
* status of a broadcast channel; channel.name exists even after the
* channel is closed. Failure to update the subscribed tabs, should
* not block the focused tab's selection and so it is caught here.
* An error will often be thrown if the dialog remains open during HMR.
**/
}
}
}
export default RoleChannel;

View File

@ -0,0 +1,38 @@
/*****************************************************************************
* 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 { SESSION_STORAGE_KEY } from './constants';
class SessionPersistance {
getActiveRole() {
return sessionStorage.getItem(SESSION_STORAGE_KEY);
}
setActiveRole(role) {
return sessionStorage.setItem(SESSION_STORAGE_KEY, role);
}
clearActiveRole() {
return sessionStorage.removeItem(SESSION_STORAGE_KEY);
}
}
export default new SessionPersistance();

View File

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

View File

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

View File

@ -21,125 +21,173 @@
*****************************************************************************/
import EventEmitter from 'EventEmitter';
import { MULTIPLE_PROVIDER_ERROR, NO_PROVIDER_ERROR } from './constants';
import {
MULTIPLE_PROVIDER_ERROR,
NO_PROVIDER_ERROR
} from './constants';
import StatusAPI from './StatusAPI';
import User from './User';
import SessionPersistance from './SessionPersistance';
class UserAPI extends EventEmitter {
/**
* @param {OpenMCT} openmct
* @param {UserAPIConfiguration} config
*/
constructor(openmct, config) {
super();
/**
* @param {OpenMCT} openmct
* @param {UserAPIConfiguration} config
*/
constructor(openmct, config) {
super();
this._openmct = openmct;
this._provider = undefined;
this._openmct = openmct;
this._provider = undefined;
this.User = User;
this.status = new StatusAPI(this, openmct, config);
}
/**
* Set the user provider for the user API. This allows you
* to specifiy ONE user provider to be used with Open MCT.
* @method setProvider
* @memberof module:openmct.UserAPI#
* @param {module:openmct.UserAPI~UserProvider} provider the new
* user provider
*/
setProvider(provider) {
if (this.hasProvider()) {
this.error(MULTIPLE_PROVIDER_ERROR);
this.User = User;
this.status = new StatusAPI(this, openmct, config);
}
this._provider = provider;
this.emit('providerAdded', this._provider);
}
/**
* Set the user provider for the user API. This allows you
* to specifiy ONE user provider to be used with Open MCT.
* @method setProvider
* @memberof module:openmct.UserAPI#
* @param {module:openmct.UserAPI~UserProvider} provider the new
* user provider
*/
setProvider(provider) {
if (this.hasProvider()) {
this.error(MULTIPLE_PROVIDER_ERROR);
}
getProvider() {
return this._provider;
}
/**
* Return true if the user provider has been set.
*
* @memberof module:openmct.UserAPI#
* @returns {boolean} true if the user provider exists
*/
hasProvider() {
return this._provider !== undefined;
}
/**
* If a user provider is set, it will return a copy of a user object from
* the provider. If the user is not logged in, it will return undefined;
*
* @memberof module:openmct.UserAPI#
* @returns {Function|Promise} user provider 'getCurrentUser' method
* @throws Will throw an error if no user provider is set
*/
getCurrentUser() {
if (!this.hasProvider()) {
return Promise.resolve(undefined);
} else {
return this._provider.getCurrentUser();
}
}
/**
* If a user provider is set, it will return the user provider's
* 'isLoggedIn' method
*
* @memberof module:openmct.UserAPI#
* @returns {Function|Boolean} user provider 'isLoggedIn' method
* @throws Will throw an error if no user provider is set
*/
isLoggedIn() {
if (!this.hasProvider()) {
return false;
this._provider = provider;
this.emit('providerAdded', this._provider);
}
return this._provider.isLoggedIn();
}
/**
* If a user provider is set, it will return a call to it's
* 'hasRole' method
*
* @memberof module:openmct.UserAPI#
* @returns {Function|Boolean} user provider 'isLoggedIn' method
* @param {string} roleId id of role to check for
* @throws Will throw an error if no user provider is set
*/
hasRole(roleId) {
this.noProviderCheck();
return this._provider.hasRole(roleId);
}
/**
* Checks if a provider is set and if not, will throw error
*
* @private
* @throws Will throw an error if no user provider is set
*/
noProviderCheck() {
if (!this.hasProvider()) {
this.error(NO_PROVIDER_ERROR);
getProvider() {
return this._provider;
}
}
/**
* Utility function for throwing errors
*
* @private
* @param {string} error description of error
* @throws Will throw error passed in
*/
error(error) {
throw new Error(error);
}
/**
* Return true if the user provider has been set.
*
* @memberof module:openmct.UserAPI#
* @returns {boolean} true if the user provider exists
*/
hasProvider() {
return this._provider !== undefined;
}
/**
* If a user provider is set, it will return a copy of a user object from
* the provider. If the user is not logged in, it will return undefined;
*
* @memberof module:openmct.UserAPI#
* @returns {Function|Promise} user provider 'getCurrentUser' method
* @throws Will throw an error if no user provider is set
*/
getCurrentUser() {
if (!this.hasProvider()) {
return Promise.resolve(undefined);
} else {
return this._provider.getCurrentUser();
}
}
getPossibleRoles() {
if (!this.hasProvider()) {
return Promise.resolve(undefined);
} else {
return this._provider.getPossibleRoles();
}
}
/**
* If a user provider is set, it will return the active role Id
* @returns object
*/
getActiveRole() {
if (!this.hasProvider()) {
return Promise.resolve(undefined);
}
// get from session storage
const sessionStorageValue = SessionPersistance.getActiveRole();
if (sessionStorageValue === 'undefined' || sessionStorageValue === undefined) {
return undefined;
}
return sessionStorageValue;
}
setActiveRole(role) {
SessionPersistance.setActiveRole(role);
}
/**
* Will return if a role can provide a operator status response
* @memberof module:openmct.UserApi#
* @returns {Boolean}
*/
canProvideStatusForRole() {
if (!this || !this.hasProvider()) {
return Promise.resolve(undefined);
}
const activeRole = this.getActiveRole();
return this._provider.canProvideStatusForRole?.(activeRole);
}
/**
* If a user provider is set, it will return the user provider's
* 'isLoggedIn' method
*
* @memberof module:openmct.UserAPI#
* @returns {Function|Boolean} user provider 'isLoggedIn' method
* @throws Will throw an error if no user provider is set
*/
isLoggedIn() {
if (!this.hasProvider()) {
return false;
}
return this._provider.isLoggedIn();
}
/**
* If a user provider is set, it will return a call to it's
* 'hasRole' method
*
* @memberof module:openmct.UserAPI#
* @returns {Function|Boolean} user provider 'isLoggedIn' method
* @param {string} roleId id of role to check for
* @throws Will throw an error if no user provider is set
*/
hasRole(roleId) {
this.noProviderCheck();
return this._provider.hasRole(roleId);
}
/**
* Checks if a provider is set and if not, will throw error
*
* @private
* @throws Will throw an error if no user provider is set
*/
noProviderCheck() {
if (!this.hasProvider()) {
this.error(NO_PROVIDER_ERROR);
}
}
/**
* Utility function for throwing errors
*
* @private
* @param {string} error description of error
* @throws Will throw error passed in
*/
error(error) {
throw new Error(error);
}
}
export default UserAPI;
@ -159,4 +207,4 @@ export default UserAPI;
* @property {String} statusClass The class to apply to the indicator when this status is active eg. "s-status-error"
* @property {String} statusBgColor The background color to apply in the status summary section of the poll question popup for this status eg."#9900cc"
* @property {String} statusFgColor The foreground color to apply in the status summary section of the poll question popup for this status eg. "#fff"
*/
*/

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 SESSION_STORAGE_KEY = 'USER_ROLE';
export const BROADCAST_CHANNEL_NAME = 'USER_ROLE';

View File

@ -21,158 +21,169 @@
-->
<template>
<div
:style="position"
class="c-status-poll-panel c-status-poll-panel--operator"
@click.stop="noop"
:style="position"
class="c-status-poll-panel c-status-poll-panel--operator"
@click.stop="noop"
>
<div class="c-status-poll-panel__section c-status-poll-panel__top">
<div class="c-status-poll-panel__title">Status Poll</div>
<div class="c-status-poll-panel__user-role icon-person">{{ role }}</div>
<div class="c-status-poll-panel__updated">{{ pollQuestionUpdated }}</div>
</div>
<div class="c-status-poll-panel__section c-status-poll-panel__poll-question">
{{ currentPollQuestion }}
</div>
<div class="c-status-poll-panel__section c-status-poll-panel__bottom">
<div class="c-status-poll-panel__set-status-label">My status:</div>
<select v-model="selectedStatus" name="setStatus" @change="changeStatus">
<option v-for="status in allStatuses" :key="status.key" :value="status.key">
{{ status.label }}
</option>
</select>
</div>
<div class="c-status-poll-panel__section c-status-poll-panel__top">
<div
class="c-status-poll-panel__title"
>Status Poll</div>
<div class="c-status-poll-panel__user-role icon-person">{{ role }}</div>
<div class="c-status-poll-panel__updated">{{ pollQuestionUpdated }}</div>
</div>
<div class="c-status-poll-panel__section c-status-poll-panel__poll-question">
{{ currentPollQuestion }}
</div>
<div class="c-status-poll-panel__section c-status-poll-panel__bottom">
<div class="c-status-poll-panel__set-status-label">My status:</div>
<select
v-model="selectedStatus"
name="setStatus"
@change="changeStatus"
>
<option
v-for="status in allStatuses"
:key="status.key"
:value="status.key"
>
{{ status.label }}
</option>
</select>
</div>
</div>
</template>
<script>
const DEFAULT_POLL_QUESTION = 'NO POLL QUESTION';
export default {
inject: ['openmct', 'indicator', 'configuration'],
props: {
positionX: {
type: Number,
required: true
},
positionY: {
type: Number,
required: true
}
},
data() {
return {
allRoles: [],
role: '--',
pollQuestionUpdated: '--',
currentPollQuestion: DEFAULT_POLL_QUESTION,
selectedStatus: undefined,
allStatuses: []
};
},
computed: {
position() {
return {
position: 'absolute',
left: `${this.positionX}px`,
top: `${this.positionY}px`
};
}
},
beforeDestroy() {
this.openmct.user.status.off('statusChange', this.setStatus);
this.openmct.user.status.off('pollQuestionChange', this.setPollQuestion);
},
async mounted() {
this.unsubscribe = [];
await this.fetchUser();
await this.findFirstApplicableRole();
this.fetchPossibleStatusesForUser();
this.fetchCurrentPoll();
this.fetchMyStatus();
this.subscribeToMyStatus();
this.subscribeToPollQuestion();
},
methods: {
async findFirstApplicableRole() {
this.role = await this.openmct.user.status.getStatusRoleForCurrentUser();
},
async fetchUser() {
this.user = await this.openmct.user.getCurrentUser();
},
async fetchCurrentPoll() {
const pollQuestion = await this.openmct.user.status.getPollQuestion();
if (pollQuestion !== undefined) {
this.setPollQuestion(pollQuestion);
</template>
<script>
const DEFAULT_POLL_QUESTION = 'NO POLL QUESTION';
export default {
inject: ['openmct', 'indicator', 'configuration'],
props: {
positionX: {
type: Number,
required: true
},
positionY: {
type: Number,
required: true
}
},
data() {
return {
allRoles: [],
role: '--',
pollQuestionUpdated: '--',
currentPollQuestion: DEFAULT_POLL_QUESTION,
selectedStatus: undefined,
allStatuses: []
};
},
computed: {
position() {
return {
position: 'absolute',
left: `${this.positionX}px`,
top: `${this.positionY}px`
};
}
},
beforeDestroy() {
this.openmct.user.status.off('statusChange', this.setStatus);
this.openmct.user.status.off('pollQuestionChange', this.setPollQuestion);
},
async mounted() {
this.unsubscribe = [];
await this.fetchUser();
this.fetchPossibleStatusesForUser();
this.fetchCurrentPoll();
await this.fetchMyStatus();
this.subscribeToMyStatus();
this.subscribeToPollQuestion();
},
methods: {
async fetchUser() {
this.user = await this.openmct.user.getCurrentUser();
},
async fetchCurrentPoll() {
const pollQuestion = await this.openmct.user.status.getPollQuestion();
if (pollQuestion !== undefined) {
this.setPollQuestion(pollQuestion);
}
},
async fetchPossibleStatusesForUser() {
this.allStatuses = await this.openmct.user.status.getPossibleStatuses();
},
setPollQuestion(pollQuestion) {
this.currentPollQuestion = pollQuestion.question;
this.pollQuestionUpdated = new Date(pollQuestion.timestamp).toISOString();
this.indicator.text(pollQuestion?.question || '');
},
async fetchMyStatus() {
const activeRole = await this.openmct.user.getActiveRole();
const status = await this.openmct.user.status.getStatusForRole(activeRole);
if (status !== undefined) {
this.setStatus({status});
}
},
subscribeToMyStatus() {
this.openmct.user.status.on('statusChange', this.setStatus);
},
subscribeToPollQuestion() {
this.openmct.user.status.on('pollQuestionChange', this.setPollQuestion);
},
setStatus({status}) {
status = this.applyStyling(status);
this.selectedStatus = status.key;
this.indicator.iconClass(status.iconClassPoll);
this.indicator.statusClass(status.statusClass);
if (this.isDefaultStatus(status)) {
this.indicator.text(this.currentPollQuestion);
} else {
this.indicator.text(status.label);
}
},
isDefaultStatus(status) {
return status.key === this.allStatuses[0].key;
},
findStatusByKey(statusKey) {
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(statusObject);
if (result === true) {
this.openmct.notifications.info("Successfully set operator status");
} else {
this.openmct.notifications.error("Unable to set operator status");
}
}
},
applyStyling(status) {
const stylesForStatus = this.configuration?.statusStyles?.[status.label];
if (stylesForStatus !== undefined) {
return {
...status,
...stylesForStatus
};
} else {
return status;
}
},
noop() {}
}
},
async fetchPossibleStatusesForUser() {
this.allStatuses = await this.openmct.user.status.getPossibleStatuses();
},
setPollQuestion(pollQuestion) {
this.currentPollQuestion = pollQuestion.question;
this.pollQuestionUpdated = new Date(pollQuestion.timestamp).toISOString();
this.indicator.text(pollQuestion?.question || '');
},
async fetchMyStatus() {
const activeStatusRole = await this.openmct.user.status.getStatusRoleForCurrentUser();
const status = await this.openmct.user.status.getStatusForRole(activeStatusRole);
if (status !== undefined) {
this.setStatus({ status });
}
},
subscribeToMyStatus() {
this.openmct.user.status.on('statusChange', this.setStatus);
},
subscribeToPollQuestion() {
this.openmct.user.status.on('pollQuestionChange', this.setPollQuestion);
},
setStatus({ role, status }) {
status = this.applyStyling(status);
this.selectedStatus = status.key;
this.indicator.iconClass(status.iconClassPoll);
this.indicator.statusClass(status.statusClass);
if (this.isDefaultStatus(status)) {
this.indicator.text(this.currentPollQuestion);
} else {
this.indicator.text(status.label);
}
},
isDefaultStatus(status) {
return status.key === this.allStatuses[0].key;
},
findStatusByKey(statusKey) {
return this.allStatuses.find((possibleMatch) => possibleMatch.key === statusKey);
},
async changeStatus() {
if (this.selectedStatus !== undefined) {
const statusObject = this.findStatusByKey(this.selectedStatus);
const result = await this.openmct.user.status.setStatusForRole(this.role, statusObject);
if (result === true) {
this.openmct.notifications.info('Successfully set operator status');
} else {
this.openmct.notifications.error('Unable to set operator status');
}
}
},
applyStyling(status) {
const stylesForStatus = this.configuration?.statusStyles?.[status.label];
if (stylesForStatus !== undefined) {
return {
...status,
...stylesForStatus
};
} else {
return status;
}
},
noop() {}
}
};
</script>
};
</script>

View File

@ -28,22 +28,24 @@ 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();
}
});
if (openmct.user.hasProvider()) {
const activeRole = openmct.user.getActiveRole();
openmct.user.status.canProvideStatusForRole(activeRole).then(canProvideStatus => {
if (canProvideStatus) {
const operatorStatusIndicator = new OperatorStatusIndicator(openmct, configuration);
openmct.user.status.canSetPollQuestion().then((canSetPollQuestion) => {
if (canSetPollQuestion) {
const pollQuestionIndicator = new PollQuestionIndicator(openmct, configuration);
operatorStatusIndicator.install();
}
});
pollQuestionIndicator.install();
}
});
}
openmct.user.status.canSetPollQuestion().then(canSetPollQuestion => {
if (canSetPollQuestion) {
const pollQuestionIndicator = new PollQuestionIndicator(openmct, configuration);
pollQuestionIndicator.install();
}
});
}
};
}
}

View File

@ -21,33 +21,93 @@
-->
<template>
<div class="c-indicator icon-person c-indicator--clickable">
<span class="label c-indicator__label">
{{ userName }}
</span>
</div>
</template>
<script>
export default {
inject: ['openmct'],
data() {
return {
userName: undefined,
loggedIn: false
<div class="c-indicator icon-person c-indicator--clickable">
<span class="label c-indicator__label">
{{ role ? `${userName}: ${role}` : userName }}
<button @click="promptForRoleSelection">Change Role</button>
</span>
</div>
</template>
<script>
import RoleChannelProvider from '../../../api/user/RoleChannel';
export default {
inject: ['openmct'],
data() {
return {
userName: undefined,
role: undefined,
loggedIn: false,
roleChannelProvider: undefined
};
},
async mounted() {
this.getUserInfo();
this.roleChannelProvider = new RoleChannelProvider(this.openmct);
this.roleChannelProvider.createRoleChannel();
this.roleChannelProvider.subscribeToRole(this.setRoleSelection);
await this.fetchOrPromptForRole();
},
beforeDestroy() {
this.roleChannelProvider.unsubscribeToRole();
},
methods: {
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();
},
fetchOrPromptForRole() {
const UserAPI = this.openmct.user;
const activeRole = UserAPI.getActiveRole();
this.role = activeRole;
if (!activeRole) {
this.promptForRoleSelection();
}
},
promptForRoleSelection() {
const allRoles = this.openmct.user.getPossibleRoles();
const selectionOptions = allRoles.map(x => ({
key: x,
name: x
})).filter(this.openmct.user.canProvideStatusForRole);
const dialog = this.openmct.overlays.selection({
selectionOptions,
iconClass: 'info',
title: 'Select Role',
message: 'Please select your role for operator status.',
currentSelection: this.role,
onChange: (event) => {
this.role = event.target.value;
},
buttons: [
{
label: 'Select',
emphasis: true,
callback: () => {
dialog.dismiss();
this.updateRole(this.role);
this.openmct.notifications.info(`Successfully set new role to ${this.role}`);
}
}
]
});
},
setRoleSelection(role) {
this.role = role;
},
updateRole(role) {
this.setRoleSelection(role);
this.openmct.user.setActiveRole(role);
// update other tabs through broadcast channel
this.roleChannelProvider.broadcastNewRole(role);
}
}
};
},
mounted() {
this.getUserInfo();
},
methods: {
getUserInfo() {
this.openmct.user.getCurrentUser().then((user) => {
this.userName = user.getName();
this.loggedIn = this.openmct.user.isLoggedIn();
});
}
}
};
</script>
</script>