Skip to content

Commit

Permalink
feat: Mission Status for Situational Awareness (#7418)
Browse files Browse the repository at this point in the history
* refactor: `UserIndicator` use vue component directly

* style(WIP): filler styles for user-indicator

* feat(WIP): working on mission status indicators

* feat: support mission statuses

* feat(WIP): can display mission statuses now

* feat(WIP): add composables and dynamically calculate popup position

* feat(WIP): dismissible popup, use moar compositionAPI

* Closes #7420
- Styling and markup for mission status control panel.
- Tweaks and additions to some common style elements.

* feat: set/unset mission status for role

* refactor: rename some functions

* feat: more renaming, get mission role statuses working

* refactor: more method renaming

* fix: remove dupe method

* feat: hook up event listeners

* refactor: convert to CompositionAPI and listen to events

* fix: add that back in, woops

* test: fix some existing tests

* lint: words for the word god

* refactor: rename

* fix: setting mission statuses

* style: fix go styling

* style: add mission status button

* refactor: rename `MissionRole` -> `MissionAction`

* test: fix most existing tests

* test: remove integration tests already covered by e2e

- These tests are going to be wonky since they depend on the View. Any unit tests depending on Vue / the View will become increasingly volatile over time as we migrate more of the app into the main Vue app. Since these LOC are already covered by e2e, I'm going to remove them. We will need to move towards a more component / Vue-friendly testing framework to stabilize all of this.

* docs: add documentation

* refactor: rename

* fix: a comma

* refactor: a word

* fix: emit parameter format

* fix: correct emit for `missionStatusActionChange`

---------

Co-authored-by: Charles Hacskaylo <[email protected]>
Co-authored-by: John Hill <[email protected]>
  • Loading branch information
3 people authored Feb 2, 2024
1 parent ee5081f commit 82fa4c1
Show file tree
Hide file tree
Showing 16 changed files with 701 additions and 86 deletions.
1 change: 1 addition & 0 deletions .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,7 @@
"unnnormalized",
"checksnapshots",
"specced",
"composables",
"countup"
],
"dictionaries": ["npm", "softwareTerms", "node", "html", "css", "bash", "en_US", "en-gb", "misc"],
Expand Down
5 changes: 5 additions & 0 deletions example/exampleUser/ExampleUserProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@ export default class ExampleUserProvider extends EventEmitter {
canSetPollQuestion() {
return Promise.resolve(true);
}

canSetMissionStatus() {
return Promise.resolve(false);
}

hasRole(roleId) {
if (!this.loggedIn) {
Promise.resolve(undefined);
Expand Down
101 changes: 101 additions & 0 deletions src/api/user/StatusAPI.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export default class StatusAPI extends EventEmitter {

this.onProviderStatusChange = this.onProviderStatusChange.bind(this);
this.onProviderPollQuestionChange = this.onProviderPollQuestionChange.bind(this);
this.onMissionActionStatusChange = this.onMissionActionStatusChange.bind(this);
this.listenToStatusEvents = this.listenToStatusEvents.bind(this);

this.#openmct.once('destroy', () => {
Expand All @@ -40,6 +41,7 @@ export default class StatusAPI extends EventEmitter {
if (typeof provider?.off === 'function') {
provider.off('statusChange', this.onProviderStatusChange);
provider.off('pollQuestionChange', this.onProviderPollQuestionChange);
provider.off('missionActionStatusChange', this.onMissionActionStatusChange);
}
});

Expand Down Expand Up @@ -100,6 +102,67 @@ export default class StatusAPI extends EventEmitter {
}
}

/**
* Can the currently logged in user set the mission status.
* @returns {Promise<Boolean>} true if the currently logged in user can set the mission status, false otherwise.
*/
canSetMissionStatus() {
const provider = this.#userAPI.getProvider();

if (provider.canSetMissionStatus) {
return provider.canSetMissionStatus();
} else {
return Promise.resolve(false);
}
}

/**
* Fetch the current status for the given mission action
* @param {MissionAction} action
* @returns {string}
*/
getStatusForMissionAction(action) {
const provider = this.#userAPI.getProvider();

if (provider.getStatusForMissionAction) {
return provider.getStatusForMissionAction(action);
} else {
this.#userAPI.error('User provider does not support getting mission action status');
}
}

/**
* Fetch the list of possible mission status options (GO, NO-GO, etc.)
* @returns {Promise<MissionStatusOption[]>} the complete list of possible mission statuses
*/
async getPossibleMissionActionStatuses() {
const provider = this.#userAPI.getProvider();

if (provider.getPossibleMissionActionStatuses) {
const possibleOptions = await provider.getPossibleMissionActionStatuses();

return possibleOptions;
} else {
this.#userAPI.error('User provider does not support mission status options');
}
}

/**
* Fetch the list of possible mission actions
* @returns {Promise<string[]>} the list of possible mission actions
*/
async getPossibleMissionActions() {
const provider = this.#userAPI.getProvider();

if (provider.getPossibleMissionActions) {
const possibleActions = await provider.getPossibleMissionActions();

return possibleActions;
} else {
this.#userAPI.error('User provider does not support mission statuses');
}
}

/**
* @returns {Promise<Array<Status>>} the complete list of possible states that an operator can reply to a poll question with.
*/
Expand Down Expand Up @@ -166,6 +229,21 @@ export default class StatusAPI extends EventEmitter {
}
}

/**
* @param {MissionAction} action
* @param {MissionStatusOption} status
* @returns {Promise<Boolean>} true if operation was successful, otherwise false.
*/
setStatusForMissionAction(action, status) {
const provider = this.#userAPI.getProvider();

if (provider.setStatusForMissionAction) {
return provider.setStatusForMissionAction(action, status);
} else {
this.#userAPI.error('User provider does not support setting mission role status');
}
}

/**
* Resets the status of the provided role back to its default status.
* @param {import("./UserAPI").Role} role The role to set the status for.
Expand Down Expand Up @@ -245,6 +323,7 @@ export default class StatusAPI extends EventEmitter {
if (typeof provider.on === 'function') {
provider.on('statusChange', this.onProviderStatusChange);
provider.on('pollQuestionChange', this.onProviderPollQuestionChange);
provider.on('missionActionStatusChange', this.onMissionActionStatusChange);
}
}

Expand All @@ -261,21 +340,43 @@ export default class StatusAPI extends EventEmitter {
onProviderPollQuestionChange(pollQuestion) {
this.emit('pollQuestionChange', pollQuestion);
}

/**
* @private
*/
onMissionActionStatusChange({ action, status }) {
this.emit('missionActionStatusChange', { action, status });
}
}

/**
* @typedef {import('./UserProvider')} UserProvider
*/

/**
* @typedef {import('./StatusUserProvider')} StatusUserProvider
*/

/**
* The PollQuestion type
* @typedef {Object} PollQuestion
* @property {String} question - The question to be presented to users
* @property {Number} timestamp - The time that the poll question was set.
*/

/**
* The MissionStatus type
* @typedef {Object} MissionStatusOption
* @extends {Status}
* @property {String} color A color to be used when displaying the mission status
*/

/**
* @typedef {Object} MissionAction
* @property {String} key A unique identifier for this action
* @property {String} label A human readable label for this action
*/

/**
* The Status type
* @typedef {Object} Status
Expand Down
4 changes: 2 additions & 2 deletions src/api/user/StatusUserProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ import UserProvider from './UserProvider.js';

export default class StatusUserProvider extends UserProvider {
/**
* @param {('statusChange'|'pollQuestionChange')} event the name of the event to listen to
* @param {('statusChange'|'pollQuestionChange'|'missionActionStatusChange')} 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 {('statusChange'|'pollQuestionChange'|'missionActionStatusChange')} event the name of the event to stop listen to
* @param {Function} callback the callback function used to register the listener
*/
off(event, callback) {}
Expand Down
47 changes: 0 additions & 47 deletions src/api/user/UserAPISpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,6 @@ import ExampleUserProvider from '../../../example/exampleUser/ExampleUserProvide
import { createOpenMct, resetApplicationState } from '../../utils/testing.js';
import { MULTIPLE_PROVIDER_ERROR } from './constants.js';

const USERNAME = 'Test User';
const EXAMPLE_ROLE = 'flight';

describe('The User API', () => {
let openmct;

Expand Down Expand Up @@ -65,48 +62,4 @@ describe('The User API', () => {
expect(openmct.user.hasProvider()).toBeTrue();
});
});

describe('provides the ability', () => {
let provider;

beforeEach(() => {
provider = new ExampleUserProvider(openmct);
provider.autoLogin(USERNAME);
});

it('to check if a user (not specific) is logged in', (done) => {
expect(openmct.user.isLoggedIn()).toBeFalse();

openmct.user.on('providerAdded', () => {
expect(openmct.user.isLoggedIn()).toBeTrue();
done();
});

// this will trigger the user indicator plugin,
// which will in turn login the user
openmct.user.setProvider(provider);
});

it('to get the current user', (done) => {
openmct.user.setProvider(provider);
openmct.user
.getCurrentUser()
.then((apiUser) => {
expect(apiUser.name).toEqual(USERNAME);
})
.finally(done);
});

it('to check if a user has a specific role (by id)', (done) => {
openmct.user.setProvider(provider);
let junkIdCheckPromise = openmct.user.hasRole('junk-id').then((hasRole) => {
expect(hasRole).toBeFalse();
});
let realIdCheckPromise = openmct.user.hasRole(EXAMPLE_ROLE).then((hasRole) => {
expect(hasRole).toBeTrue();
});

Promise.all([junkIdCheckPromise, realIdCheckPromise]).finally(done);
});
});
});
9 changes: 2 additions & 7 deletions src/plugins/operatorStatus/operatorStatus/OperatorStatus.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,7 @@
at runtime from the About dialog for additional information.
-->
<template>
<div
:style="position"
class="c-status-poll-panel c-status-poll-panel--operator"
@click.stop="noop"
>
<div :style="position" class="c-status-poll-panel c-status-poll-panel--operator" @click.stop>
<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>
Expand Down Expand Up @@ -191,8 +187,7 @@ export default {
} else {
return status;
}
},
noop() {}
}
}
};
</script>
Loading

0 comments on commit 82fa4c1

Please sign in to comment.