6098 operator status indicator v11 improvements (#6112)

* Added clear poll button to clear all statuses
* Clear current poll question
* Added table for operator status

Co-authored-by: Michael Rogers <contact@mhrogers.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
This commit is contained in:
Scott Bell 2023-01-20 03:56:46 +01:00 committed by GitHub
parent e0ca6200bb
commit 22621aaaf8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 350 additions and 12 deletions

View File

@ -0,0 +1,27 @@
/*****************************************************************************
* 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.
*****************************************************************************/
// This should be used to install the Example User
document.addEventListener('DOMContentLoaded', () => {
const openmct = window.openmct;
openmct.install(openmct.plugins.example.ExampleUser());
});

View File

@ -0,0 +1,27 @@
/*****************************************************************************
* 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.
*****************************************************************************/
// This should be used to install the Operator Status
document.addEventListener('DOMContentLoaded', () => {
const openmct = window.openmct;
openmct.install(openmct.plugins.OperatorStatus());
});

View File

@ -0,0 +1,156 @@
/*****************************************************************************
* 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.
*****************************************************************************/
/*
* This test suite is dedicated to testing the operator status plugin.
*/
const path = require('path');
const { test, expect } = require('../../../../pluginFixtures');
/*
Precondition: Inject Example User, Operator Status Plugins
Verify that user 1 sees updates from user/role 2 (Not possible without openmct-yamcs implementation)
Clear Role Status of single user test
STUB (test.fixme) Rolling through each
*/
test.describe('Operator Status', () => {
test.beforeEach(async ({ page }) => {
// FIXME: determine if plugins will be added to index.html or need to be injected
// eslint-disable-next-line no-undef
await page.addInitScript({ path: path.join(__dirname, '../../../../helper/', 'addInitExampleUser.js')});
// eslint-disable-next-line no-undef
await page.addInitScript({ path: path.join(__dirname, '../../../../helper/', 'addInitOperatorStatus.js')});
await page.goto('./', { waitUntil: 'networkidle' });
});
// verify that operator status is visible
test('operator status is visible and expands when clicked', async ({ page }) => {
await expect(page.locator('div[title="Set my operator status"]')).toBeVisible();
await page.locator('div[title="Set my operator status"]').click();
// expect default status to be 'GO'
await expect(page.locator('.c-status-poll-panel')).toBeVisible();
});
test('poll question indicator remains when blank poll set', async ({ page }) => {
await expect(page.locator('div[title="Set the current poll question"]')).toBeVisible();
await page.locator('div[title="Set the current poll question"]').click();
// set to blank
await page.getByRole('button', { name: 'Update' }).click();
// should still be visible
await expect(page.locator('div[title="Set the current poll question"]')).toBeVisible();
});
// Verify that user 1 sees updates from user/role 2 (Not possible without openmct-yamcs implementation)
test('operator status table reflects answered values', async ({ page }) => {
// user navigates to operator status poll
const statusPollIndicator = page.locator('div[title="Set my operator status"]');
await statusPollIndicator.click();
// get user role value
const userRole = page.locator('.c-status-poll-panel__user-role');
const userRoleText = await userRole.innerText();
// get selected status value
const selectStatus = page.locator('select[name="setStatus"]');
await selectStatus.selectOption({ index: 1});
const initialStatusValue = await selectStatus.inputValue();
// open manage status poll
const manageStatusPollIndicator = page.locator('div[title="Set the current poll question"]');
await manageStatusPollIndicator.click();
// parse the table row values
const row = page.locator(`tr:has-text("${userRoleText}")`);
const rowValues = await row.innerText();
const rowValuesArr = rowValues.split('\t');
const COLUMN_STATUS_INDEX = 1;
// check initial set value matches status table
expect(rowValuesArr[COLUMN_STATUS_INDEX].toLowerCase())
.toEqual(initialStatusValue.toLowerCase());
// change user status
await statusPollIndicator.click();
// FIXME: might want to grab a dynamic option instead of arbitrary
await page.locator('select[name="setStatus"]').selectOption({ index: 2});
const updatedStatusValue = await selectStatus.inputValue();
// verify user status is reflected in table
await manageStatusPollIndicator.click();
const updatedRow = page.locator(`tr:has-text("${userRoleText}")`);
const updatedRowValues = await updatedRow.innerText();
const updatedRowValuesArr = updatedRowValues.split('\t');
expect(updatedRowValuesArr[COLUMN_STATUS_INDEX].toLowerCase())
.toEqual(updatedStatusValue.toLowerCase());
});
test('clear poll button removes poll responses', async ({ page }) => {
// user navigates to operator status poll
const statusPollIndicator = page.locator('div[title="Set my operator status"]');
await statusPollIndicator.click();
// get user role value
const userRole = page.locator('.c-status-poll-panel__user-role');
const userRoleText = await userRole.innerText();
// get selected status value
const selectStatus = page.locator('select[name="setStatus"]');
// FIXME: might want to grab a dynamic option instead of arbitrary
await selectStatus.selectOption({ index: 1});
const initialStatusValue = await selectStatus.inputValue();
// open manage status poll
const manageStatusPollIndicator = page.locator('div[title="Set the current poll question"]');
await manageStatusPollIndicator.click();
// parse the table row values
const row = page.locator(`tr:has-text("${userRoleText}")`);
const rowValues = await row.innerText();
const rowValuesArr = rowValues.split('\t');
const COLUMN_STATUS_INDEX = 1;
// check initial set value matches status table
expect(rowValuesArr[COLUMN_STATUS_INDEX].toLowerCase())
.toEqual(initialStatusValue.toLowerCase());
// clear the poll
await page.locator('button[title="Clear the previous poll question"]').click();
const updatedRow = page.locator(`tr:has-text("${userRoleText}")`);
const updatedRowValues = await updatedRow.innerText();
const updatedRowValuesArr = updatedRowValues.split('\t');
const UNSET_VALUE_LABEL = 'Not set';
expect(updatedRowValuesArr[COLUMN_STATUS_INDEX])
.toEqual(UNSET_VALUE_LABEL);
});
test.fixme('iterate through all possible response values', async ({ page }) => {
// test all possible respone values for the poll
});
});

View File

@ -65,7 +65,7 @@ export default class ExampleUserProvider extends EventEmitter {
this.user = undefined; this.user = undefined;
this.loggedIn = false; this.loggedIn = false;
this.autoLoginUser = undefined; this.autoLoginUser = undefined;
this.status = STATUSES[1]; this.status = STATUSES[0];
this.pollQuestion = undefined; this.pollQuestion = undefined;
this.defaultStatusRole = defaultStatusRole; this.defaultStatusRole = defaultStatusRole;
@ -124,6 +124,7 @@ export default class ExampleUserProvider extends EventEmitter {
} }
setStatusForRole(role, status) { setStatusForRole(role, status) {
status.timestamp = Date.now();
this.status = status; this.status = status;
this.emit('statusChange', { this.emit('statusChange', {
role, role,
@ -133,14 +134,23 @@ export default class ExampleUserProvider extends EventEmitter {
return true; return true;
} }
getPollQuestion() { // eslint-disable-next-line require-await
return Promise.resolve({ async getPollQuestion() {
question: 'Set "GO" if your position is ready for a boarding action on the Klingon cruiser', if (this.pollQuestion) {
timestamp: Date.now() return this.pollQuestion;
}); } else {
return undefined;
}
} }
setPollQuestion(pollQuestion) { 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 = { this.pollQuestion = {
question: pollQuestion, question: pollQuestion,
timestamp: Date.now() timestamp: Date.now()

View File

@ -291,5 +291,6 @@ export default class StatusAPI extends EventEmitter {
* The Status type * The Status type
* @typedef {Object} Status * @typedef {Object} Status
* @property {String} key - A unique identifier for this status * @property {String} key - A unique identifier for this status
* @property {Number} label - A human readable label 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

@ -88,6 +88,14 @@
padding: 3px 0; padding: 3px 0;
} }
[class*='__label'] {
padding: 3px 0;
}
[class*='__poll-table'] {
grid-column: span 2;
}
[class*='new-question'] { [class*='new-question'] {
align-items: center; align-items: center;
display: flex; display: flex;
@ -123,6 +131,12 @@
opacity: 0.6; opacity: 0.6;
} }
} }
&__actions {
display:flex;
flex: auto;
flex-direction: row;
justify-content: flex-end;
}
} }
.c-indicator { .c-indicator {

View File

@ -58,6 +58,13 @@
{{ entry.roleCount }} {{ entry.roleCount }}
</div> </div>
</div> </div>
<div class="c-status-poll-report__actions">
<button
class="c-button"
title="Clear the previous poll question"
@click="clearPollQuestion"
>Clear Poll</button>
</div>
</div> </div>
</template> </template>
@ -74,6 +81,41 @@
@click="updatePollQuestion" @click="updatePollQuestion"
>Update</button> >Update</button>
</div> </div>
<div class="c-table c-spq__poll-table">
<table class="c-table__body">
<thead class="c-table__header">
<tr>
<th>
Position
</th>
<th>
Status
</th>
<th>
Age
</th>
</tr>
</thead>
<tbody>
<tr
v-for="statusForRole in statusesForRolesViewModel"
:key="statusForRole.key"
>
<td>
{{ statusForRole.role }}
</td>
<td
:style="{ background: statusForRole.status.statusBgColor, color: statusForRole.status.statusFgColor }"
>
{{ statusForRole.status.label }}
</td>
<td>
{{ statusForRole.age }}
</td>
</tr>
</tbody>
</table>
</div>
</div> </div>
</div> </div>
@ -97,9 +139,11 @@ export default {
data() { data() {
return { return {
pollQuestionUpdated: '--', pollQuestionUpdated: '--',
pollQuestionTimestamp: undefined,
currentPollQuestion: '--', currentPollQuestion: '--',
newPollQuestion: undefined, newPollQuestion: undefined,
statusCountViewModel: [] statusCountViewModel: [],
statusesForRolesViewModel: []
}; };
}, },
computed: { computed: {
@ -135,9 +179,17 @@ export default {
this.openmct.user.status.on('pollQuestionChange', this.setPollQuestion); this.openmct.user.status.on('pollQuestionChange', this.setPollQuestion);
}, },
setPollQuestion(pollQuestion) { setPollQuestion(pollQuestion) {
this.currentPollQuestion = pollQuestion.question; let pollQuestionText = pollQuestion.question;
if (!pollQuestionText || pollQuestionText === '') {
pollQuestionText = '--';
this.indicator.text('No Poll Question');
} else {
this.indicator.text(pollQuestionText);
}
this.currentPollQuestion = pollQuestionText;
this.pollQuestionTimestamp = pollQuestion.timestamp;
this.pollQuestionUpdated = new Date(pollQuestion.timestamp).toISOString(); this.pollQuestionUpdated = new Date(pollQuestion.timestamp).toISOString();
this.indicator.text(pollQuestion.question);
}, },
async updatePollQuestion() { async updatePollQuestion() {
const result = await this.openmct.user.status.setPollQuestion(this.newPollQuestion); const result = await this.openmct.user.status.setPollQuestion(this.newPollQuestion);
@ -149,6 +201,13 @@ export default {
this.newPollQuestion = undefined; this.newPollQuestion = undefined;
}, },
async clearPollQuestion() {
this.currentPollQuestion = undefined;
await Promise.all([
this.openmct.user.status.resetAllStatuses(),
this.openmct.user.status.setPollQuestion()
]);
},
async fetchStatusSummary() { async fetchStatusSummary() {
const allStatuses = await this.openmct.user.status.getPossibleStatuses(); const allStatuses = await this.openmct.user.status.getPossibleStatuses();
const statusCountMap = allStatuses.reduce((statusToCountMap, status) => { const statusCountMap = allStatuses.reduce((statusToCountMap, status) => {
@ -158,7 +217,6 @@ export default {
}, {}); }, {});
const allStatusRoles = await this.openmct.user.status.getAllStatusRoles(); const allStatusRoles = await this.openmct.user.status.getAllStatusRoles();
const statusesForRoles = await Promise.all(allStatusRoles.map(role => this.openmct.user.status.getStatusForRole(role))); const statusesForRoles = await Promise.all(allStatusRoles.map(role => this.openmct.user.status.getStatusForRole(role)));
statusesForRoles.forEach((status, i) => { statusesForRoles.forEach((status, i) => {
const currentCount = statusCountMap[status.key]; const currentCount = statusCountMap[status.key];
statusCountMap[status.key] = currentCount + 1; statusCountMap[status.key] = currentCount + 1;
@ -170,6 +228,51 @@ export default {
roleCount: statusCountMap[status.key] roleCount: statusCountMap[status.key]
}; };
}); });
const defaultStatuses = await Promise.all(allStatusRoles.map(role => this.openmct.user.status.getDefaultStatusForRole(role)));
this.statusesForRolesViewModel = [];
statusesForRoles.forEach((status, index) => {
const isDefaultStatus = defaultStatuses[index].key === status.key;
let statusTimestamp = status.timestamp;
if (isDefaultStatus) {
// if the default is selected, set timestamp to undefined
statusTimestamp = undefined;
}
this.statusesForRolesViewModel.push({
status: this.applyStyling(status),
role: allStatusRoles[index],
age: this.formatStatusAge(statusTimestamp, this.pollQuestionTimestamp)
});
});
},
formatStatusAge(statusTimestamp, pollQuestionTimestamp) {
if (statusTimestamp === undefined || pollQuestionTimestamp === undefined) {
return '--';
}
const statusAgeInMs = statusTimestamp - pollQuestionTimestamp;
const absoluteTotalSeconds = Math.floor(Math.abs(statusAgeInMs) / 1000);
let hours = Math.floor(absoluteTotalSeconds / 3600);
let minutes = Math.floor((absoluteTotalSeconds - (hours * 3600)) / 60);
let secondsString = absoluteTotalSeconds - (hours * 3600) - (minutes * 60);
if (statusAgeInMs > 0 || (absoluteTotalSeconds === 0)) {
hours = `+ ${hours}`;
} else {
hours = `- ${hours}`;
}
if (minutes < 10) {
minutes = `0${minutes}`;
}
if (secondsString < 10) {
secondsString = `0${secondsString}`;
}
const statusAgeString = `${hours}:${minutes}:${secondsString}`;
return statusAgeString;
}, },
applyStyling(status) { applyStyling(status) {
const stylesForStatus = this.configuration?.statusStyles?.[status.label]; const stylesForStatus = this.configuration?.statusStyles?.[status.label];

View File

@ -51,7 +51,7 @@ export default class PollQuestionIndicator extends AbstractStatusIndicator {
createIndicator() { createIndicator() {
const pollQuestionIndicator = this.openmct.indicators.simpleIndicator(); const pollQuestionIndicator = this.openmct.indicators.simpleIndicator();
pollQuestionIndicator.text("Poll Question"); pollQuestionIndicator.text("No Poll Question");
pollQuestionIndicator.description("Set the current poll question"); pollQuestionIndicator.description("Set the current poll question");
pollQuestionIndicator.iconClass('icon-status-poll-edit'); pollQuestionIndicator.iconClass('icon-status-poll-edit');
pollQuestionIndicator.element.classList.add("c-indicator--operator-status"); pollQuestionIndicator.element.classList.add("c-indicator--operator-status");