From f5796c984ef84f918c0a7448440013f6f2469e0a Mon Sep 17 00:00:00 2001 From: Charles Hacskaylo Date: Thu, 2 Jun 2022 13:46:13 -0700 Subject: [PATCH] Operator status (#5179) * Added click event to simple indicator * Moved operator status plugin to Open * Implementing user role status API * Support adding indicators asynchronously * Adding user status API * Updated example user provider * Update icon with status * Adding admin indicator * Apply config options * Set status class on indicator. Clear all statuses * Show poll question in op stat indicator * Implementing status summary * Get statuses from providers. Reset statuses when poll question set * Styling for operator status - New icon glyph - IMPORTANT: OVERRIDE ANY MERGE CONFLICTS USING THIS COMMIT! - Fixed erroneous font glyph mapping; - Added default color for indicator icon; - Changed user indicator to display response when set to other than "NO_STATUS". - Standardized icon display. * Cherrypick symbols font updates from restricted-notebook branch. This is the most full and complete version of the symbols font - OVERRIDE ANY MERGE CONFLICTS WITH THIS COMMIT! * Fix positioning of popups * Also fix positioning of status indicator * Get roles by status instead of users * Refactor how status summary is determined to simplify API * Re-fetch status summary on status change * Implemented status reset * Move status into separate API * Refactor user status to its own sub-API * Create RAF utility class * Error handling * Add copyright notices * Fix test issues * Added jsdocs * Additional tests for raf utility function * Move status style configuration into Open * Move styling from the API into the view * Added some docs * Added some unit tests and fixed a bug found in the process. Tests work\! Co-authored-by: Andrew Henry --- example/exampleUser/ExampleUserProvider.js | 105 +++- example/exampleUser/plugin.js | 15 +- example/exampleUser/pluginSpec.js | 7 +- src/api/api.js | 2 +- src/api/indicators/IndicatorAPI.js | 34 +- src/api/indicators/SimpleIndicator.js | 171 ++++--- src/api/telemetry/TelemetryMetadataManager.js | 2 +- src/api/user/StatusAPI.js | 295 +++++++++++ src/api/user/StatusUserProvider.js | 81 +++ src/api/user/UserAPI.js | 42 +- src/api/user/UserProvider.js | 36 ++ src/api/user/UserStatusAPISpec.js | 103 ++++ .../components/TelemetryView.vue | 2 +- src/plugins/objectMigration/Migrations.js | 2 +- .../operatorStatus/AbstractStatusIndicator.js | 106 ++++ .../operatorStatus/operator-status.scss | 142 ++++++ .../operatorStatus/OperatorStatus.vue | 187 +++++++ .../operatorStatus/OperatorStatusIndicator.js | 63 +++ src/plugins/operatorStatus/plugin.js | 50 ++ .../pollQuestion/PollQuestion.vue | 184 +++++++ .../pollQuestion/PollQuestionIndicator.js | 63 +++ src/plugins/plugins.js | 3 + src/styles/_constants.scss | 9 + src/styles/_glyphs.scss | 9 + src/styles/fonts/Open MCT Symbols 16px.json | 473 +++++++++++++----- src/styles/fonts/Open-MCT-Symbols-16px.svg | 328 ++++++------ src/styles/fonts/Open-MCT-Symbols-16px.ttf | Bin 24692 -> 26020 bytes src/styles/fonts/Open-MCT-Symbols-16px.woff | Bin 24768 -> 26096 bytes src/styles/vue-styles.scss | 1 + src/ui/layout/status-bar/Indicators.vue | 13 +- src/utils/raf.js | 14 + src/utils/rafSpec.js | 61 +++ 32 files changed, 2216 insertions(+), 387 deletions(-) create mode 100644 src/api/user/StatusAPI.js create mode 100644 src/api/user/StatusUserProvider.js create mode 100644 src/api/user/UserProvider.js create mode 100644 src/api/user/UserStatusAPISpec.js create mode 100644 src/plugins/operatorStatus/AbstractStatusIndicator.js create mode 100644 src/plugins/operatorStatus/operator-status.scss create mode 100644 src/plugins/operatorStatus/operatorStatus/OperatorStatus.vue create mode 100644 src/plugins/operatorStatus/operatorStatus/OperatorStatusIndicator.js create mode 100644 src/plugins/operatorStatus/plugin.js create mode 100644 src/plugins/operatorStatus/pollQuestion/PollQuestion.vue create mode 100644 src/plugins/operatorStatus/pollQuestion/PollQuestionIndicator.js create mode 100644 src/utils/raf.js create mode 100644 src/utils/rafSpec.js diff --git a/example/exampleUser/ExampleUserProvider.js b/example/exampleUser/ExampleUserProvider.js index 7e17de98ef..2926f0ba92 100644 --- a/example/exampleUser/ExampleUserProvider.js +++ b/example/exampleUser/ExampleUserProvider.js @@ -24,16 +24,53 @@ import EventEmitter from 'EventEmitter'; import { v4 as uuid } from 'uuid'; import createExampleUser from './exampleUserCreator'; +const STATUSES = [{ + key: "NO_STATUS", + label: "Not set", + iconClass: "icon-question-mark", + iconClassPoll: "icon-status-poll-question-mark" +}, { + key: "GO", + label: "GO", + iconClass: "icon-check", + iconClassPoll: "icon-status-poll-question-mark", + statusClass: "s-status-ok", + statusBgColor: "#33cc33", + statusFgColor: "#000" +}, { + key: "MAYBE", + label: "MAYBE", + iconClass: "icon-alert-triangle", + iconClassPoll: "icon-status-poll-question-mark", + statusClass: "s-status-warning", + statusBgColor: "#ffb66c", + statusFgColor: "#000" +}, { + key: "NO_GO", + label: "NO GO", + iconClass: "icon-circle-slash", + iconClassPoll: "icon-status-poll-question-mark", + statusClass: "s-status-error", + statusBgColor: "#9900cc", + statusFgColor: "#fff" +}]; +/** + * @implements {StatusUserProvider} + */ export default class ExampleUserProvider extends EventEmitter { - constructor(openmct) { + constructor(openmct, {defaultStatusRole} = {defaultStatusRole: undefined}) { super(); this.openmct = openmct; this.user = undefined; this.loggedIn = false; this.autoLoginUser = undefined; + this.status = STATUSES[1]; + this.pollQuestion = undefined; + this.defaultStatusRole = defaultStatusRole; this.ExampleUser = createExampleUser(this.openmct.user.User); + this.loginPromise = undefined; } isLoggedIn() { @@ -45,11 +82,19 @@ export default class ExampleUserProvider extends EventEmitter { } getCurrentUser() { - if (this.loggedIn) { - return Promise.resolve(this.user); + if (!this.loginPromise) { + this.loginPromise = this._login().then(() => this.user); } - return this._login().then(() => this.user); + return this.loginPromise; + } + + canProvideStatusForRole() { + return Promise.resolve(true); + } + + canSetPollQuestion() { + return Promise.resolve(true); } hasRole(roleId) { @@ -60,6 +105,55 @@ export default class ExampleUserProvider extends EventEmitter { return Promise.resolve(this.user.getRoles().includes(roleId)); } + getStatusRoleForCurrentUser() { + return Promise.resolve(this.defaultStatusRole); + } + + getAllStatusRoles() { + return Promise.resolve([this.defaultStatusRole]); + } + + getStatusForRole(role) { + return Promise.resolve(this.status); + } + + async getDefaultStatusForRole(role) { + const allRoles = await this.getPossibleStatuses(); + + return allRoles?.[0]; + } + + setStatusForRole(role, status) { + this.status = status; + this.emit('statusChange', { + role, + status + }); + + return true; + } + + getPollQuestion() { + return Promise.resolve({ + question: 'Set "GO" if your position is ready for a boarding action on the Klingon cruiser', + timestamp: Date.now() + }); + } + + setPollQuestion(pollQuestion) { + this.pollQuestion = { + question: pollQuestion, + timestamp: Date.now() + }; + this.emit("pollQuestionChange", this.pollQuestion); + + return true; + } + + getPossibleStatuses() { + return Promise.resolve(STATUSES); + } + _login() { const id = uuid(); @@ -108,3 +202,6 @@ export default class ExampleUserProvider extends EventEmitter { ); } } +/** + * @typedef {import('@/api/user/StatusUserProvider').default} StatusUserProvider + */ diff --git a/example/exampleUser/plugin.js b/example/exampleUser/plugin.js index f7094131e6..af533f098b 100644 --- a/example/exampleUser/plugin.js +++ b/example/exampleUser/plugin.js @@ -22,8 +22,19 @@ import ExampleUserProvider from './ExampleUserProvider'; -export default function ExampleUserPlugin() { +export default function ExampleUserPlugin({autoLoginUser, defaultStatusRole} = { + autoLoginUser: 'guest', + defaultStatusRole: 'test-role' +}) { return function install(openmct) { - openmct.user.setProvider(new ExampleUserProvider(openmct)); + const userProvider = new ExampleUserProvider(openmct, { + defaultStatusRole + }); + + if (autoLoginUser !== undefined) { + userProvider.autoLogin(autoLoginUser); + } + + openmct.user.setProvider(userProvider); }; } diff --git a/example/exampleUser/pluginSpec.js b/example/exampleUser/pluginSpec.js index dd8ea6bba5..02719d99d5 100644 --- a/example/exampleUser/pluginSpec.js +++ b/example/exampleUser/pluginSpec.js @@ -26,7 +26,7 @@ import { } from '../../src/utils/testing'; import ExampleUserProvider from './ExampleUserProvider'; -xdescribe("The Example User Plugin", () => { +describe("The Example User Plugin", () => { let openmct; beforeEach(() => { @@ -47,9 +47,4 @@ xdescribe("The Example User Plugin", () => { }); openmct.install(openmct.plugins.example.ExampleUser()); }); - - // The rest of the functionality of the ExampleUser Plugin is - // tested in both the UserAPISpec.js and in the UserIndicatorPlugin spec. - // If that changes, those tests can be moved here. - }); diff --git a/src/api/api.js b/src/api/api.js index 1a0174d574..7e31bec7aa 100644 --- a/src/api/api.js +++ b/src/api/api.js @@ -56,7 +56,7 @@ define([ CompositionAPI: CompositionAPI, EditorAPI: EditorAPI, FormsAPI: FormsAPI, - IndicatorAPI: IndicatorAPI, + IndicatorAPI: IndicatorAPI.default, MenuAPI: MenuAPI.default, NotificationAPI: NotificationAPI.default, ObjectAPI: ObjectAPI, diff --git a/src/api/indicators/IndicatorAPI.js b/src/api/indicators/IndicatorAPI.js index ef81f67884..98d78112ca 100644 --- a/src/api/indicators/IndicatorAPI.js +++ b/src/api/indicators/IndicatorAPI.js @@ -19,27 +19,27 @@ * this source code distribution or the Licensing information page available * at runtime from the About dialog for additional information. *****************************************************************************/ -define([ - './SimpleIndicator', - 'lodash' -], function ( - SimpleIndicator, - _ -) { - function IndicatorAPI(openmct) { + +import EventEmitter from "EventEmitter"; +import SimpleIndicator from "./SimpleIndicator"; + +class IndicatorAPI extends EventEmitter { + constructor(openmct) { + super(); + this.openmct = openmct; this.indicatorObjects = []; } - IndicatorAPI.prototype.getIndicatorObjectsByPriority = function () { + getIndicatorObjectsByPriority() { const sortedIndicators = this.indicatorObjects.sort((a, b) => b.priority - a.priority); return sortedIndicators; - }; + } - IndicatorAPI.prototype.simpleIndicator = function () { + simpleIndicator() { return new SimpleIndicator(this.openmct); - }; + } /** * Accepts an indicator object, which is a simple object @@ -62,14 +62,16 @@ define([ * myIndicator.iconClass("icon-info"); * */ - IndicatorAPI.prototype.add = function (indicator) { + add(indicator) { if (!indicator.priority) { indicator.priority = this.openmct.priority.DEFAULT; } this.indicatorObjects.push(indicator); - }; - return IndicatorAPI; + this.emit('addIndicator', indicator); + } -}); +} + +export default IndicatorAPI; diff --git a/src/api/indicators/SimpleIndicator.js b/src/api/indicators/SimpleIndicator.js index 7556dd512e..1ef99e6888 100644 --- a/src/api/indicators/SimpleIndicator.js +++ b/src/api/indicators/SimpleIndicator.js @@ -20,82 +20,107 @@ * at runtime from the About dialog for additional information. *****************************************************************************/ -define(['zepto', './res/indicator-template.html'], - function ($, indicatorTemplate) { - const DEFAULT_ICON_CLASS = 'icon-info'; +import EventEmitter from 'EventEmitter'; +import indicatorTemplate from './res/indicator-template.html'; - function SimpleIndicator(openmct) { - this.openmct = openmct; - this.element = $(indicatorTemplate)[0]; - this.priority = openmct.priority.DEFAULT; +const DEFAULT_ICON_CLASS = 'icon-info'; - this.textElement = this.element.querySelector('.js-indicator-text'); +class SimpleIndicator extends EventEmitter { + constructor(openmct) { + super(); - //Set defaults - this.text('New Indicator'); - this.description(''); - this.iconClass(DEFAULT_ICON_CLASS); - this.statusClass(''); + this.openmct = openmct; + this.element = compileTemplate(indicatorTemplate)[0]; + this.priority = openmct.priority.DEFAULT; + + this.textElement = this.element.querySelector('.js-indicator-text'); + + //Set defaults + this.text('New Indicator'); + this.description(''); + this.iconClass(DEFAULT_ICON_CLASS); + + this.click = this.click.bind(this); + + this.element.addEventListener('click', this.click); + openmct.once('destroy', () => { + this.removeAllListeners(); + this.element.removeEventListener('click', this.click); + }); + } + + text(text) { + if (text !== undefined && text !== this.textValue) { + this.textValue = text; + this.textElement.innerText = text; + + if (!text) { + this.element.classList.add('hidden'); + } else { + this.element.classList.remove('hidden'); + } } - SimpleIndicator.prototype.text = function (text) { - if (text !== undefined && text !== this.textValue) { - this.textValue = text; - this.textElement.innerText = text; - - if (!text) { - this.element.classList.add('hidden'); - } else { - this.element.classList.remove('hidden'); - } - } - - return this.textValue; - }; - - SimpleIndicator.prototype.description = function (description) { - if (description !== undefined && description !== this.descriptionValue) { - this.descriptionValue = description; - this.element.title = description; - } - - return this.descriptionValue; - }; - - SimpleIndicator.prototype.iconClass = function (iconClass) { - if (iconClass !== undefined && iconClass !== this.iconClassValue) { - // element.classList is precious and throws errors if you try and add - // or remove empty strings - if (this.iconClassValue) { - this.element.classList.remove(this.iconClassValue); - } - - if (iconClass) { - this.element.classList.add(iconClass); - } - - this.iconClassValue = iconClass; - } - - return this.iconClassValue; - }; - - SimpleIndicator.prototype.statusClass = function (statusClass) { - if (statusClass !== undefined && statusClass !== this.statusClassValue) { - if (this.statusClassValue) { - this.element.classList.remove(this.statusClassValue); - } - - if (statusClass) { - this.element.classList.add(statusClass); - } - - this.statusClassValue = statusClass; - } - - return this.statusClassValue; - }; - - return SimpleIndicator; + return this.textValue; } -); + + description(description) { + if (description !== undefined && description !== this.descriptionValue) { + this.descriptionValue = description; + this.element.title = description; + } + + return this.descriptionValue; + } + + iconClass(iconClass) { + if (iconClass !== undefined && iconClass !== this.iconClassValue) { + // element.classList is precious and throws errors if you try and add + // or remove empty strings + if (this.iconClassValue) { + this.element.classList.remove(this.iconClassValue); + } + + if (iconClass) { + this.element.classList.add(iconClass); + } + + this.iconClassValue = iconClass; + } + + return this.iconClassValue; + } + + statusClass(statusClass) { + if (arguments.length === 1 && statusClass !== this.statusClassValue) { + if (this.statusClassValue) { + this.element.classList.remove(this.statusClassValue); + } + + if (statusClass !== undefined) { + this.element.classList.add(statusClass); + } + + this.statusClassValue = statusClass; + } + + return this.statusClassValue; + } + + click(event) { + this.emit('click', event); + } + + getElement() { + return this.element; + } +} + +function compileTemplate(htmlTemplate) { + const templateNode = document.createElement('template'); + templateNode.innerHTML = htmlTemplate; + + return templateNode.content.cloneNode(true).children; +} + +export default SimpleIndicator; diff --git a/src/api/telemetry/TelemetryMetadataManager.js b/src/api/telemetry/TelemetryMetadataManager.js index 1f55f5829d..0e21ad0797 100644 --- a/src/api/telemetry/TelemetryMetadataManager.js +++ b/src/api/telemetry/TelemetryMetadataManager.js @@ -138,7 +138,7 @@ define([ valueMetadata = this.values()[0]; } - return valueMetadata.key; + return valueMetadata; }; return TelemetryMetadataManager; diff --git a/src/api/user/StatusAPI.js b/src/api/user/StatusAPI.js new file mode 100644 index 0000000000..4e53d96143 --- /dev/null +++ b/src/api/user/StatusAPI.js @@ -0,0 +1,295 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2022, 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 EventEmitter from "EventEmitter"; + +export default class StatusAPI extends EventEmitter { + #userAPI; + #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.#openmct.once('destroy', () => { + const provider = this.#userAPI.getProvider(); + + 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} + */ + getPollQuestion() { + const provider = this.#userAPI.getProvider(); + + if (provider.getPollQuestion) { + return provider.getPollQuestion(); + } else { + this.#userAPI.error("User provider does not support polling questions"); + } + } + + /** + * 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} true if operation was successful, otherwise false. + */ + async setPollQuestion(questionText) { + const canSetPollQuestion = await this.canSetPollQuestion(); + + 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"); + } + } + + /** + * Can the currently logged in user set the operator status poll question. + * @returns {Promise} + */ + canSetPollQuestion() { + const provider = this.#userAPI.getProvider(); + + if (provider.canSetPollQuestion) { + return provider.canSetPollQuestion(); + } else { + return Promise.resolve(false); + } + } + + /** + * @returns {Promise>} 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.getPossibleStatuses) { + const possibleStatuses = await provider.getPossibleStatuses() || []; + + return possibleStatuses.map(status => status); + } else { + this.#userAPI.error("User provider cannot provide statuses"); + } + } + + /** + * @param {import("./UserAPI").Role} role The role to fetch the current status for. + * @returns {Promise} the current status of the provided role + */ + async getStatusForRole(role) { + const provider = this.#userAPI.getProvider(); + + 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 + * @returns {Promise} true if the configured UserProvider can provide status for the given role + * @see StatusUserProvider + */ + canProvideStatusForRole(role) { + const provider = this.#userAPI.getProvider(); + + if (provider.canProvideStatusForRole) { + return provider.canProvideStatusForRole(role); + } else { + return false; + } + } + + /** + * @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} true if operation was successful, otherwise false. + */ + setStatusForRole(role, status) { + 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"); + } + } + + /** + * 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} true if operation was successful, otherwise false. + */ + async resetStatusForRole(role) { + const provider = this.#userAPI.getProvider(); + const defaultStatus = await this.getDefaultStatus(); + + if (provider.setStatusForRole) { + return provider.setStatusForRole(role, defaultStatus); + } else { + this.#userAPI.error("User provider does not support resetting role status"); + } + } + + /** + * Resets the status of all operators to their default status + * @returns {Promise} true if operation was successful, otherwise false. + */ + async resetAllStatuses() { + const allStatusRoles = await this.getAllStatusRoles(); + + 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} 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>} 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"); + } + } + + /** + * The status role of the current user. A user may have multiple roles, but will only have one role + * that provides status at any time. + * @returns {Promise} the role for which the current user can provide status. + */ + getStatusRoleForCurrentUser() { + const provider = this.#userAPI.getProvider(); + + if (provider.getStatusRoleForCurrentUser) { + return provider.getStatusRoleForCurrentUser(); + } else { + this.#userAPI.error("User provider cannot provide role status for this user"); + } + } + + /** + * @returns {Promise} true if the configured UserProvider can provide status for the currently logged in user, false otherwise. + * @see StatusUserProvider + */ + async canProvideStatusForCurrentUser() { + const provider = this.#userAPI.getProvider(); + + if (provider.getStatusRoleForCurrentUser) { + const activeStatusRole = await this.#userAPI.getProvider().getStatusRoleForCurrentUser(); + const canProvideStatus = await this.canProvideStatusForRole(activeStatusRole); + + return canProvideStatus; + } else { + 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); + } +} + +/** + * @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 Status type + * @typedef {Object} Status + * @property {String} key - A unique identifier for this status + * @property {Number} label - A human readable label for this status + */ diff --git a/src/api/user/StatusUserProvider.js b/src/api/user/StatusUserProvider.js new file mode 100644 index 0000000000..b474fdbedb --- /dev/null +++ b/src/api/user/StatusUserProvider.js @@ -0,0 +1,81 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2022, 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 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} true if operation was successful, otherwise false + */ + async setPollQuestion(pollQuestion) {} + /** + * @returns {Promise} true if the current user can set the poll question, otherwise false + */ + async canSetPollQuestion() {} + /** + * @returns {Promise>} a list of the possible statuses that an operator can be in + */ + async getPossibleStatuses() {} + /** + * @param {import("./UserAPI").Role} role + * @returns {Promise} true if operation was successful, otherwise false. + */ + async setStatusForRole(role, status) {} + /** + * @param {import("./UserAPI").Role} role + * @returns {Promise>} a list of all available status roles, if user permissions allow it. + */ + async getAllStatusRoles() {} + /** + * @returns {Promise} the active status role for the currently logged in user + */ + async getStatusRoleForCurrentUser() {} +} diff --git a/src/api/user/UserAPI.js b/src/api/user/UserAPI.js index 1948021734..0ab2d91569 100644 --- a/src/api/user/UserAPI.js +++ b/src/api/user/UserAPI.js @@ -25,16 +25,22 @@ import { MULTIPLE_PROVIDER_ERROR, NO_PROVIDER_ERROR } from './constants'; +import StatusAPI from './StatusAPI'; import User from './User'; class UserAPI extends EventEmitter { - constructor(openmct) { + /** + * @param {OpenMCT} openmct + * @param {UserAPIConfiguration} config + */ + constructor(openmct, config) { super(); this._openmct = openmct; this._provider = undefined; this.User = User; + this.status = new StatusAPI(this, openmct, config); } /** @@ -47,14 +53,17 @@ class UserAPI extends EventEmitter { */ setProvider(provider) { if (this.hasProvider()) { - this._error(MULTIPLE_PROVIDER_ERROR); + this.error(MULTIPLE_PROVIDER_ERROR); } this._provider = provider; - this.emit('providerAdded', this._provider); } + getProvider() { + return this._provider; + } + /** * Return true if the user provider has been set. * @@ -74,7 +83,7 @@ class UserAPI extends EventEmitter { * @throws Will throw an error if no user provider is set */ getCurrentUser() { - this._noProviderCheck(); + this.noProviderCheck(); return this._provider.getCurrentUser(); } @@ -105,7 +114,7 @@ class UserAPI extends EventEmitter { * @throws Will throw an error if no user provider is set */ hasRole(roleId) { - this._noProviderCheck(); + this.noProviderCheck(); return this._provider.hasRole(roleId); } @@ -116,9 +125,9 @@ class UserAPI extends EventEmitter { * @private * @throws Will throw an error if no user provider is set */ - _noProviderCheck() { + noProviderCheck() { if (!this.hasProvider()) { - this._error(NO_PROVIDER_ERROR); + this.error(NO_PROVIDER_ERROR); } } @@ -129,9 +138,26 @@ class UserAPI extends EventEmitter { * @param {string} error description of error * @throws Will throw error passed in */ - _error(error) { + error(error) { throw new Error(error); } } export default UserAPI; +/** + * @typedef {String} Role + */ +/** + * @typedef {Object} OpenMCT + */ +/** + * @typedef {{statusStyles: Object.}} UserAPIConfiguration + */ +/** + * @typedef {Object} StatusStyleDefinition + * @property {String} iconClass The icon class to apply to the status indicator when this status is active "icon-circle-slash", + * @property {String} iconClassPoll The icon class to apply to the poll question indicator when this style is active eg. "icon-status-poll-question-mark" + * @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" + */ diff --git a/src/api/user/UserProvider.js b/src/api/user/UserProvider.js new file mode 100644 index 0000000000..8502dd54e9 --- /dev/null +++ b/src/api/user/UserProvider.js @@ -0,0 +1,36 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2022, 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. + *****************************************************************************/ +export default class UserProvider { + /** + * @returns {Promise} A promise that resolves with the currently logged in user + */ + getCurrentUser() {} + /** + * @returns {Boolean} true if a user is currently logged in, otherwise false + */ + isLoggedIn() {} + /** + * @param {String} role + * @returns {Promise} true if the current user has the given role + */ + hasRole(role) {} +} diff --git a/src/api/user/UserStatusAPISpec.js b/src/api/user/UserStatusAPISpec.js new file mode 100644 index 0000000000..30df2820ce --- /dev/null +++ b/src/api/user/UserStatusAPISpec.js @@ -0,0 +1,103 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2022, 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 { + createOpenMct, + resetApplicationState +} from '../../utils/testing'; + +describe("The User Status API", () => { + let openmct; + let userProvider; + let mockUser; + + beforeEach(() => { + userProvider = jasmine.createSpyObj("userProvider", [ + "setPollQuestion", + "getPollQuestion", + "getCurrentUser", + "getPossibleStatuses", + "getAllStatusRoles", + "canSetPollQuestion", + "isLoggedIn", + "on" + ]); + openmct = createOpenMct(); + mockUser = new openmct.user.User("test-user", "A test user"); + userProvider.getCurrentUser.and.returnValue(Promise.resolve(mockUser)); + userProvider.getPossibleStatuses.and.returnValue(Promise.resolve([])); + userProvider.getAllStatusRoles.and.returnValue(Promise.resolve([])); + userProvider.canSetPollQuestion.and.returnValue(Promise.resolve(false)); + userProvider.isLoggedIn.and.returnValue(true); + }); + + afterEach(() => { + return resetApplicationState(openmct); + }); + + describe("the poll question", () => { + it('can be set via a user status provider if supported', () => { + openmct.user.setProvider(userProvider); + userProvider.canSetPollQuestion.and.returnValue(Promise.resolve(true)); + + return openmct.user.status.setPollQuestion('This is a poll question').then(() => { + expect(userProvider.setPollQuestion).toHaveBeenCalledWith('This is a poll question'); + }); + }); + // fit('emits an event when the poll question changes', () => { + // const pollQuestionChangeCallback = jasmine.createSpy('pollQuestionChangeCallback'); + // let pollQuestionListener; + + // userProvider.canSetPollQuestion.and.returnValue(Promise.resolve(true)); + // userProvider.on.and.callFake((eventName, listener) => { + // if (eventName === 'pollQuestionChange') { + // pollQuestionListener = listener; + // } + // }); + + // openmct.user.on('pollQuestionChange', pollQuestionChangeCallback); + + // openmct.user.setProvider(userProvider); + + // return openmct.user.status.setPollQuestion('This is a poll question').then(() => { + // expect(pollQuestionListener).toBeDefined(); + // pollQuestionListener(); + // expect(pollQuestionChangeCallback).toHaveBeenCalled(); + + // const pollQuestion = pollQuestionChangeCallback.calls.mostRecent().args[0]; + // expect(pollQuestion.question).toBe('This is a poll question'); + + // openmct.user.off('pollQuestionChange', pollQuestionChangeCallback); + // }); + // }); + it('cannot be set if the user is not permitted', () => { + openmct.user.setProvider(userProvider); + userProvider.canSetPollQuestion.and.returnValue(Promise.resolve(false)); + + return openmct.user.status.setPollQuestion('This is a poll question').catch((error) => { + expect(error).toBeInstanceOf(Error); + }).finally(() => { + expect(userProvider.setPollQuestion).not.toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/src/plugins/displayLayout/components/TelemetryView.vue b/src/plugins/displayLayout/components/TelemetryView.vue index 3a759db9df..6886baa52d 100644 --- a/src/plugins/displayLayout/components/TelemetryView.vue +++ b/src/plugins/displayLayout/components/TelemetryView.vue @@ -91,7 +91,7 @@ export default { width: DEFAULT_TELEMETRY_DIMENSIONS[0], height: DEFAULT_TELEMETRY_DIMENSIONS[1], displayMode: 'all', - value: metadata.getDefaultDisplayValue(), + value: metadata.getDefaultDisplayValue()?.key, stroke: "", fill: "", color: "", diff --git a/src/plugins/objectMigration/Migrations.js b/src/plugins/objectMigration/Migrations.js index 46bd8f3831..493845e1fc 100644 --- a/src/plugins/objectMigration/Migrations.js +++ b/src/plugins/objectMigration/Migrations.js @@ -145,7 +145,7 @@ define([ item.size = element.size || DEFAULT_SIZE; item.identifier = telemetryObjects[element.id].identifier; item.displayMode = element.titled ? 'all' : 'value'; - item.value = openmct.telemetry.getMetadata(telemetryObjects[element.id]).getDefaultDisplayValue(); + item.value = openmct.telemetry.getMetadata(telemetryObjects[element.id]).getDefaultDisplayValue()?.key; } else if (element.type === 'fixed.box') { item.type = "box-view"; item.stroke = element.stroke || DEFAULT_STROKE; diff --git a/src/plugins/operatorStatus/AbstractStatusIndicator.js b/src/plugins/operatorStatus/AbstractStatusIndicator.js new file mode 100644 index 0000000000..7d2a012938 --- /dev/null +++ b/src/plugins/operatorStatus/AbstractStatusIndicator.js @@ -0,0 +1,106 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2022, 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 raf from '@/utils/raf'; + +export default class AbstractStatusIndicator { + #popupComponent; + #indicator; + #configuration; + + /** + * @param {*} openmct the Open MCT API (proper jsdoc to come) + * @param {import('@/api/user/UserAPI').UserAPIConfiguration} configuration Per-deployment status styling. See the type definition in UserAPI + */ + constructor(openmct, configuration) { + this.openmct = openmct; + this.#configuration = configuration; + + this.showPopup = this.showPopup.bind(this); + this.clearPopup = this.clearPopup.bind(this); + this.positionBox = this.positionBox.bind(this); + this.positionBox = raf(this.positionBox); + + this.#indicator = this.createIndicator(); + this.#popupComponent = this.createPopupComponent(); + } + + install() { + this.openmct.indicators.add(this.#indicator); + } + + showPopup() { + const popupElement = this.getPopupElement(); + + document.body.appendChild(popupElement.$el); + //Use capture so we don't trigger immediately on the same iteration of the event loop + document.addEventListener('click', this.clearPopup, { + capture: true + }); + + this.positionBox(); + + window.addEventListener('resize', this.positionBox); + } + + positionBox() { + const popupElement = this.getPopupElement(); + const indicator = this.getIndicator(); + + let indicatorBox = indicator.element.getBoundingClientRect(); + popupElement.positionX = indicatorBox.left; + popupElement.positionY = indicatorBox.bottom; + + const popupRight = popupElement.positionX + popupElement.$el.clientWidth; + const offsetLeft = Math.min(window.innerWidth - popupRight, 0); + popupElement.positionX = popupElement.positionX + offsetLeft; + } + + clearPopup(clickAwayEvent) { + const popupElement = this.getPopupElement(); + + if (!popupElement.$el.contains(clickAwayEvent.target)) { + popupElement.$el.remove(); + document.removeEventListener('click', this.clearPopup); + window.removeEventListener('resize', this.positionBox); + } + } + + createPopupComponent() { + throw new Error('Must override createPopupElement method'); + } + + getPopupElement() { + return this.#popupComponent; + } + + createIndicator() { + throw new Error('Must override createIndicator method'); + } + + getIndicator() { + return this.#indicator; + } + + getConfiguration() { + return this.#configuration; + } +} diff --git a/src/plugins/operatorStatus/operator-status.scss b/src/plugins/operatorStatus/operator-status.scss new file mode 100644 index 0000000000..144ffff402 --- /dev/null +++ b/src/plugins/operatorStatus/operator-status.scss @@ -0,0 +1,142 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2022, 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. + *****************************************************************************/ + + $statusCountWidth: 30px; + +.c-status-poll-panel { + @include menuOuter(); + display: flex; + flex-direction: column; + padding: $interiorMarginLg; + min-width: 350px; + max-width: 35%; + + > * + * { + margin-top: $interiorMarginLg; + } + + *:before { + font-size: 0.8em; + margin-right: $interiorMarginSm; + } + + &__section { + display: flex; + align-items: center; + flex-direction: row; + + > * + * { + margin-left: $interiorMarginLg; + } + } + + &__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-status-poll-panel--admin & { + background: rgba($colorBodyFg, 0.1); + color: $colorBodyFg; + } + } + + /****** Admin interface */ + &__content { + $m: $interiorMargin; + display: grid; + grid-template-columns: min-content 1fr; + grid-column-gap: $m; + grid-row-gap: $m; + + [class*='__label'] { + padding: 3px 0; + } + + [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; } + } + } +} + +.c-status-poll-report { + display: flex; + flex-direction: row; + > * + * { margin-left: $interiorMargin; } + + &__count { + background: rgba($colorBodyFg, 0.2); + border-radius: $controlCr; + display: flex; + flex-direction: row; + font-size: 1.25em; + align-items: center; + padding: $interiorMarginSm $interiorMarginLg; + + &-type { + line-height: 1em; + opacity: 0.6; + } + } +} + +.c-indicator { + &:before { + // Indicator icon + color: $colorKey; + } + + &--operator-status { + cursor: pointer; + max-width: 150px; + + @include hover() { + background: $colorIndicatorBgHov; + } + } +} diff --git a/src/plugins/operatorStatus/operatorStatus/OperatorStatus.vue b/src/plugins/operatorStatus/operatorStatus/OperatorStatus.vue new file mode 100644 index 0000000000..e51f3d08d9 --- /dev/null +++ b/src/plugins/operatorStatus/operatorStatus/OperatorStatus.vue @@ -0,0 +1,187 @@ + + + + diff --git a/src/plugins/operatorStatus/operatorStatus/OperatorStatusIndicator.js b/src/plugins/operatorStatus/operatorStatus/OperatorStatusIndicator.js new file mode 100644 index 0000000000..9eb96e938c --- /dev/null +++ b/src/plugins/operatorStatus/operatorStatus/OperatorStatusIndicator.js @@ -0,0 +1,63 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2022, 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 Vue from 'vue'; + +import AbstractStatusIndicator from '../AbstractStatusIndicator'; +import OperatorStatusComponent from './OperatorStatus.vue'; + +export default class OperatorStatusIndicator extends AbstractStatusIndicator { + createPopupComponent() { + const indicator = this.getIndicator(); + const popupElement = new Vue({ + components: { + OperatorStatus: OperatorStatusComponent + }, + provide: { + openmct: this.openmct, + indicator: indicator, + configuration: this.getConfiguration() + }, + data() { + return { + positionX: 0, + positionY: 0 + }; + }, + template: '' + }).$mount(); + + return popupElement; + } + + createIndicator() { + const operatorIndicator = this.openmct.indicators.simpleIndicator(); + + operatorIndicator.text("My Operator Status"); + operatorIndicator.description("Set my operator status"); + operatorIndicator.iconClass('icon-status-poll-question-mark'); + operatorIndicator.element.classList.add("c-indicator--operator-status"); + operatorIndicator.element.classList.add("no-minify"); + operatorIndicator.on('click', this.showPopup); + + return operatorIndicator; + } +} diff --git a/src/plugins/operatorStatus/plugin.js b/src/plugins/operatorStatus/plugin.js new file mode 100644 index 0000000000..3d449d1ebd --- /dev/null +++ b/src/plugins/operatorStatus/plugin.js @@ -0,0 +1,50 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2022, 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 OperatorStatusIndicator from './operatorStatus/OperatorStatusIndicator'; +import PollQuestionIndicator from './pollQuestion/PollQuestionIndicator'; + +/** + * @param {import('@/api/user/UserAPI').UserAPIConfiguration} configuration + * @returns {function} The plugin install function + */ +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(); + } + }); + + openmct.user.status.canSetPollQuestion().then(canSetPollQuestion => { + if (canSetPollQuestion) { + const pollQuestionIndicator = new PollQuestionIndicator(openmct, configuration); + + pollQuestionIndicator.install(); + } + }); + } + }; +} diff --git a/src/plugins/operatorStatus/pollQuestion/PollQuestion.vue b/src/plugins/operatorStatus/pollQuestion/PollQuestion.vue new file mode 100644 index 0000000000..f279e57975 --- /dev/null +++ b/src/plugins/operatorStatus/pollQuestion/PollQuestion.vue @@ -0,0 +1,184 @@ + + + + diff --git a/src/plugins/operatorStatus/pollQuestion/PollQuestionIndicator.js b/src/plugins/operatorStatus/pollQuestion/PollQuestionIndicator.js new file mode 100644 index 0000000000..ea85d5905d --- /dev/null +++ b/src/plugins/operatorStatus/pollQuestion/PollQuestionIndicator.js @@ -0,0 +1,63 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2022, 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 Vue from 'vue'; + +import AbstractStatusIndicator from '../AbstractStatusIndicator'; +import PollQuestionComponent from './PollQuestion.vue'; + +export default class PollQuestionIndicator extends AbstractStatusIndicator { + createPopupComponent() { + const indicator = this.getIndicator(); + const pollQuestionElement = new Vue({ + components: { + PollQuestion: PollQuestionComponent + }, + provide: { + openmct: this.openmct, + indicator: indicator, + configuration: this.getConfiguration() + }, + data() { + return { + positionX: 0, + positionY: 0 + }; + }, + template: '' + }).$mount(); + + return pollQuestionElement; + } + + createIndicator() { + const pollQuestionIndicator = this.openmct.indicators.simpleIndicator(); + + pollQuestionIndicator.text("Poll Question"); + pollQuestionIndicator.description("Set the current poll question"); + pollQuestionIndicator.iconClass('icon-status-poll-edit'); + pollQuestionIndicator.element.classList.add("c-indicator--operator-status"); + pollQuestionIndicator.element.classList.add("no-minify"); + pollQuestionIndicator.on('click', this.showPopup); + + return pollQuestionIndicator; + } +} diff --git a/src/plugins/plugins.js b/src/plugins/plugins.js index 7beccebd54..e53ac68433 100644 --- a/src/plugins/plugins.js +++ b/src/plugins/plugins.js @@ -78,6 +78,7 @@ define([ './userIndicator/plugin', '../../example/exampleUser/plugin', './localStorage/plugin', + './operatorStatus/plugin', './gauge/GaugePlugin', './timelist/plugin' ], function ( @@ -138,6 +139,7 @@ define([ UserIndicator, ExampleUser, LocalStorage, + OperatorStatus, GaugePlugin, TimeList ) { @@ -217,6 +219,7 @@ define([ plugins.DeviceClassifier = DeviceClassifier.default; plugins.UserIndicator = UserIndicator.default; plugins.LocalStorage = LocalStorage.default; + plugins.OperatorStatus = OperatorStatus.default; plugins.Gauge = GaugePlugin.default; plugins.Timelist = TimeList.default; diff --git a/src/styles/_constants.scss b/src/styles/_constants.scss index 8d2d34179e..8b4994756e 100755 --- a/src/styles/_constants.scss +++ b/src/styles/_constants.scss @@ -156,6 +156,13 @@ $glyph-icon-notebook-page: '\e92c'; $glyph-icon-unlocked: '\e92d'; $glyph-icon-circle: '\e92e'; $glyph-icon-draft: '\e92f'; +$glyph-icon-circle-slash: '\e930'; +$glyph-icon-question-mark: '\e931'; +$glyph-icon-status-poll-check: '\e932'; +$glyph-icon-status-poll-caution: '\e933'; +$glyph-icon-status-poll-circle-slash: '\e934'; +$glyph-icon-status-poll-question-mark: '\e935'; +$glyph-icon-status-poll-edit: '\e936'; $glyph-icon-arrows-right-left: '\ea00'; $glyph-icon-arrows-up-down: '\ea01'; $glyph-icon-bullet: '\ea02'; @@ -264,6 +271,7 @@ $glyph-icon-bar-chart: '\eb2c'; $glyph-icon-map: '\eb2d'; $glyph-icon-plan: '\eb2e'; $glyph-icon-timelist: '\eb2f'; +$glyph-icon-notebook-shift-log: '\eb31'; $glyph-icon-plot-scatter: '\eb30'; /************************** GLYPHS AS DATA URI */ @@ -317,4 +325,5 @@ $bg-icon-bar-chart: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://w $bg-icon-map: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M448 32.7 384 64v448l64-31.3c35.2-17.21 64-60.1 64-95.3v-320c0-35.2-28.8-49.91-64-32.7ZM160 456l193.6 48.4v-448L160 8v448zM129.6.4 128 0 64 31.3C28.8 48.51 0 91.4 0 126.6v320c0 35.2 28.8 49.91 64 32.7l64-31.3 1.6.4Z'/%3e%3c/svg%3e"); $bg-icon-plan: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cg data-name='Layer 2'%3e%3cg data-name='Layer 1'%3e%3cpath fill='%23000000' d='M128 96V64a64.19 64.19 0 0 1 64-64h128a64.19 64.19 0 0 1 64 64v32Z'/%3e%3cpath fill='%23000000' d='M416 64v64H96V64c-52.8 0-96 43.2-96 96v256c0 52.8 43.2 96 96 96h320c52.8 0 96-43.2 96-96V160c0-52.8-43.2-96-96-96ZM64 288v-64h128v64Zm256 128H128v-64h192Zm128 0h-64v-64h64Zm0-128H256v-64h192Z'/%3e%3c/g%3e%3c/g%3e%3c/svg%3e"); $bg-icon-timelist: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cg data-name='Layer 2'%3e%3cpath d='M448 0H64A64.19 64.19 0 0 0 0 64v384a64.19 64.19 0 0 0 64 64h384a64.19 64.19 0 0 0 64-64V64a64.19 64.19 0 0 0-64-64ZM213.47 266.73a24 24 0 0 1-32.2 10.74L104 238.83V128a24 24 0 0 1 48 0v81.17l50.73 25.36a24 24 0 0 1 10.74 32.2ZM448 448H288v-64h160Zm0-96H288v-64h160Zm0-96H288v-64h160Zm0-96H288V96h160Z' data-name='Layer 1'/%3e%3c/g%3e%3c/svg%3e"); +$bg-icon-notebook-shift-log: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath d='M448 55.36c0-39.95-27.69-63.66-61.54-52.68L0 128h448V55.36ZM448 160H0v288c0 35.2 28.8 64 64 64h384c35.2 0 64-28.8 64-64V224c0-35.2-28.8-64-64-64ZM128 416H64v-64h64v64Zm0-96H64v-64h64v64Zm320 96H192v-64h256v64Zm0-96H192v-64h256v64Z'/%3e%3c/svg%3e"); $bg-icon-plot-scatter: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cg data-name='Layer 2'%3e%3cpath d='M96 0C43.2 0 0 43.2 0 96v320c0 52.8 43.2 96 96 96h320c52.8 0 96-43.2 96-96V96c0-52.8-43.2-96-96-96ZM64 176a48 48 0 1 1 48 48 48 48 0 0 1-48-48Zm80 240a48 48 0 1 1 48-48 48 48 0 0 1-48 48Zm128-96a48 48 0 1 1 48-48 48 48 0 0 1-48 48Zm0-160a48 48 0 1 1 48-48 48 48 0 0 1-48 48Zm128 256a48 48 0 1 1 48-48 48 48 0 0 1-48 48Z' data-name='Layer 1'/%3e%3c/g%3e%3c/svg%3e"); diff --git a/src/styles/_glyphs.scss b/src/styles/_glyphs.scss index ac82d60060..80b43eb3de 100755 --- a/src/styles/_glyphs.scss +++ b/src/styles/_glyphs.scss @@ -87,6 +87,13 @@ .icon-unlocked { @include glyphBefore($glyph-icon-unlocked); } .icon-circle { @include glyphBefore($glyph-icon-circle); } .icon-draft { @include glyphBefore($glyph-icon-draft); } +.icon-question-mark { @include glyphBefore($glyph-icon-question-mark); } +.icon-circle-slash { @include glyphBefore($glyph-icon-circle-slash); } +.icon-status-poll-check { @include glyphBefore($glyph-icon-status-poll-check); } +.icon-status-poll-caution { @include glyphBefore($glyph-icon-status-poll-caution); } +.icon-status-poll-circle-slash { @include glyphBefore($glyph-icon-status-poll-circle-slash); } +.icon-status-poll-question-mark { @include glyphBefore($glyph-icon-status-poll-question-mark); } +.icon-status-poll-edit { @include glyphBefore($glyph-icon-status-poll-edit); } .icon-arrows-right-left { @include glyphBefore($glyph-icon-arrows-right-left); } .icon-arrows-up-down { @include glyphBefore($glyph-icon-arrows-up-down); } .icon-bullet { @include glyphBefore($glyph-icon-bullet); } @@ -195,6 +202,7 @@ .icon-map { @include glyphBefore($glyph-icon-map); } .icon-plan { @include glyphBefore($glyph-icon-plan); } .icon-timelist { @include glyphBefore($glyph-icon-timelist); } +.icon-notebook-shift-log { @include glyphBefore($glyph-icon-notebook-shift-log); } .icon-plot-scatter { @include glyphBefore($glyph-icon-plot-scatter); } /************************** 12 PX CLASSES */ @@ -256,4 +264,5 @@ .bg-icon-map { @include glyphBg($bg-icon-map); } .bg-icon-plan { @include glyphBg($bg-icon-plan); } .bg-icon-timelist { @include glyphBg($bg-icon-timelist); } +.bg-icon-notebook-shift-log { @include glyphBg($bg-icon-notebook-shift-log); } .bg-icon-plot-scatter { @include glyphBg($bg-icon-plot-scatter); } diff --git a/src/styles/fonts/Open MCT Symbols 16px.json b/src/styles/fonts/Open MCT Symbols 16px.json index 7e5d8a02a9..11cc387378 100644 --- a/src/styles/fonts/Open MCT Symbols 16px.json +++ b/src/styles/fonts/Open MCT Symbols 16px.json @@ -2,7 +2,7 @@ "metadata": { "name": "Open MCT Symbols 16px", "lastOpened": 0, - "created": 1650916650636 + "created": 1651949568729 }, "iconSets": [ { @@ -391,13 +391,69 @@ "code": 59695, "tempChar": "" }, + { + "order": 212, + "id": 183, + "name": "icon-circle-slash", + "prevSize": 16, + "code": 59696, + "tempChar": "" + }, + { + "order": 213, + "id": 182, + "name": "icon-question-mark", + "prevSize": 16, + "code": 59697, + "tempChar": "" + }, + { + "order": 206, + "id": 179, + "name": "icon-status-poll-check", + "prevSize": 16, + "code": 59698, + "tempChar": "" + }, + { + "order": 207, + "id": 178, + "name": "icon-status-poll-caution", + "prevSize": 16, + "code": 59699, + "tempChar": "" + }, + { + "order": 210, + "id": 180, + "name": "icon-status-poll-circle-slash", + "prevSize": 16, + "code": 59700, + "tempChar": "" + }, + { + "order": 211, + "id": 181, + "name": "icon-status-poll-question-mark", + "prevSize": 16, + "code": 59701, + "tempChar": "" + }, + { + "order": 209, + "id": 176, + "name": "icon-status-poll-edit", + "prevSize": 16, + "code": 59702, + "tempChar": "" + }, { "order": 27, "id": 105, "name": "icon-arrows-right-left", "prevSize": 16, "code": 59904, - "tempChar": "" + "tempChar": "" }, { "order": 26, @@ -405,7 +461,7 @@ "name": "icon-arrows-up-down", "prevSize": 16, "code": 59905, - "tempChar": "" + "tempChar": "" }, { "order": 68, @@ -413,7 +469,7 @@ "name": "icon-bullet", "prevSize": 16, "code": 59906, - "tempChar": "" + "tempChar": "" }, { "order": 150, @@ -421,7 +477,7 @@ "prevSize": 16, "code": 59907, "name": "icon-calendar", - "tempChar": "" + "tempChar": "" }, { "order": 45, @@ -429,7 +485,7 @@ "name": "icon-chain-links", "prevSize": 16, "code": 59908, - "tempChar": "" + "tempChar": "" }, { "order": 73, @@ -437,7 +493,7 @@ "name": "icon-download", "prevSize": 16, "code": 59909, - "tempChar": "" + "tempChar": "" }, { "order": 39, @@ -445,7 +501,7 @@ "name": "icon-duplicate", "prevSize": 16, "code": 59910, - "tempChar": "" + "tempChar": "" }, { "order": 50, @@ -453,7 +509,7 @@ "name": "icon-folder-new", "prevSize": 16, "code": 59911, - "tempChar": "" + "tempChar": "" }, { "order": 138, @@ -461,7 +517,7 @@ "name": "icon-fullscreen-collapse", "prevSize": 16, "code": 59912, - "tempChar": "" + "tempChar": "" }, { "order": 139, @@ -469,7 +525,7 @@ "name": "icon-fullscreen-expand", "prevSize": 16, "code": 59913, - "tempChar": "" + "tempChar": "" }, { "order": 122, @@ -477,7 +533,7 @@ "name": "icon-layers", "prevSize": 16, "code": 59914, - "tempChar": "" + "tempChar": "" }, { "order": 151, @@ -485,7 +541,7 @@ "name": "icon-line-horz", "prevSize": 16, "code": 59915, - "tempChar": "" + "tempChar": "" }, { "order": 100, @@ -493,7 +549,7 @@ "name": "icon-magnify", "prevSize": 16, "code": 59916, - "tempChar": "" + "tempChar": "" }, { "order": 99, @@ -501,7 +557,7 @@ "name": "icon-magnify-in", "prevSize": 16, "code": 59917, - "tempChar": "" + "tempChar": "" }, { "order": 101, @@ -509,7 +565,7 @@ "name": "icon-magnify-out-v2", "prevSize": 16, "code": 59918, - "tempChar": "" + "tempChar": "" }, { "order": 103, @@ -517,7 +573,7 @@ "name": "icon-menu", "prevSize": 16, "code": 59919, - "tempChar": "" + "tempChar": "" }, { "order": 124, @@ -525,7 +581,7 @@ "name": "icon-move", "prevSize": 16, "code": 59920, - "tempChar": "" + "tempChar": "" }, { "order": 7, @@ -533,7 +589,7 @@ "name": "icon-new-window", "prevSize": 16, "code": 59921, - "tempChar": "" + "tempChar": "" }, { "order": 63, @@ -541,7 +597,7 @@ "name": "icon-paint-bucket-v2", "prevSize": 16, "code": 59922, - "tempChar": "" + "tempChar": "" }, { "order": 15, @@ -549,7 +605,7 @@ "name": "icon-pencil", "prevSize": 16, "code": 59923, - "tempChar": "" + "tempChar": "" }, { "order": 54, @@ -557,7 +613,7 @@ "name": "icon-pencil-edit-in-place", "prevSize": 16, "code": 59924, - "tempChar": "" + "tempChar": "" }, { "order": 40, @@ -565,7 +621,7 @@ "name": "icon-play", "prevSize": 16, "code": 59925, - "tempChar": "" + "tempChar": "" }, { "order": 125, @@ -573,7 +629,7 @@ "name": "icon-pause", "prevSize": 16, "code": 59926, - "tempChar": "" + "tempChar": "" }, { "order": 119, @@ -581,7 +637,7 @@ "name": "icon-plot-resource", "prevSize": 16, "code": 59927, - "tempChar": "" + "tempChar": "" }, { "order": 48, @@ -589,7 +645,7 @@ "name": "icon-pointer-left", "prevSize": 16, "code": 59928, - "tempChar": "" + "tempChar": "" }, { "order": 47, @@ -597,7 +653,7 @@ "name": "icon-pointer-right", "prevSize": 16, "code": 59929, - "tempChar": "" + "tempChar": "" }, { "order": 85, @@ -605,7 +661,7 @@ "name": "icon-refresh", "prevSize": 16, "code": 59930, - "tempChar": "" + "tempChar": "" }, { "order": 55, @@ -613,7 +669,7 @@ "name": "icon-save", "prevSize": 16, "code": 59931, - "tempChar": "" + "tempChar": "" }, { "order": 56, @@ -621,7 +677,7 @@ "name": "icon-save-as", "prevSize": 16, "code": 59932, - "tempChar": "" + "tempChar": "" }, { "order": 58, @@ -629,7 +685,7 @@ "name": "icon-sine", "prevSize": 16, "code": 59933, - "tempChar": "" + "tempChar": "" }, { "order": 113, @@ -637,7 +693,7 @@ "name": "icon-font", "prevSize": 16, "code": 59934, - "tempChar": "" + "tempChar": "" }, { "order": 41, @@ -645,7 +701,7 @@ "name": "icon-thumbs-strip", "prevSize": 16, "code": 59935, - "tempChar": "" + "tempChar": "" }, { "order": 146, @@ -653,7 +709,7 @@ "name": "icon-two-parts-both", "prevSize": 16, "code": 59936, - "tempChar": "" + "tempChar": "" }, { "order": 145, @@ -661,7 +717,7 @@ "name": "icon-two-parts-one-only", "prevSize": 16, "code": 59937, - "tempChar": "" + "tempChar": "" }, { "order": 82, @@ -669,7 +725,7 @@ "name": "icon-resync", "prevSize": 16, "code": 59938, - "tempChar": "" + "tempChar": "" }, { "order": 86, @@ -677,7 +733,7 @@ "name": "icon-reset", "prevSize": 16, "code": 59939, - "tempChar": "" + "tempChar": "" }, { "order": 61, @@ -685,7 +741,7 @@ "name": "icon-x-in-circle", "prevSize": 16, "code": 59940, - "tempChar": "" + "tempChar": "" }, { "order": 84, @@ -693,7 +749,7 @@ "name": "icon-brightness", "prevSize": 16, "code": 59941, - "tempChar": "" + "tempChar": "" }, { "order": 83, @@ -701,7 +757,7 @@ "name": "icon-contrast", "prevSize": 16, "code": 59942, - "tempChar": "" + "tempChar": "" }, { "order": 87, @@ -709,7 +765,7 @@ "name": "icon-expand", "prevSize": 16, "code": 59943, - "tempChar": "" + "tempChar": "" }, { "order": 89, @@ -717,7 +773,7 @@ "name": "icon-list-view", "prevSize": 16, "code": 59944, - "tempChar": "" + "tempChar": "" }, { "order": 133, @@ -725,7 +781,7 @@ "name": "icon-grid-snap-to", "prevSize": 16, "code": 59945, - "tempChar": "" + "tempChar": "" }, { "order": 132, @@ -733,7 +789,7 @@ "name": "icon-grid-snap-no", "prevSize": 16, "code": 59946, - "tempChar": "" + "tempChar": "" }, { "order": 94, @@ -741,7 +797,7 @@ "name": "icon-frame-show", "prevSize": 16, "code": 59947, - "tempChar": "" + "tempChar": "" }, { "order": 95, @@ -749,7 +805,7 @@ "name": "icon-frame-hide", "prevSize": 16, "code": 59948, - "tempChar": "" + "tempChar": "" }, { "order": 97, @@ -757,7 +813,7 @@ "name": "icon-import", "prevSize": 16, "code": 59949, - "tempChar": "" + "tempChar": "" }, { "order": 96, @@ -765,7 +821,7 @@ "name": "icon-export", "prevSize": 16, "code": 59950, - "tempChar": "" + "tempChar": "" }, { "order": 194, @@ -773,7 +829,7 @@ "name": "icon-font-size", "prevSize": 16, "code": 59951, - "tempChar": "" + "tempChar": "" }, { "order": 163, @@ -781,7 +837,7 @@ "name": "icon-clear-data", "prevSize": 16, "code": 59952, - "tempChar": "" + "tempChar": "" }, { "order": 173, @@ -789,7 +845,7 @@ "name": "icon-history", "prevSize": 16, "code": 59953, - "tempChar": "" + "tempChar": "" }, { "order": 181, @@ -797,7 +853,7 @@ "name": "icon-arrow-up-to-parent", "prevSize": 16, "code": 59954, - "tempChar": "" + "tempChar": "" }, { "order": 184, @@ -805,7 +861,7 @@ "name": "icon-crosshair-in-circle", "prevSize": 16, "code": 59955, - "tempChar": "" + "tempChar": "" }, { "order": 185, @@ -813,7 +869,7 @@ "name": "icon-target", "prevSize": 16, "code": 59956, - "tempChar": "" + "tempChar": "" }, { "order": 187, @@ -821,7 +877,7 @@ "name": "icon-items-collapse", "prevSize": 16, "code": 59957, - "tempChar": "" + "tempChar": "" }, { "order": 188, @@ -829,7 +885,7 @@ "name": "icon-items-expand", "prevSize": 16, "code": 59958, - "tempChar": "" + "tempChar": "" }, { "order": 190, @@ -837,7 +893,7 @@ "name": "icon-3-dots", "prevSize": 16, "code": 59959, - "tempChar": "" + "tempChar": "" }, { "order": 193, @@ -845,7 +901,7 @@ "name": "icon-grid-on", "prevSize": 16, "code": 59960, - "tempChar": "" + "tempChar": "" }, { "order": 192, @@ -853,7 +909,7 @@ "name": "icon-grid-off", "prevSize": 16, "code": 59961, - "tempChar": "" + "tempChar": "" }, { "order": 191, @@ -861,7 +917,7 @@ "name": "icon-camera", "prevSize": 16, "code": 59962, - "tempChar": "" + "tempChar": "" }, { "order": 196, @@ -869,7 +925,7 @@ "name": "icon-folders-collapse", "prevSize": 16, "code": 59963, - "tempChar": "" + "tempChar": "" }, { "order": 144, @@ -877,7 +933,7 @@ "name": "icon-activity", "prevSize": 16, "code": 60160, - "tempChar": "" + "tempChar": "" }, { "order": 104, @@ -885,7 +941,7 @@ "name": "icon-activity-mode", "prevSize": 16, "code": 60161, - "tempChar": "" + "tempChar": "" }, { "order": 137, @@ -893,7 +949,7 @@ "name": "icon-autoflow-tabular", "prevSize": 16, "code": 60162, - "tempChar": "" + "tempChar": "" }, { "order": 115, @@ -901,7 +957,7 @@ "name": "icon-clock", "prevSize": 16, "code": 60163, - "tempChar": "" + "tempChar": "" }, { "order": 2, @@ -909,7 +965,7 @@ "name": "icon-database", "prevSize": 16, "code": 60164, - "tempChar": "" + "tempChar": "" }, { "order": 3, @@ -917,7 +973,7 @@ "name": "icon-database-query", "prevSize": 16, "code": 60165, - "tempChar": "" + "tempChar": "" }, { "order": 67, @@ -925,7 +981,7 @@ "name": "icon-dataset", "prevSize": 16, "code": 60166, - "tempChar": "" + "tempChar": "" }, { "order": 59, @@ -933,7 +989,7 @@ "name": "icon-datatable", "prevSize": 16, "code": 60167, - "tempChar": "" + "tempChar": "" }, { "order": 136, @@ -941,7 +997,7 @@ "name": "icon-dictionary", "prevSize": 16, "code": 60168, - "tempChar": "" + "tempChar": "" }, { "order": 51, @@ -949,7 +1005,7 @@ "name": "icon-folder", "prevSize": 16, "code": 60169, - "tempChar": "" + "tempChar": "" }, { "order": 147, @@ -957,7 +1013,7 @@ "name": "icon-image", "prevSize": 16, "code": 60170, - "tempChar": "" + "tempChar": "" }, { "order": 4, @@ -965,7 +1021,7 @@ "name": "icon-layout", "prevSize": 16, "code": 60171, - "tempChar": "" + "tempChar": "" }, { "order": 24, @@ -973,7 +1029,7 @@ "name": "icon-object", "prevSize": 16, "code": 60172, - "tempChar": "" + "tempChar": "" }, { "order": 52, @@ -981,7 +1037,7 @@ "name": "icon-object-unknown", "prevSize": 16, "code": 60173, - "tempChar": "" + "tempChar": "" }, { "order": 105, @@ -989,7 +1045,7 @@ "name": "icon-packet", "prevSize": 16, "code": 60174, - "tempChar": "" + "tempChar": "" }, { "order": 126, @@ -997,7 +1053,7 @@ "name": "icon-page", "prevSize": 16, "code": 60175, - "tempChar": "" + "tempChar": "" }, { "order": 130, @@ -1005,7 +1061,7 @@ "name": "icon-plot-overlay", "prevSize": 16, "code": 60176, - "tempChar": "" + "tempChar": "" }, { "order": 80, @@ -1013,7 +1069,7 @@ "name": "icon-plot-stacked", "prevSize": 16, "code": 60177, - "tempChar": "" + "tempChar": "" }, { "order": 134, @@ -1021,7 +1077,7 @@ "name": "icon-session", "prevSize": 16, "code": 60178, - "tempChar": "" + "tempChar": "" }, { "order": 109, @@ -1029,7 +1085,7 @@ "name": "icon-tabular", "prevSize": 16, "code": 60179, - "tempChar": "" + "tempChar": "" }, { "order": 107, @@ -1037,7 +1093,7 @@ "name": "icon-tabular-lad", "prevSize": 16, "code": 60180, - "tempChar": "" + "tempChar": "" }, { "order": 106, @@ -1045,7 +1101,7 @@ "name": "icon-tabular-lad-set", "prevSize": 16, "code": 60181, - "tempChar": "" + "tempChar": "" }, { "order": 70, @@ -1053,7 +1109,7 @@ "name": "icon-tabular-realtime", "prevSize": 16, "code": 60182, - "tempChar": "" + "tempChar": "" }, { "order": 60, @@ -1061,7 +1117,7 @@ "name": "icon-tabular-scrolling", "prevSize": 16, "code": 60183, - "tempChar": "" + "tempChar": "" }, { "order": 131, @@ -1069,7 +1125,7 @@ "name": "icon-telemetry", "prevSize": 16, "code": 60184, - "tempChar": "" + "tempChar": "" }, { "order": 202, @@ -1077,7 +1133,7 @@ "name": "icon-timeline", "prevSize": 16, "code": 60185, - "tempChar": "" + "tempChar": "" }, { "order": 81, @@ -1085,7 +1141,7 @@ "name": "icon-timer", "prevSize": 16, "code": 60186, - "tempChar": "" + "tempChar": "" }, { "order": 69, @@ -1093,7 +1149,7 @@ "name": "icon-topic", "prevSize": 16, "code": 60187, - "tempChar": "" + "tempChar": "" }, { "order": 79, @@ -1101,7 +1157,7 @@ "name": "icon-box-with-dashed-lines-v2", "prevSize": 16, "code": 60188, - "tempChar": "" + "tempChar": "" }, { "order": 90, @@ -1109,7 +1165,7 @@ "name": "icon-summary-widget", "prevSize": 16, "code": 60189, - "tempChar": "" + "tempChar": "" }, { "order": 92, @@ -1117,7 +1173,7 @@ "name": "icon-notebook", "prevSize": 16, "code": 60190, - "tempChar": "" + "tempChar": "" }, { "order": 168, @@ -1125,7 +1181,7 @@ "name": "icon-tabs-view", "prevSize": 16, "code": 60191, - "tempChar": "" + "tempChar": "" }, { "order": 117, @@ -1133,7 +1189,7 @@ "name": "icon-flexible-layout", "prevSize": 16, "code": 60192, - "tempChar": "" + "tempChar": "" }, { "order": 166, @@ -1141,7 +1197,7 @@ "name": "icon-generator-sine", "prevSize": 16, "code": 60193, - "tempChar": "" + "tempChar": "" }, { "order": 167, @@ -1149,7 +1205,7 @@ "name": "icon-generator-event", "prevSize": 16, "code": 60194, - "tempChar": "" + "tempChar": "" }, { "order": 165, @@ -1157,7 +1213,7 @@ "name": "icon-gauge-v2", "prevSize": 16, "code": 60195, - "tempChar": "" + "tempChar": "" }, { "order": 170, @@ -1165,7 +1221,7 @@ "name": "icon-spectra", "prevSize": 16, "code": 60196, - "tempChar": "" + "tempChar": "" }, { "order": 171, @@ -1173,7 +1229,7 @@ "name": "icon-telemetry-spectra", "prevSize": 16, "code": 60197, - "tempChar": "" + "tempChar": "" }, { "order": 172, @@ -1181,7 +1237,7 @@ "name": "icon-pushbutton", "prevSize": 16, "code": 60198, - "tempChar": "" + "tempChar": "" }, { "order": 174, @@ -1189,7 +1245,7 @@ "name": "icon-conditional", "prevSize": 16, "code": 60199, - "tempChar": "" + "tempChar": "" }, { "order": 178, @@ -1197,7 +1253,7 @@ "name": "icon-condition-widget", "prevSize": 16, "code": 60200, - "tempChar": "" + "tempChar": "" }, { "order": 180, @@ -1205,7 +1261,7 @@ "name": "icon-alphanumeric", "prevSize": 16, "code": 60201, - "tempChar": "" + "tempChar": "" }, { "order": 183, @@ -1213,7 +1269,7 @@ "name": "icon-image-telemetry", "prevSize": 16, "code": 60202, - "tempChar": "" + "tempChar": "" }, { "order": 198, @@ -1221,7 +1277,7 @@ "name": "icon-telemetry-aggregate", "prevSize": 16, "code": 60203, - "tempChar": "" + "tempChar": "" }, { "order": 199, @@ -1229,7 +1285,7 @@ "name": "icon-bar-graph", "prevSize": 16, "code": 60204, - "tempChar": "" + "tempChar": "" }, { "order": 200, @@ -1237,7 +1293,7 @@ "name": "icon-map", "prevSize": 16, "code": 60205, - "tempChar": "" + "tempChar": "" }, { "order": 203, @@ -1245,7 +1301,7 @@ "name": "icon-plan", "prevSize": 16, "code": 60206, - "tempChar": "" + "tempChar": "" }, { "order": 204, @@ -1253,7 +1309,15 @@ "name": "icon-timelist", "prevSize": 16, "code": 60207, - "tempChar": "" + "tempChar": "" + }, + { + "order": 214, + "id": 184, + "name": "icon-notebook-restricted", + "prevSize": 16, + "code": 60209, + "tempChar": "" }, { "order": 205, @@ -2107,6 +2171,162 @@ ] } }, + { + "id": 183, + "paths": [ + "M512 0c-282.78 0-512 229.22-512 512s229.22 512 512 512 512-229.22 512-512-229.22-512-512-512zM263.1 263.1c66.48-66.48 154.88-103.1 248.9-103.1 66.74 0 130.64 18.48 185.9 52.96l-484.94 484.94c-34.5-55.24-52.96-119.16-52.96-185.9 0-94.020 36.62-182.42 103.1-248.9zM760.9 760.9c-66.48 66.48-154.88 103.1-248.9 103.1-66.74 0-130.64-18.48-185.9-52.96l484.94-484.94c34.5 55.24 52.96 119.16 52.96 185.9 0 94.020-36.62 182.42-103.1 248.9z" + ], + "attrs": [ + {} + ], + "isMulticolor": false, + "isMulticolor2": false, + "grid": 16, + "tags": [ + "icon-circle-slash" + ], + "colorPermutations": { + "12552552551": [ + {} + ] + } + }, + { + "id": 182, + "paths": [ + "M136.86 52.26c54.080-34.82 120.58-52.26 199.44-52.26 103.6 0 189.7 24.76 258.24 74.28s102.82 122.88 102.82 220.060c0 59.6-14.86 109.8-44.58 150.6-17.38 24.76-50.76 56.4-100.14 94.9l-48.68 37.82c-26.54 20.64-44.14 44.7-52.82 72.2-5.5 17.44-8.46 44.48-8.92 81.14h-186.4c2.74-77.48 10.060-131 21.94-160.58s42.5-63.62 91.88-102.12l50.060-39.2c16.46-12.38 29.72-25.9 39.78-40.58 18.28-25.2 27.42-52.96 27.42-83.22 0-34.84-10.18-66.6-30.52-95.24-20.36-28.64-57.52-42.98-111.48-42.98s-90.68 17.66-112.88 52.96c-22.18 35.32-33.26 71.98-33.26 110.040h-198.76c5.5-130.64 51.12-223.24 136.86-277.82zM251.020 825.24h205.62v198.74h-205.62v-198.74z" + ], + "attrs": [ + {} + ], + "width": 697, + "isMulticolor": false, + "isMulticolor2": false, + "grid": 16, + "tags": [ + "icon-question-mark" + ], + "colorPermutations": { + "12552552551": [ + {} + ] + } + }, + { + "id": 179, + "paths": [ + "M512 0c-282.76 0-512 214.9-512 480 0 92.26 27.8 178.44 75.92 251.6l-75.92 292.4 313.5-101.42c61.040 24.1 128.12 37.42 198.5 37.42 282.76 0 512-214.9 512-480s-229.24-480-512-480zM768 448l-320 320-192-192v-192l192 192 320-320v192z" + ], + "attrs": [ + {} + ], + "isMulticolor": false, + "isMulticolor2": false, + "grid": 16, + "tags": [ + "icon-status-poll-check" + ], + "colorPermutations": { + "12552552551": [ + {} + ] + } + }, + { + "id": 178, + "paths": [ + "M512 0c-282.76 0-512 214.9-512 480 0 92.26 27.8 178.44 75.92 251.6l-75.92 292.4 313.5-101.42c61.040 24.1 128.12 37.42 198.5 37.42 282.76 0 512-214.9 512-480s-229.24-480-512-480zM781.36 704h-538.72c-44.96 0-63.5-31.94-41.2-70.98l270-472.48c22.3-39.040 58.82-39.040 81.12 0l269.98 472.48c22.3 39.040 3.78 70.98-41.2 70.98z", + "M457.14 417.86l24.2 122.64h61.32l24.2-122.64v-163.5h-109.72v163.5z", + "M471.12 581.36h81.76v81.76h-81.76v-81.76z" + ], + "attrs": [ + {}, + {}, + {} + ], + "isMulticolor": false, + "isMulticolor2": false, + "grid": 16, + "tags": [ + "icon-status-poll-caution" + ], + "colorPermutations": { + "12552552551": [ + {}, + {}, + {} + ] + } + }, + { + "id": 180, + "paths": [ + "M391.18 668.7c35.72 22.98 77.32 35.3 120.82 35.3 59.84 0 116.080-23.3 158.4-65.6 42.3-42.3 65.6-98.56 65.6-158.4 0-43.5-12.32-85.080-35.3-120.82l-309.52 309.52z", + "M512 256c-59.84 0-116.080 23.3-158.4 65.6-42.3 42.3-65.6 98.56-65.6 158.4 0 43.5 12.32 85.080 35.3 120.82l309.52-309.52c-35.72-22.98-77.32-35.3-120.82-35.3z", + "M512 0c-282.76 0-512 214.9-512 480 0 92.26 27.8 178.44 75.92 251.6l-75.92 292.4 313.5-101.42c61.040 24.1 128.12 37.42 198.5 37.42 282.76 0 512-214.9 512-480s-229.24-480-512-480zM512 800c-176.74 0-320-143.26-320-320s143.26-320 320-320 320 143.26 320 320-143.26 320-320 320z" + ], + "attrs": [ + {}, + {}, + {} + ], + "isMulticolor": false, + "isMulticolor2": false, + "grid": 16, + "tags": [ + "icon-status-poll-circle-slash" + ], + "colorPermutations": { + "12552552551": [ + {}, + {}, + {} + ] + } + }, + { + "id": 181, + "paths": [ + "M512 0c-282.76 0-512 214.9-512 480 0 92.26 27.8 178.44 75.92 251.6l-75.92 292.4 313.5-101.42c61.040 24.1 128.12 37.42 198.5 37.42 282.76 0 512-214.9 512-480s-229.24-480-512-480zM579.020 832h-141.36v-136.64h141.36v136.64zM713.84 433.9c-11.94 17.020-34.9 38.78-68.84 65.24l-33.48 26c-18.24 14.18-30.34 30.74-36.32 49.64-3.78 11.98-5.82 30.58-6.14 55.8h-128.12c1.88-53.26 6.92-90.060 15.080-110.4 8.18-20.34 29.22-43.74 63.16-70.22l34.42-26.94c11.3-8.52 20.42-17.8 27.34-27.9 12.56-17.34 18.86-36.4 18.86-57.2 0-23.94-7-45.78-20.98-65.48-14-19.7-39.54-29.54-76.64-29.54s-62.34 12.14-77.6 36.4c-15.24 24.28-22.88 49.48-22.88 75.64h-136.64c3.78-89.84 35.14-153.5 94.080-191.020 37.18-23.94 82.9-35.94 137.12-35.94 71.22 0 130.42 17.020 177.54 51.060s70.68 84.48 70.68 151.3c0 40.98-10.22 75.5-30.66 103.54z" + ], + "attrs": [ + {} + ], + "isMulticolor": false, + "isMulticolor2": false, + "grid": 16, + "tags": [ + "icon-status-poll-question-mark" + ], + "colorPermutations": { + "12552552551": [ + {} + ] + } + }, + { + "id": 176, + "paths": [ + "M1000.080 334.64l-336.6 336.76-20.52 6.88-450.96 153.72 160.68-471.52 332.34-332.34c-54.040-18.2-112.28-28.14-173.020-28.14-282.76 0-512 214.9-512 480 0 92.26 27.8 178.44 75.92 251.6l-75.92 292.4 313.5-101.42c61.040 24.1 128.12 37.42 198.5 37.42 282.76 0 512-214.9 512-480 0-50.68-8.4-99.5-23.92-145.36z", + "M408.42 395.24l-2.16 6.3-111.7 327.9 334.12-113.86 4.62-4.68 350.28-350.28c6.8-6.78 14.96-19.1 14.96-38.9 0-34.86-26.82-83.28-69.88-126.38-26.54-26.54-55.9-47.6-82.7-59.34-47.34-20.8-72.020-6.24-82.64 4.36l-354.9 354.88zM470.56 421.42h44v88h88v44l-4.7 12.72-139.68 47.54-47.94-47.94 47.6-139.72 12.72-4.6z" + ], + "attrs": [ + {}, + {} + ], + "isMulticolor": false, + "isMulticolor2": false, + "grid": 16, + "tags": [ + "icon-status-poll-edit" + ], + "colorPermutations": { + "12552552551": [ + {}, + {} + ] + } + }, { "id": 105, "paths": [ @@ -3326,15 +3546,21 @@ { "id": 76, "paths": [ - "M510-2l-512 320v384l512 320 512-320v-384l-512-320zM585.4 859.2c-21.2 20.8-46 30.8-76 30.8-31.2 0-56.2-9.8-76.2-29.6-20-20-29.6-44.8-29.6-76.2 0-30.4 10.2-55.2 31-76.2s45.2-31.2 74.8-31.2c29.6 0 54.2 10.4 75.6 32s31.8 46.4 31.8 76c-0.2 29-10.8 54-31.4 74.4zM638.2 546.6c-23.6 11.8-37.4 22-43.4 32.4-3.6 6.2-6 14.8-7.4 26.8v41h-161.4v-44.2c0-40.2 4.4-69.8 13-88 8-17.2 22.6-30.2 44.8-40l34.8-15.4c32-14.2 48.2-35.2 48.2-62.8 0-16-6-30.4-17.2-41.8-11.2-11.2-25.6-17.2-41.6-17.2-24 0-54.4 10-62.8 57.4l-2.2 12.2h-147l1.4-16.2c4-44.6 17-82.4 38.8-112.2 19.6-27 45.6-48.6 77-64.6s64.6-24 98.2-24c60.6 0 110.2 19.4 151.4 59.6 41.2 40 61.2 88 61.2 147.2 0 70.8-28.8 121.4-85.8 149.8z" + "M511.98 0l-511.98 320v384l512 320 512-320v-384l-512.020-320zM586.22 896h-141.36v-136.64h141.36v136.64zM721.040 497.9c-11.94 17.020-34.9 38.78-68.84 65.24l-33.48 26c-18.24 14.18-30.34 30.74-36.32 49.64-3.78 11.98-5.82 30.58-6.14 55.8h-128.12c1.88-53.26 6.92-90.060 15.080-110.4 8.18-20.34 29.22-43.74 63.16-70.22l34.42-26.94c11.3-8.52 20.42-17.8 27.34-27.9 12.56-17.34 18.86-36.4 18.86-57.2 0-23.94-7-45.78-20.98-65.48-14-19.7-39.54-29.54-76.64-29.54s-62.34 12.14-77.6 36.4c-15.24 24.28-22.88 49.48-22.88 75.64h-136.64c3.78-89.84 35.14-153.5 94.080-191.020 37.18-23.94 82.9-35.94 137.12-35.94 71.22 0 130.42 17.020 177.54 51.060s70.68 84.48 70.68 151.3c0 40.98-10.22 75.5-30.66 103.54z" + ], + "attrs": [ + {} ], - "attrs": [], "grid": 16, "tags": [ "icon-object-unknown" ], + "isMulticolor": false, + "isMulticolor2": false, "colorPermutations": { - "12552552551": [] + "12552552551": [ + {} + ] } }, { @@ -4009,6 +4235,29 @@ ] } }, + { + "id": 184, + "paths": [ + "M896 110.72c0-79.9-55.38-127.32-123.080-105.36l-772.92 250.64h896v-145.28z", + "M896 320h-896v576c0 70.4 57.6 128 128 128h768c70.4 0 128-57.6 128-128v-448c0-70.4-57.6-128-128-128zM256 832h-128v-128h128v128zM256 640h-128v-128h128v128zM896 832h-512v-128h512v128zM896 640h-512v-128h512v128z" + ], + "attrs": [ + {}, + {} + ], + "isMulticolor": false, + "isMulticolor2": false, + "grid": 16, + "tags": [ + "icon-notebook-restricted" + ], + "colorPermutations": { + "12552552551": [ + {}, + {} + ] + } + }, { "id": 176, "paths": [ diff --git a/src/styles/fonts/Open-MCT-Symbols-16px.svg b/src/styles/fonts/Open-MCT-Symbols-16px.svg index c6455e381c..38ce5985a3 100644 --- a/src/styles/fonts/Open-MCT-Symbols-16px.svg +++ b/src/styles/fonts/Open-MCT-Symbols-16px.svg @@ -3,165 +3,173 @@ Generated by IcoMoon - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/styles/fonts/Open-MCT-Symbols-16px.ttf b/src/styles/fonts/Open-MCT-Symbols-16px.ttf index 073e2c6ec556deac1f1e5c31eb9885033409d428..94ab53538b6ee4ee72e83b1e7a1afc6b78958239 100644 GIT binary patch delta 6255 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 diff --git a/src/styles/fonts/Open-MCT-Symbols-16px.woff b/src/styles/fonts/Open-MCT-Symbols-16px.woff index ca1cc26bede033633737bcd580ab3615e72ae99a..510d6e9a9b85dae9d62daa494a38a0bdad0131be 100644 GIT binary patch delta 6470 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 { + this.openmct.indicators.getIndicatorObjectsByPriority().forEach(this.addIndicator); + + this.openmct.indicators.on('addIndicator', this.addIndicator); + }, + methods: { + addIndicator(indicator) { this.$el.appendChild(indicator.element); - }); + } } + }; diff --git a/src/utils/raf.js b/src/utils/raf.js new file mode 100644 index 0000000000..d5c0c48fe5 --- /dev/null +++ b/src/utils/raf.js @@ -0,0 +1,14 @@ +export default function raf(callback) { + let rendering = false; + + return () => { + if (!rendering) { + rendering = true; + + requestAnimationFrame(() => { + callback(); + rendering = false; + }); + } + }; +} diff --git a/src/utils/rafSpec.js b/src/utils/rafSpec.js new file mode 100644 index 0000000000..0bf5ae9d9c --- /dev/null +++ b/src/utils/rafSpec.js @@ -0,0 +1,61 @@ +import raf from "./raf"; + +describe('The raf utility function', () => { + it('Throttles function calls that arrive in quick succession using Request Animation Frame', () => { + const unthrottledFunction = jasmine.createSpy('unthrottledFunction'); + const throttledCallback = jasmine.createSpy('throttledCallback'); + const throttledFunction = raf(throttledCallback); + + for (let i = 0; i < 10; i++) { + unthrottledFunction(); + throttledFunction(); + } + + return new Promise((resolve) => { + requestAnimationFrame(resolve); + }).then(() => { + expect(unthrottledFunction).toHaveBeenCalledTimes(10); + expect(throttledCallback).toHaveBeenCalledTimes(1); + }); + }); + it('Only invokes callback once per animation frame', () => { + const throttledCallback = jasmine.createSpy('throttledCallback'); + const throttledFunction = raf(throttledCallback); + + for (let i = 0; i < 10; i++) { + throttledFunction(); + } + + return new Promise(resolve => { + requestAnimationFrame(resolve); + }).then(() => { + return new Promise(resolve => { + requestAnimationFrame(resolve); + }); + }).then(() => { + expect(throttledCallback).toHaveBeenCalledTimes(1); + }); + }); + it('Invokes callback again if called in subsequent animation frame', () => { + const throttledCallback = jasmine.createSpy('throttledCallback'); + const throttledFunction = raf(throttledCallback); + + for (let i = 0; i < 10; i++) { + throttledFunction(); + } + + return new Promise(resolve => { + requestAnimationFrame(resolve); + }).then(() => { + for (let i = 0; i < 10; i++) { + throttledFunction(); + } + + return new Promise(resolve => { + requestAnimationFrame(resolve); + }); + }).then(() => { + expect(throttledCallback).toHaveBeenCalledTimes(2); + }); + }); +});