Compare commits

...

9 Commits

Author SHA1 Message Date
Charles Hacskaylo
14d6c57bfa Closes #7420
- Styling and markup for mission status control panel.
- Tweaks and additions to some common style elements.
2024-01-26 13:36:47 -08:00
Jesse Mazzella
31ee4d925e changes 2024-01-26 11:45:26 -08:00
Mazzella, Jesse D. (ARC-TI)[KBR Wyle Services, LLC]
c24f310126 feat(WIP): dismissible popup, use moar compositionAPI 2024-01-25 17:38:01 -08:00
Mazzella, Jesse D. (ARC-TI)[KBR Wyle Services, LLC]
458b2822c3 feat(WIP): add composables and dynamically calculate popup position 2024-01-25 17:14:24 -08:00
Mazzella, Jesse D. (ARC-TI)[KBR Wyle Services, LLC]
f11e4aa7a1 feat(WIP): can display mission statuses now 2024-01-25 14:09:58 -08:00
Mazzella, Jesse D. (ARC-TI)[KBR Wyle Services, LLC]
ebc67ebbc9 feat: support mission statuses 2024-01-24 13:35:48 -08:00
Mazzella, Jesse D. (ARC-TI)[KBR Wyle Services, LLC]
0d1a6f97c2 feat(WIP): working on mission status indicators 2024-01-24 13:13:37 -08:00
Mazzella, Jesse D. (ARC-TI)[KBR Wyle Services, LLC]
5984315929 style(WIP): filler styles for user-indicator 2024-01-24 13:12:54 -08:00
Mazzella, Jesse D. (ARC-TI)[KBR Wyle Services, LLC]
8040d1d5be refactor: UserIndicator use vue component directly 2024-01-24 13:12:27 -08:00
11 changed files with 579 additions and 51 deletions

View File

@@ -100,6 +100,52 @@ 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 list of possible mission status options
* @returns {Promise<MissionStatusOption[]>} the current mission status
*/
async getPossibleMissionStatusOptions() {
const provider = this.#userAPI.getProvider();
if (provider.getPossibleMissionStatusOptions) {
const possibleOptions = await provider.getPossibleMissionStatusOptions();
return possibleOptions;
} else {
this.#userAPI.error('User provider does not support mission status options');
}
}
/**
* Fetch the list of possible mission status options
* @returns {Promise<MissionStatusOption[]>} the current mission status
*/
async getPossibleMissionStatuses() {
const provider = this.#userAPI.getProvider();
if (provider.getPossibleMissionStatuses) {
const possibleStatuses = await provider.getPossibleMissionStatuses();
return possibleStatuses;
} 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.
*/
@@ -166,6 +212,21 @@ export default class StatusAPI extends EventEmitter {
}
}
/**
* @param {MissionStatusRole} role
* @param {MissionStatusOption} status
* @returns {Promise<Boolean>} true if operation was successful, otherwise false.
*/
setStatusForMissionRole(role, status) {
const provider = this.#userAPI.getProvider();
if (provider.setStatusForMissionRole) {
return provider.setStatusForMissionRole(role, 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.
@@ -276,6 +337,19 @@ export default class StatusAPI extends EventEmitter {
* @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} MissionStatusRole
* @property {String} key A unique identifier for this role
* @property {String} label A human readable label for this role
*/
/**
* The Status type
* @typedef {Object} Status

View File

@@ -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>
@@ -191,8 +187,7 @@ export default {
} else {
return status;
}
},
noop() {}
}
}
};
</script>

View File

@@ -0,0 +1,57 @@
<template>
<div class="c-user-control-panel__component">
<div class="c-user-control-panel__header">
<div class="c-user-control-panel__title">Mission Status</div>
<button
aria-label="Close"
class="c-icon-button c-icon-button--sm t-close-btn icon-x"
@click.stop="onDismiss"
></button>
</div>
<div class="c-ucp-mission-status">
<template v-for="status in missionStatuses" :key="status">
<label class="c-ucp-mission-status__label" :for="status">{{ status }}</label>
<div class="c-ucp-mission-status__widget --is-no-go">NO GO</div>
<div class="c-ucp-mission-status__select">
<select :id="status.label">
<option v-for="option in missionStatusOptions" :key="option.key">
{{ option.label }}
</option>
</select>
</div>
</template>
</div>
</div>
</template>
<script>
export default {
inject: ['openmct'],
emits: ['dismiss'],
data() {
return {
missionStatuses: [],
missionStatusOptions: []
};
},
async created() {
this.missionStatuses = ['Command', 'Drive', 'Camera'];
this.missionStatusOptions = [
{
key: '0',
label: 'NO GO'
},
{
key: '1',
label: 'GO'
}
];
},
methods: {
onDismiss() {
this.$emit('dismiss');
}
}
};
</script>

View File

@@ -21,18 +21,59 @@
-->
<template>
<div class="c-indicator icon-person c-indicator--clickable">
<div
ref="userIndicatorRef"
class="c-indicator c-indicator--user icon-person"
:class="canSetMissionStatus ? 'c-indicator--clickable' : ''"
v-bind="$attrs"
@click.stop="togglePopup"
>
<span class="label c-indicator__label" aria-label="User Role">
{{ role ? `${userName}: ${role}` : userName }}
<button v-if="availableRoles?.length > 1" @click="promptForRoleSelection">Change Role</button>
<button v-if="availableRoles?.length > 1" @click.stop="promptForRoleSelection">
Change Role
</button>
</span>
</div>
<Teleport to="body">
<div v-show="isPopupVisible" ref="popupRef" class="c-user-control-panel" :style="popupStyle">
<MissionStatusPopup v-show="true" @dismiss="togglePopup" />
</div>
</Teleport>
</template>
<script>
import { ref } from 'vue';
import ActiveRoleSynchronizer from '../../../api/user/ActiveRoleSynchronizer.js';
import { useEventListener } from '../../../ui/composables/event.js';
import { useWindowResize } from '../../../ui/composables/resize.js';
import MissionStatusPopup from './MissionStatusPopup.vue';
export default {
name: 'UserIndicator',
components: {
MissionStatusPopup
},
inject: ['openmct'],
inheritAttrs: false,
setup() {
const { windowSize } = useWindowResize();
const isPopupVisible = ref(false);
const userIndicatorRef = ref(null);
const popupRef = ref(null);
// eslint-disable-next-line func-style
const handleOutsideClick = (event) => {
if (isPopupVisible.value && popupRef.value && !popupRef.value.contains(event.target)) {
isPopupVisible.value = false;
}
};
useEventListener(document, 'click', handleOutsideClick);
return { windowSize, isPopupVisible, popupRef, userIndicatorRef };
},
data() {
return {
userName: undefined,
@@ -40,29 +81,64 @@ export default {
availableRoles: [],
loggedIn: false,
inputRoleSelection: undefined,
roleSelectionDialog: undefined
roleSelectionDialog: undefined,
canSetMissionStatus: true
};
},
computed: {
popupStyle() {
return {
top: `${this.position.top}px`,
left: `${this.position.left}px`
};
},
position() {
if (!this.isPopupVisible) {
return { top: 0, left: 0 };
}
const indicator = this.userIndicatorRef;
const indicatorRect = indicator.getBoundingClientRect();
let top = indicatorRect.bottom;
let left = indicatorRect.left;
async mounted() {
const popupRect = this.popupRef.getBoundingClientRect();
const popupWidth = popupRect.width;
const popupHeight = popupRect.height;
// Check if the popup goes beyond the right edge of the window
if (left + popupWidth > this.windowSize.width) {
left = this.windowSize.width - popupWidth; // Adjust left to fit within the window
}
// Check if the popup goes beyond the bottom edge of the window
if (top + popupHeight > this.windowSize.height) {
top = indicatorRect.top - popupHeight; // Place popup above the indicator
}
return { top, left };
}
},
async created() {
await this.getUserInfo();
},
mounted() {
// need to wait for openmct to be loaded before using openmct.overlays.selection
// as document.body could be null
this.openmct.on('start', this.fetchOrPromptForRole);
await this.getUserInfo();
this.roleChannel = new ActiveRoleSynchronizer(this.openmct);
this.roleChannel.subscribeToRoleChanges(this.onRoleChange);
// this.openmct.on('start', this.fetchOrPromptForRole);
// this.roleChannel = new ActiveRoleSynchronizer(this.openmct);
// this.roleChannel.subscribeToRoleChanges(this.onRoleChange);
},
beforeUnmount() {
this.roleChannel.unsubscribeFromRoleChanges(this.onRoleChange);
this.openmct.off('start', this.fetchOrPromptForRole);
// this.roleChannel.unsubscribeFromRoleChanges(this.onRoleChange);
// this.openmct.off('start', this.fetchOrPromptForRole);
},
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();
// const user = await this.openmct.user.getCurrentUser();
this.canSetMissionStatus = true;
this.userName = 'Charles';
this.role = 'Cool guy';
this.loggedIn = true;
},
async fetchOrPromptForRole() {
const UserAPI = this.openmct.user;
@@ -138,6 +214,9 @@ export default {
this.openmct.user.setActiveRole(role);
// update other tabs through broadcast channel
this.roleChannel.broadcastNewRole(role);
},
togglePopup() {
this.isPopupVisible = !this.isPopupVisible;
}
}
};

View File

@@ -20,43 +20,25 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import mount from 'utils/mount';
import UserIndicator from './components/UserIndicator.vue';
export default function UserIndicatorPlugin() {
function addIndicator(openmct) {
const { vNode, destroy } = mount(
{
components: {
UserIndicator
},
provide: {
openmct: openmct
},
template: '<UserIndicator />'
},
{
app: openmct.app
}
);
openmct.indicators.add({
key: 'user-indicator',
element: vNode.el,
priority: openmct.priority.HIGH,
destroy: destroy
vueComponent: UserIndicator,
priority: openmct.priority.HIGH
});
}
return function install(openmct) {
if (openmct.user.hasProvider()) {
addIndicator(openmct);
} else {
// back up if user provider added after indicator installed
openmct.user.on('providerAdded', () => {
addIndicator(openmct);
});
}
// if (openmct.user.hasProvider()) {
addIndicator(openmct);
// } else {
// back up if user provider added after indicator installed
// openmct.user.on('providerAdded', () => {
// addIndicator(openmct);
// });
// }
};
}

View File

@@ -0,0 +1,190 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, 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.
*****************************************************************************/
.c-indicator {
&:before {
// Indicator icon
color: $colorKey;
}
&--clickable {
cursor: pointer;
max-width: 250px;
@include hover() {
background: $colorIndicatorBgHov;
}
}
}
$statusCountWidth: 30px;
.c-user-control-panel {
@include menuOuter();
display: flex;
flex-direction: column;
padding: $interiorMarginLg;
min-width: max-content;
max-width: 35%;
> * + * {
margin-top: $interiorMarginLg;
}
*:before {
font-size: 0.8em;
margin-right: $interiorMarginSm; // WTF - this is BAD!
}
&__section {
display: flex;
align-items: center;
flex-direction: row;
> * + * {
margin-left: $interiorMarginLg;
}
}
&__header {
align-items: center;
display: flex;
column-gap: $interiorMargin;
text-transform: uppercase;
> * { flex: 0 0 auto; }
[class*='title'] {
flex: 1 1 auto;
}
}
.t-close-btn {
&:before {
margin-right: 0;
}
}
&__component {
// General classes for new control panel component
display: flex;
flex-direction: column;
gap: $interiorMargin;
}
&__top {
text-transform: uppercase;
}
&__user-role,
&__updated {
opacity: 50%;
}
&__updated {
flex: 1 1 auto;
text-align: right;
}
&__poll-question {
background: $colorBodyFg;
color: $colorBodyBg;
border-radius: $controlCr;
font-weight: bold;
padding: $interiorMarginSm $interiorMargin;
.c-user-control-panel--admin & {
background: rgba($colorBodyFg, 0.1);
color: $colorBodyFg;
}
}
/*************************************************** ADMIN INTERFACE */
&__content {
$m: $interiorMargin;
display: grid;
grid-template-columns: max-content 1fr;
grid-column-gap: $m;
grid-row-gap: $m;
[class*='__label'] {
padding: 3px 0;
}
[class*='__label'] {
padding: 3px 0;
}
[class*='__poll-table'] {
grid-column: span 2;
}
[class*='new-question'] {
align-items: center;
display: flex;
flex-direction: row;
> * + * {
margin-left: $interiorMargin;
}
input {
flex: 1 1 auto;
height: $btnStdH;
}
button {
flex: 0 0 auto;
}
}
}
}
/**************** STYLES FOR THE MISSION STATUS USER CONTROL PANEL */
.c-ucp-mission-status {
$bg: rgba(black, 0.7);
display: grid;
grid-template-columns: max-content max-content 1fr;
align-items: center;
grid-column-gap: $interiorMarginLg;
grid-row-gap: $interiorMargin;
&__widget {
border-radius: $basicCr;
background: $bg;
border: 1px solid transparent;
padding: $interiorMarginSm $interiorMarginLg;
text-align: center;
&.--is-go {
$c: #2C7527;
background: $c;
color: white;
}
&.--is-no-go {
$c: #FBC147;
background: $bg;
border: 1px solid $c;
color: $c;
}
}
}

View File

@@ -222,6 +222,10 @@ button {
font-size: 1.1em;
}
}
&--sm {
padding: $interiorMarginSm $interiorMargin;
}
}
.c-list-button {

View File

@@ -98,6 +98,7 @@ body.desktop {
}
div,
ul,
span {
// Firefox
scrollbar-color: $scrollbarThumbColor $scrollbarTrackColorBg;

View File

@@ -57,7 +57,8 @@
@import '../plugins/notebook/components/sidebar.scss';
@import '../plugins/gauge/gauge.scss';
@import '../plugins/faultManagement/fault-manager.scss';
@import '../plugins/operatorStatus/operator-status';
@import '../plugins/operatorStatus/operator-status.scss';
@import '../plugins/userIndicator/user-indicator.scss';
@import '../plugins/inspectorDataVisualization/inspector-data-visualization.scss';
#splash-screen {

View File

@@ -0,0 +1,77 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, 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.
*****************************************************************************/
/* eslint-disable func-style */
import { isRef, onBeforeMount, onBeforeUnmount, onMounted, watch } from 'vue';
/**
* Registers an event listener on the specified target and automatically removes it when the
* component is unmounted.
* This is a Vue composition API utility function.
* @param {EventTarget} target - The target to attach the event listener to.
* @param {string} event - The name of the event to listen for.
* @param {Function} callback - The callback function to execute when the event is triggered.
*/
export function useEventListener(target, event, handler) {
const addListener = (el) => {
if (el) {
el.addEventListener(event, handler);
}
};
const removeListener = (el) => {
if (el) {
el.removeEventListener(event, handler);
}
};
// If target is a reactive ref, watch it for changes
if (isRef(target)) {
watch(
target,
(newTarget, oldTarget) => {
if (newTarget !== oldTarget) {
removeListener(oldTarget);
addListener(newTarget);
}
},
{ immediate: true }
);
} else {
// Otherwise use lifecycle hooks to add/remove listener
onMounted(() => addListener(target));
onBeforeUnmount(() => removeListener(target));
}
}
/**
* Registers an event listener on the specified EventEmitter instance and automatically removes it
* when the component is unmounted.
* This is a Vue composition API utility function.
* @param {import('eventemitter3').EventEmitter} emitter - The EventEmitter instance to attach the event listener to.
* @param {string} event - The name of the event to listen for.
* @param {Function} callback - The callback function to execute when the event is triggered.
*/
export function useEventEmitter(emitter, event, callback) {
onBeforeMount(() => emitter.on(event, callback));
onBeforeUnmount(() => emitter.off(event, callback));
}

View File

@@ -0,0 +1,68 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, 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.
*****************************************************************************/
/* eslint-disable func-style */
import { onBeforeUnmount, reactive } from 'vue';
import throttle from '../../utils/throttle.js';
import { useEventListener } from './event.js';
export function useResizeObserver() {
const size = reactive({ width: 0, height: 0 });
let observer;
const startObserving = (element) => {
if (!element) {
return;
}
observer = new ResizeObserver((entries) => {
if (entries[0]) {
const { width, height } = entries[0].contentRect;
size.width = width;
size.height = height;
}
});
observer.observe(element);
};
onBeforeUnmount(() => {
if (observer) {
observer.disconnect();
}
});
return { size, startObserving };
}
export function useWindowResize() {
const windowSize = reactive({ width: window.innerWidth, height: window.innerHeight });
const handleResize = throttle(() => {
windowSize.width = window.innerWidth;
windowSize.height = window.innerHeight;
}, 100);
useEventListener(window, 'resize', handleResize);
return { windowSize };
}