diff --git a/e2e/tests/plugins/gauge.e2eSpec.js b/e2e/tests/plugins/gauge.e2eSpec.js new file mode 100644 index 0000000000..3f53ec40e4 --- /dev/null +++ b/e2e/tests/plugins/gauge.e2eSpec.js @@ -0,0 +1,31 @@ +// 1. create an gauge plugin +// 2. create with custom settings +// 3. verify required fields are required +// 4. should not be able to create gaugue inside a gauge. +// 5. should not be able to move/duplicate gaugue inside a gauge. +// 6. delete gauge +// 7. snapshot gauge +// 8. snapshot gauge inside Notebook entry +// 9. gauge inside Notebook entry +// 10. can drop inside other objects: +// can drop to display layout : yes +// can drop to Flexible layout : yes +// can drop to Folder : yes +// can drop to Bar graph: No +// can drop to clock: No +// . can drop to condition set: No +// . can drop to condition widegt: No +// // .............all other objects +// . can drop to webpage: No +// 11. drop telemetry inside a gauge and reflect on data in both fixed and realtime +// 12. can have only one telemetry per gague. (show confirmation dialog if want to replace) +// 12. all form props (except hidefromInspector) shows inside inspector +// 13. should be able to export and import +// 14. able to search in tree +// 15. supports fix and realtime mode +// 17. refresh after creating +// 18. open in new tab +// 19. two separate gauges inside display layout should not show same numbers unless have same telemetry (should show respctive telemetry data). +// 20. inside display layout compare gague values vs its telemetry value (should be same in fixed time and should tick with same rate/value in realtime) +// 21. same for two diff gauges- > inside display layout compare gague values vs its telemetry value (should be same in fixed time and should tick with same rate/value in realtime) +// 22. export jpeg/png of plugin diff --git a/index.html b/index.html index 105b2d49e0..5aae47c16f 100644 --- a/index.html +++ b/index.html @@ -77,7 +77,7 @@ openmct.install(openmct.plugins.LocalStorage()); - + openmct.install(openmct.plugins.example.Generator()); openmct.install(openmct.plugins.example.EventGeneratorPlugin()); openmct.install(openmct.plugins.example.ExampleImagery()); diff --git a/src/MCT.js b/src/MCT.js index e83cf89fb0..19700d691f 100644 --- a/src/MCT.js +++ b/src/MCT.js @@ -242,6 +242,7 @@ define([ // Plugins that are installed by default + this.install(this.plugins.Gauge()); this.install(this.plugins.Plot()); this.install(this.plugins.Chart()); this.install(this.plugins.TelemetryTable.default()); diff --git a/src/api/forms/FormController.js b/src/api/forms/FormController.js index 95a5b4a28f..68e195f4eb 100644 --- a/src/api/forms/FormController.js +++ b/src/api/forms/FormController.js @@ -1,5 +1,6 @@ import AutoCompleteField from './components/controls/AutoCompleteField.vue'; import ClockDisplayFormatField from './components/controls/ClockDisplayFormatField.vue'; +import CheckBoxField from './components/controls/CheckBoxField.vue'; import Datetime from './components/controls/Datetime.vue'; import FileInput from './components/controls/FileInput.vue'; import Locator from './components/controls/Locator.vue'; @@ -7,11 +8,13 @@ import NumberField from './components/controls/NumberField.vue'; import SelectField from './components/controls/SelectField.vue'; import TextAreaField from './components/controls/TextAreaField.vue'; import TextField from './components/controls/TextField.vue'; +import ToggleSwitchField from './components/controls/ToggleSwitchField.vue'; import Vue from 'vue'; export const DEFAULT_CONTROLS_MAP = { 'autocomplete': AutoCompleteField, + 'checkbox': CheckBoxField, 'composite': ClockDisplayFormatField, 'datetime': Datetime, 'file-input': FileInput, @@ -19,7 +22,8 @@ export const DEFAULT_CONTROLS_MAP = { 'numberfield': NumberField, 'select': SelectField, 'textarea': TextAreaField, - 'textfield': TextField + 'textfield': TextField, + 'toggleSwitch': ToggleSwitchField }; export default class FormControl { @@ -94,4 +98,3 @@ export default class FormControl { }; } } - diff --git a/src/api/forms/components/FormRow.vue b/src/api/forms/components/FormRow.vue index 800ca7c589..c6e3aba454 100644 --- a/src/api/forms/components/FormRow.vue +++ b/src/api/forms/components/FormRow.vue @@ -79,10 +79,12 @@ export default { rowClass() { let cssClass = this.cssClass; - if (this.row.required) { - cssClass = `${cssClass} req`; + if (!this.row.required) { + return; } + cssClass = `${cssClass} req`; + if (this.visited && this.valid !== undefined) { if (this.valid === true) { cssClass = `${cssClass} valid`; diff --git a/src/api/forms/components/controls/CheckBoxField.vue b/src/api/forms/components/controls/CheckBoxField.vue new file mode 100644 index 0000000000..bfff992bd6 --- /dev/null +++ b/src/api/forms/components/controls/CheckBoxField.vue @@ -0,0 +1,55 @@ +/***************************************************************************** +* 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. +*****************************************************************************/ + + + + diff --git a/src/api/forms/components/controls/NumberField.vue b/src/api/forms/components/controls/NumberField.vue index e552b492c9..d4edd2b1b3 100644 --- a/src/api/forms/components/controls/NumberField.vue +++ b/src/api/forms/components/controls/NumberField.vue @@ -58,7 +58,6 @@ export default { }, methods: { updateText() { - console.log('updateText', this.field); const data = { model: this.model, value: this.field diff --git a/src/api/forms/components/controls/ToggleSwitchField.vue b/src/api/forms/components/controls/ToggleSwitchField.vue new file mode 100644 index 0000000000..0c3a3e1186 --- /dev/null +++ b/src/api/forms/components/controls/ToggleSwitchField.vue @@ -0,0 +1,62 @@ +/***************************************************************************** +* 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. +*****************************************************************************/ + + + + diff --git a/src/api/forms/toggle-check-box-mixin.js b/src/api/forms/toggle-check-box-mixin.js new file mode 100644 index 0000000000..f15c344039 --- /dev/null +++ b/src/api/forms/toggle-check-box-mixin.js @@ -0,0 +1,19 @@ +export default { + data() { + return { + isChecked: false + }; + }, + methods: { + toggleCheckBox(event) { + this.isChecked = !this.isChecked; + + const data = { + model: this.model, + value: this.isChecked + }; + + this.$emit('onChange', data); + } + } +}; diff --git a/src/plugins/formActions/CreateWizard.js b/src/plugins/formActions/CreateWizard.js index 8921a100ba..d17bdeb6bd 100644 --- a/src/plugins/formActions/CreateWizard.js +++ b/src/plugins/formActions/CreateWizard.js @@ -90,6 +90,9 @@ export default class CreateWizard { rows: this.properties.map(property => { const row = JSON.parse(JSON.stringify(property)); row.value = this.getValue(row); + if (property.validate) { + row.validate = property.validate; + } return row; }).filter(row => row && row.control) diff --git a/src/plugins/gauge/GaugePlugin.js b/src/plugins/gauge/GaugePlugin.js new file mode 100644 index 0000000000..c9db912df1 --- /dev/null +++ b/src/plugins/gauge/GaugePlugin.js @@ -0,0 +1,199 @@ +/***************************************************************************** + * 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 GaugeViewProvider from './GaugeViewProvider'; +import GaugeFormController from './components/GaugeFormController.vue'; +import Vue from 'vue'; + +export const GAUGE_TYPES = [ + ['Filled Dial', 'dial-filled'], + ['Needle Dial', 'dial-needle'], + ['Vertical Meter', 'meter-vertical'], + ['Vertical Meter Inverted', 'meter-vertical-inverted'], + ['Horizontal Meter', 'meter-horizontal'] +]; + +export default function () { + return function install(openmct) { + openmct.objectViews.addProvider(new GaugeViewProvider(openmct)); + + openmct.forms.addNewFormControl('gauge-controller', getGaugeFormController(openmct)); + openmct.types.addType('gauge', { + name: "Gauge", + creatable: true, + description: "Graphically visualize a telemetry element's current value between a minimum and maximum.", + cssClass: 'icon-gauge', + initialize(domainObject) { + domainObject.composition = []; + domainObject.configuration = { + gaugeController: { + gaugeType: GAUGE_TYPES[0][1], + isDisplayMinMax: true, + isDisplayCurVal: true, + isUseTelemetryLimits: true, + limitLow: 10, + limitHigh: 90, + max: 100, + min: 0, + precision: 2 + } + }; + }, + form: [ + { + name: "Display current value", + control: "toggleSwitch", + cssClass: "l-input", + key: "isDisplayCurVal", + property: [ + "configuration", + "gaugeController", + "isDisplayCurVal" + ] + }, + { + name: "Display range values", + control: "toggleSwitch", + cssClass: "l-input", + key: "isDisplayMinMax", + property: [ + "configuration", + "gaugeController", + "isDisplayMinMax" + ] + }, + { + name: "Float precision", + control: "numberfield", + cssClass: "l-input-sm", + key: "precision", + property: [ + "configuration", + "gaugeController", + "precision" + ] + }, + { + name: "Gauge type", + options: GAUGE_TYPES.map(type => { + return { + name: type[0], + value: type[1] + }; + }), + control: "select", + cssClass: "l-input-sm", + key: "gaugeController", + property: [ + "configuration", + "gaugeController", + "gaugeType" + ] + }, + { + name: "Value ranges and limits", + control: "gauge-controller", + cssClass: "l-input", + key: "gaugeController", + required: false, + hideFromInspector: true, + property: [ + "configuration", + "gaugeController" + ], + validate: ({ value }, callback) => { + if (value.isUseTelemetryLimits) { + return true; + } + + const { min, max, limitLow, limitHigh } = value; + const valid = { + min: true, + max: true, + limitLow: true, + limitHigh: true + }; + + if (min === '') { + valid.min = false; + } + + if (max === '') { + valid.max = false; + } + + if (max < min) { + valid.min = false; + valid.max = false; + } + + if (limitLow !== '') { + valid.limitLow = min <= limitLow && limitLow < max; + } + + if (limitHigh !== '') { + valid.limitHigh = min < limitHigh && limitHigh <= max; + } + + if (valid.limitLow && valid.limitHigh + && limitLow !== '' && limitHigh !== '' + && limitLow > limitHigh) { + valid.limitLow = false; + valid.limitHigh = false; + } + + if (callback) { + callback(valid); + } + + return valid.min && valid.max && valid.limitLow && valid.limitHigh; + } + } + ] + }); + }; + + function getGaugeFormController(openmct) { + return { + show(element, model, onChange) { + const rowComponent = new Vue({ + el: element, + components: { + GaugeFormController + }, + provide: { + openmct + }, + data() { + return { + model, + onChange + }; + }, + template: `` + }); + + return rowComponent; + } + }; + } +} diff --git a/src/plugins/gauge/GaugePluginSpec.js b/src/plugins/gauge/GaugePluginSpec.js new file mode 100644 index 0000000000..982cb58c49 --- /dev/null +++ b/src/plugins/gauge/GaugePluginSpec.js @@ -0,0 +1,805 @@ +/***************************************************************************** + * 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'; +import { debounce } from 'lodash'; + +import Vue from 'vue'; + +let gaugeDomainObject = { + identifier: { + key: 'gauge', + namespace: 'test-namespace' + }, + type: 'gauge', + composition: [] +}; + +describe('Gauge plugin', () => { + let openmct; + let child; + let gaugeHolder; + + beforeEach((done) => { + gaugeHolder = document.createElement('div'); + gaugeHolder.style.display = 'block'; + gaugeHolder.style.width = '1920px'; + gaugeHolder.style.height = '1080px'; + + child = document.createElement('div'); + gaugeHolder.appendChild(child); + + openmct = createOpenMct(); + openmct.on('start', done); + + openmct.startHeadless(); + }); + + afterEach(() => { + return resetApplicationState(openmct); + }); + + it('Plugin installed by default', () => { + const gaugueType = openmct.types.get('gauge'); + + expect(gaugueType).not.toBeNull(); + expect(gaugueType.definition.name).toEqual('Gauge'); + }); + + it('Gaugue plugin is creatable', () => { + const gaugueType = openmct.types.get('gauge'); + + expect(gaugueType.definition.creatable).toBeTrue(); + }); + + it('Gaugue plugin is creatable', () => { + const gaugueType = openmct.types.get('gauge'); + + expect(gaugueType.definition.creatable).toBeTrue(); + }); + + it('Gaugue form controller', () => { + const gaugeController = openmct.forms.getFormControl('gauge-controller'); + expect(gaugeController).toBeDefined(); + }); + + describe('Gaugue with Filled Dial', () => { + let gaugeViewProvider; + let gaugeView; + let gaugeViewObject; + let mutablegaugeObject; + let randomValue; + + const minValue = -1; + const maxValue = 1; + + beforeEach(() => { + randomValue = Math.random(); + gaugeViewObject = { + ...gaugeDomainObject, + configuration: { + gaugeController: { + gaugeType: 'dial-filled', + isDisplayMinMax: true, + isDisplayCurVal: true, + isUseTelemetryLimits: false, + limitLow: -0.9, + limitHigh: 0.9, + max: maxValue, + min: minValue, + precision: 2 + } + }, + composition: [ + { + namespace: 'test-namespace', + key: 'test-object' + } + ], + id: 'test-object', + name: 'gauge' + }; + + const testObjectProvider = jasmine.createSpyObj('testObjectProvider', [ + 'get', + 'create', + 'update', + 'observe' + ]); + + openmct.editor = {}; + openmct.editor.isEditing = () => false; + + const applicableViews = openmct.objectViews.get(gaugeViewObject, [gaugeViewObject]); + gaugeViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'gauge'); + + testObjectProvider.get.and.returnValue(Promise.resolve(gaugeViewObject)); + testObjectProvider.create.and.returnValue(Promise.resolve(gaugeViewObject)); + openmct.objects.addProvider('test-namespace', testObjectProvider); + testObjectProvider.observe.and.returnValue(() => {}); + testObjectProvider.create.and.returnValue(Promise.resolve(true)); + testObjectProvider.update.and.returnValue(Promise.resolve(true)); + + spyOn(openmct.telemetry, 'getMetadata').and.returnValue({ + valuesForHints: () => { + return [ + { + source: 'sin' + } + ]; + }, + value: () => 1 + }); + spyOn(openmct.telemetry, 'getValueFormatter').and.returnValue({ + parse: () => { + return 2000; + } + }); + spyOn(openmct.telemetry, 'getFormatMap').and.returnValue({ + sin: { + format: (datum) => { + return randomValue; + } + } + }); + spyOn(openmct.telemetry, 'getLimits').and.returnValue({ limits: () => Promise.resolve() }); + spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([randomValue])); + spyOn(openmct.time, 'bounds').and.returnValue({ + start: 1000, + end: 5000 + }); + + return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => { + mutablegaugeObject = mutableObject; + gaugeView = gaugeViewProvider.view(mutablegaugeObject); + gaugeView.show(child); + + return Vue.nextTick(); + }); + }); + + afterEach(() => { + gaugeView.destroy(); + + return resetApplicationState(openmct); + }); + + it('provides gauge view', () => { + expect(gaugeViewProvider).toBeDefined(); + }); + + it('renders gauge element', () => { + const gaugeElement = gaugeHolder.querySelectorAll('.c-gauge'); + expect(gaugeElement.length).toBe(1); + }); + + it('renders major elements', () => { + const wrapperElement = gaugeHolder.querySelector('.c-gauge__wrapper'); + const rangeElement = gaugeHolder.querySelector('.c-gauge__range'); + const curveElement = gaugeHolder.querySelector('.c-gauge__curval'); + const dialElement = gaugeHolder.querySelector('.c-dial'); + + const hasMajorElements = Boolean(wrapperElement && rangeElement && curveElement && dialElement); + + expect(hasMajorElements).toBe(true); + }); + + it('renders correct min max values', () => { + expect(gaugeHolder.querySelector('.c-gauge__range').textContent).toEqual(`${minValue} ${maxValue}`); + }); + + it('renders correct current value', (done) => { + function WatchUpdateValue() { + const textElement = gaugeHolder.querySelector('.c-gauge__curval-text'); + expect(Number(textElement.textContent).toFixed(gaugeViewObject.configuration.gaugeController.precision)).toBe(randomValue.toFixed(gaugeViewObject.configuration.gaugeController.precision)); + done(); + } + + const debouncedWatchUpdate = debounce(WatchUpdateValue, 200); + Vue.nextTick(debouncedWatchUpdate); + }); + }); + + describe('Gaugue with Needle Dial', () => { + let gaugeViewProvider; + let gaugeView; + let gaugeViewObject; + let mutablegaugeObject; + let randomValue; + + const minValue = -1; + const maxValue = 1; + beforeEach(() => { + randomValue = Math.random(); + gaugeViewObject = { + ...gaugeDomainObject, + configuration: { + gaugeController: { + gaugeType: 'dial-needle', + isDisplayMinMax: true, + isDisplayCurVal: true, + isUseTelemetryLimits: false, + limitLow: -0.9, + limitHigh: 0.9, + max: maxValue, + min: minValue, + precision: 2 + } + }, + composition: [ + { + namespace: 'test-namespace', + key: 'test-object' + } + ], + id: 'test-object', + name: 'gauge' + }; + + const testObjectProvider = jasmine.createSpyObj('testObjectProvider', [ + 'get', + 'create', + 'update', + 'observe' + ]); + + openmct.editor = {}; + openmct.editor.isEditing = () => false; + + const applicableViews = openmct.objectViews.get(gaugeViewObject, [gaugeViewObject]); + gaugeViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'gauge'); + + testObjectProvider.get.and.returnValue(Promise.resolve(gaugeViewObject)); + testObjectProvider.create.and.returnValue(Promise.resolve(gaugeViewObject)); + openmct.objects.addProvider('test-namespace', testObjectProvider); + testObjectProvider.observe.and.returnValue(() => {}); + testObjectProvider.create.and.returnValue(Promise.resolve(true)); + testObjectProvider.update.and.returnValue(Promise.resolve(true)); + + spyOn(openmct.telemetry, 'getMetadata').and.returnValue({ + valuesForHints: () => { + return [ + { + source: 'sin' + } + ]; + }, + value: () => 1 + }); + spyOn(openmct.telemetry, 'getValueFormatter').and.returnValue({ + parse: () => { + return 2000; + } + }); + spyOn(openmct.telemetry, 'getFormatMap').and.returnValue({ + sin: { + format: (datum) => { + return randomValue; + } + } + }); + spyOn(openmct.telemetry, 'getLimits').and.returnValue({ limits: () => Promise.resolve() }); + spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([randomValue])); + spyOn(openmct.time, 'bounds').and.returnValue({ + start: 1000, + end: 5000 + }); + + return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => { + mutablegaugeObject = mutableObject; + gaugeView = gaugeViewProvider.view(mutablegaugeObject); + gaugeView.show(child); + + return Vue.nextTick(); + }); + }); + + afterEach(() => { + gaugeView.destroy(); + + return resetApplicationState(openmct); + }); + + it('provides gauge view', () => { + expect(gaugeViewProvider).toBeDefined(); + }); + + it('renders gauge element', () => { + const gaugeElement = gaugeHolder.querySelectorAll('.c-gauge'); + expect(gaugeElement.length).toBe(1); + }); + + it('renders major elements', () => { + const wrapperElement = gaugeHolder.querySelector('.c-gauge__wrapper'); + const rangeElement = gaugeHolder.querySelector('.c-gauge__range'); + const curveElement = gaugeHolder.querySelector('.c-gauge__curval'); + const dialElement = gaugeHolder.querySelector('.c-dial'); + + const hasMajorElements = Boolean(wrapperElement && rangeElement && curveElement && dialElement); + + expect(hasMajorElements).toBe(true); + }); + + it('renders correct min max values', () => { + expect(gaugeHolder.querySelector('.c-gauge__range').textContent).toEqual(`${minValue} ${maxValue}`); + }); + + it('renders correct current value', (done) => { + function WatchUpdateValue() { + const textElement = gaugeHolder.querySelector('.c-gauge__curval-text'); + expect(Number(textElement.textContent).toFixed(gaugeViewObject.configuration.gaugeController.precision)).toBe(randomValue.toFixed(gaugeViewObject.configuration.gaugeController.precision)); + done(); + } + + const debouncedWatchUpdate = debounce(WatchUpdateValue, 200); + Vue.nextTick(debouncedWatchUpdate); + }); + }); + + describe('Gaugue with Vertical Meter', () => { + let gaugeViewProvider; + let gaugeView; + let gaugeViewObject; + let mutablegaugeObject; + let randomValue; + + const minValue = -1; + const maxValue = 1; + beforeEach(() => { + randomValue = Math.random(); + gaugeViewObject = { + ...gaugeDomainObject, + configuration: { + gaugeController: { + gaugeType: 'meter-vertical', + isDisplayMinMax: true, + isDisplayCurVal: true, + isUseTelemetryLimits: false, + limitLow: -0.9, + limitHigh: 0.9, + max: maxValue, + min: minValue, + precision: 2 + } + }, + composition: [ + { + namespace: 'test-namespace', + key: 'test-object' + } + ], + id: 'test-object', + name: 'gauge' + }; + + const testObjectProvider = jasmine.createSpyObj('testObjectProvider', [ + 'get', + 'create', + 'update', + 'observe' + ]); + + openmct.editor = {}; + openmct.editor.isEditing = () => false; + + const applicableViews = openmct.objectViews.get(gaugeViewObject, [gaugeViewObject]); + gaugeViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'gauge'); + + testObjectProvider.get.and.returnValue(Promise.resolve(gaugeViewObject)); + testObjectProvider.create.and.returnValue(Promise.resolve(gaugeViewObject)); + openmct.objects.addProvider('test-namespace', testObjectProvider); + testObjectProvider.observe.and.returnValue(() => {}); + testObjectProvider.create.and.returnValue(Promise.resolve(true)); + testObjectProvider.update.and.returnValue(Promise.resolve(true)); + + spyOn(openmct.telemetry, 'getMetadata').and.returnValue({ + valuesForHints: () => { + return [ + { + source: 'sin' + } + ]; + }, + value: () => 1 + }); + spyOn(openmct.telemetry, 'getValueFormatter').and.returnValue({ + parse: () => { + return 2000; + } + }); + spyOn(openmct.telemetry, 'getFormatMap').and.returnValue({ + sin: { + format: (datum) => { + return randomValue; + } + } + }); + spyOn(openmct.telemetry, 'getLimits').and.returnValue({ limits: () => Promise.resolve() }); + spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([randomValue])); + spyOn(openmct.time, 'bounds').and.returnValue({ + start: 1000, + end: 5000 + }); + + return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => { + mutablegaugeObject = mutableObject; + gaugeView = gaugeViewProvider.view(mutablegaugeObject); + gaugeView.show(child); + + return Vue.nextTick(); + }); + }); + + afterEach(() => { + gaugeView.destroy(); + + return resetApplicationState(openmct); + }); + + it('provides gauge view', () => { + expect(gaugeViewProvider).toBeDefined(); + }); + + it('renders gauge element', () => { + const gaugeElement = gaugeHolder.querySelectorAll('.c-gauge'); + expect(gaugeElement.length).toBe(1); + }); + + it('renders major elements', () => { + const wrapperElement = gaugeHolder.querySelector('.c-gauge__wrapper'); + const rangeElement = gaugeHolder.querySelector('.c-gauge__range'); + const curveElement = gaugeHolder.querySelector('.c-meter'); + const dialElement = gaugeHolder.querySelector('.c-meter__bg'); + + const hasMajorElements = Boolean(wrapperElement && rangeElement && curveElement && dialElement); + + expect(hasMajorElements).toBe(true); + }); + + it('renders correct min max values', () => { + expect(gaugeHolder.querySelector('.c-gauge__range').textContent).toEqual(`${maxValue} ${minValue}`); + }); + + it('renders correct current value', (done) => { + function WatchUpdateValue() { + const textElement = gaugeHolder.querySelector('.c-gauge__curval-text'); + expect(Number(textElement.textContent).toFixed(gaugeViewObject.configuration.gaugeController.precision)).toBe(randomValue.toFixed(gaugeViewObject.configuration.gaugeController.precision)); + done(); + } + + const debouncedWatchUpdate = debounce(WatchUpdateValue, 200); + Vue.nextTick(debouncedWatchUpdate); + }); + }); + + describe('Gaugue with Vertical Meter Inverted', () => { + let gaugeViewProvider; + let gaugeView; + let gaugeViewObject; + let mutablegaugeObject; + + beforeEach(() => { + gaugeViewObject = { + ...gaugeDomainObject, + configuration: { + gaugeController: { + gaugeType: 'meter-vertical', + isDisplayMinMax: true, + isDisplayCurVal: true, + isUseTelemetryLimits: false, + limitLow: -0.9, + limitHigh: 0.9, + max: 1, + min: -1, + precision: 2 + } + }, + id: 'test-object', + name: 'gauge' + }; + + const testObjectProvider = jasmine.createSpyObj('testObjectProvider', [ + 'get', + 'create', + 'update', + 'observe' + ]); + + openmct.editor = {}; + openmct.editor.isEditing = () => false; + + const applicableViews = openmct.objectViews.get(gaugeViewObject, [gaugeViewObject]); + gaugeViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'gauge'); + + testObjectProvider.get.and.returnValue(Promise.resolve(gaugeViewObject)); + testObjectProvider.create.and.returnValue(Promise.resolve(gaugeViewObject)); + openmct.objects.addProvider('test-namespace', testObjectProvider); + testObjectProvider.observe.and.returnValue(() => {}); + testObjectProvider.create.and.returnValue(Promise.resolve(true)); + testObjectProvider.update.and.returnValue(Promise.resolve(true)); + + return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => { + mutablegaugeObject = mutableObject; + + gaugeView = gaugeViewProvider.view(mutablegaugeObject); + gaugeView.show(child); + + return Vue.nextTick(); + }); + }); + + afterEach(() => { + gaugeView.destroy(); + + return resetApplicationState(openmct); + }); + + it('provides gauge view', () => { + expect(gaugeViewProvider).toBeDefined(); + }); + + it('renders gauge element', () => { + const gaugeElement = gaugeHolder.querySelectorAll('.c-gauge'); + expect(gaugeElement.length).toBe(1); + }); + + it('renders major elements', () => { + const wrapperElement = gaugeHolder.querySelector('.c-gauge__wrapper'); + const rangeElement = gaugeHolder.querySelector('.c-gauge__range'); + const curveElement = gaugeHolder.querySelector('.c-meter'); + const dialElement = gaugeHolder.querySelector('.c-meter__bg'); + + const hasMajorElements = Boolean(wrapperElement && rangeElement && curveElement && dialElement); + + expect(hasMajorElements).toBe(true); + }); + }); + + describe('Gaugue with Horizontal Meter', () => { + let gaugeViewProvider; + let gaugeView; + let gaugeViewObject; + let mutablegaugeObject; + + beforeEach(() => { + gaugeViewObject = { + ...gaugeDomainObject, + configuration: { + gaugeController: { + gaugeType: 'meter-vertical', + isDisplayMinMax: true, + isDisplayCurVal: true, + isUseTelemetryLimits: false, + limitLow: -0.9, + limitHigh: 0.9, + max: 1, + min: -1, + precision: 2 + } + }, + id: 'test-object', + name: 'gauge' + }; + + const testObjectProvider = jasmine.createSpyObj('testObjectProvider', [ + 'get', + 'create', + 'update', + 'observe' + ]); + + openmct.editor = {}; + openmct.editor.isEditing = () => false; + + const applicableViews = openmct.objectViews.get(gaugeViewObject, [gaugeViewObject]); + gaugeViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'gauge'); + + testObjectProvider.get.and.returnValue(Promise.resolve(gaugeViewObject)); + testObjectProvider.create.and.returnValue(Promise.resolve(gaugeViewObject)); + openmct.objects.addProvider('test-namespace', testObjectProvider); + testObjectProvider.observe.and.returnValue(() => {}); + testObjectProvider.create.and.returnValue(Promise.resolve(true)); + testObjectProvider.update.and.returnValue(Promise.resolve(true)); + + return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => { + mutablegaugeObject = mutableObject; + + gaugeView = gaugeViewProvider.view(mutablegaugeObject); + gaugeView.show(child); + + return Vue.nextTick(); + }); + }); + + afterEach(() => { + gaugeView.destroy(); + + return resetApplicationState(openmct); + }); + + it('provides gauge view', () => { + expect(gaugeViewProvider).toBeDefined(); + }); + + it('renders gauge element', () => { + const gaugeElement = gaugeHolder.querySelectorAll('.c-gauge'); + expect(gaugeElement.length).toBe(1); + }); + + it('renders major elements', () => { + const wrapperElement = gaugeHolder.querySelector('.c-gauge__wrapper'); + const rangeElement = gaugeHolder.querySelector('.c-gauge__range'); + const curveElement = gaugeHolder.querySelector('.c-meter'); + const dialElement = gaugeHolder.querySelector('.c-meter__bg'); + + const hasMajorElements = Boolean(wrapperElement && rangeElement && curveElement && dialElement); + + expect(hasMajorElements).toBe(true); + }); + }); + + describe('Gaugue with Filled Dial with Use Telemetry Limits', () => { + let gaugeViewProvider; + let gaugeView; + let gaugeViewObject; + let mutablegaugeObject; + let randomValue; + + beforeEach(() => { + randomValue = Math.random(); + + gaugeViewObject = { + ...gaugeDomainObject, + configuration: { + gaugeController: { + gaugeType: 'dial-filled', + isDisplayMinMax: true, + isDisplayCurVal: true, + isUseTelemetryLimits: true, + limitLow: 10, + limitHigh: 90, + max: 100, + min: 0, + precision: 2 + } + }, + composition: [ + { + namespace: 'test-namespace', + key: 'test-object' + } + ], + id: 'test-object', + name: 'gauge' + }; + + const testObjectProvider = jasmine.createSpyObj('testObjectProvider', [ + 'get', + 'create', + 'update', + 'observe' + ]); + + openmct.editor = {}; + openmct.editor.isEditing = () => false; + + const applicableViews = openmct.objectViews.get(gaugeViewObject, [gaugeViewObject]); + gaugeViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'gauge'); + + testObjectProvider.get.and.returnValue(Promise.resolve(gaugeViewObject)); + testObjectProvider.create.and.returnValue(Promise.resolve(gaugeViewObject)); + openmct.objects.addProvider('test-namespace', testObjectProvider); + testObjectProvider.observe.and.returnValue(() => {}); + testObjectProvider.create.and.returnValue(Promise.resolve(true)); + testObjectProvider.update.and.returnValue(Promise.resolve(true)); + + spyOn(openmct.telemetry, 'getMetadata').and.returnValue({ + valuesForHints: () => { + return [ + { + source: 'sin' + } + ]; + }, + value: () => 1 + }); + spyOn(openmct.telemetry, 'getValueFormatter').and.returnValue({ + parse: () => { + return 2000; + } + }); + spyOn(openmct.telemetry, 'getFormatMap').and.returnValue({ + sin: { + format: (datum) => { + return randomValue; + } + } + }); + spyOn(openmct.telemetry, 'getLimits').and.returnValue( + { + limits: () => Promise.resolve({ + CRITICAL: { + high: 0.99, + low: -0.99 + } + }) + } + ); + spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([randomValue])); + spyOn(openmct.time, 'bounds').and.returnValue({ + start: 1000, + end: 5000 + }); + + return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => { + mutablegaugeObject = mutableObject; + gaugeView = gaugeViewProvider.view(mutablegaugeObject); + gaugeView.show(child); + + return Vue.nextTick(); + }); + }); + + afterEach(() => { + gaugeView.destroy(); + + return resetApplicationState(openmct); + }); + + it('provides gauge view', () => { + expect(gaugeViewProvider).toBeDefined(); + }); + + it('renders gauge element', () => { + const gaugeElement = gaugeHolder.querySelectorAll('.c-gauge'); + expect(gaugeElement.length).toBe(1); + }); + + it('renders major elements', () => { + const wrapperElement = gaugeHolder.querySelector('.c-gauge__wrapper'); + const rangeElement = gaugeHolder.querySelector('.c-gauge__range'); + const curveElement = gaugeHolder.querySelector('.c-gauge__curval'); + const dialElement = gaugeHolder.querySelector('.c-dial'); + + const hasMajorElements = Boolean(wrapperElement && rangeElement && curveElement && dialElement); + + expect(hasMajorElements).toBe(true); + }); + + it('renders correct min max values', () => { + expect(gaugeHolder.querySelector('.c-gauge__range').textContent).toEqual(`${gaugeViewObject.configuration.gaugeController.min} ${gaugeViewObject.configuration.gaugeController.max}`); + }); + + it('renders correct current value', (done) => { + function WatchUpdateValue() { + const textElement = gaugeHolder.querySelector('.c-gauge__curval-text'); + expect(Number(textElement.textContent).toFixed(gaugeViewObject.configuration.gaugeController.precision)).toBe(randomValue.toFixed(gaugeViewObject.configuration.gaugeController.precision)); + done(); + } + + const debouncedWatchUpdate = debounce(WatchUpdateValue, 200); + Vue.nextTick(debouncedWatchUpdate); + }); + }); +}); diff --git a/src/plugins/gauge/GaugeViewProvider.js b/src/plugins/gauge/GaugeViewProvider.js new file mode 100644 index 0000000000..b70acd2ae7 --- /dev/null +++ b/src/plugins/gauge/GaugeViewProvider.js @@ -0,0 +1,65 @@ +/***************************************************************************** + * 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 GaugeComponent from './components/Gauge.vue'; +import Vue from 'vue'; + +export default function GaugeViewProvider(openmct) { + return { + key: 'gauge', + name: 'Gauge', + cssClass: 'icon-gauge', + canView: function (domainObject) { + return domainObject.type === 'gauge'; + }, + canEdit: function (domainObject) { + return false; + }, + view: function (domainObject) { + let component; + + return { + show: function (element) { + component = new Vue({ + el: element, + components: { + GaugeComponent + }, + provide: { + openmct, + domainObject, + composition: openmct.composition.get(domainObject) + }, + template: '' + }); + }, + destroy: function (element) { + component.$destroy(); + component = undefined; + } + }; + }, + priority: function () { + return 1; + } + }; +} diff --git a/src/plugins/gauge/components/Gauge.vue b/src/plugins/gauge/components/Gauge.vue new file mode 100644 index 0000000000..8c6d335156 --- /dev/null +++ b/src/plugins/gauge/components/Gauge.vue @@ -0,0 +1,467 @@ +/***************************************************************************** +* 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. +*****************************************************************************/ + + + diff --git a/src/plugins/gauge/components/GaugeFormController.vue b/src/plugins/gauge/components/GaugeFormController.vue new file mode 100644 index 0000000000..949c21018c --- /dev/null +++ b/src/plugins/gauge/components/GaugeFormController.vue @@ -0,0 +1,170 @@ +/***************************************************************************** +* 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. +*****************************************************************************/ + + + + diff --git a/src/plugins/gauge/gauge.scss b/src/plugins/gauge/gauge.scss new file mode 100644 index 0000000000..6f6dbc3c3c --- /dev/null +++ b/src/plugins/gauge/gauge.scss @@ -0,0 +1,269 @@ +$dialClip: polygon(0 0, 100% 0, 100% 100%, 50% 50%, 0 100%); +$dialClip90: polygon(0 0, 50% 50%, 0 100%); +$dialClip180: polygon(0 0, 100% 0, 0 100%); +$limitHighClip90: polygon(0 0, 100% 0, 100% 100%); +$limitHighClip180: polygon(100% 0, 100% 100%, 0 100%); + +.is-object-type-gauge { + overflow: hidden; +} + +.req-indicator { + width: 20px; + + &.invalid, + &.invalid.req { @include validationState($glyph-icon-x, $colorFormInvalid); } + + &.valid, + &.valid.req { @include validationState($glyph-icon-check, $colorFormValid); } + + &.req { @include validationState($glyph-icon-asterisk, $colorFormRequired); } +} + +.c-gauge { + // Both dial and meter types + overflow: hidden; + + &__range { + $c: $colorGaugeRange; + color: $c; + + text { + fill: $c; + } + } + + &__wrapper { + @include abs(); + overflow: hidden; + } + + svg { + path { + transform-origin: center; + } + + &.c-gauge__curval { + @include abs(); + fill: $colorGaugeTextValue; + position: absolute; + width: 100%; + height: 100%; + z-index: 2; + + .c-gauge__curval-text { + font-family: $heroFont; + transform: translate(50%, 75%); + } + } + } + + &[class*='dial'] { + // Square aspect ratio + width: 100%; + padding-bottom: 100%; + } + + &[class*='meter'] { + @include abs(); + } +} + +/********************************************** DIAL GAUGE */ +.c-dial { + // Dial elements + @include abs(); + clip-path: $dialClip; + + svg, + &__ticks, + &__bg, + &[class*='__limit'], + &__value { + @include abs(); + } + + .c-high-limit-clip--90 { + clip-path: $limitHighClip90; + } + + .c-high-limit-clip--180 { + clip-path: $limitHighClip180; + } + + &__limit-high path { fill: $colorGaugeLimitHigh; } + &__limit-low path { fill: $colorGaugeLimitLow; } + + &__value, + &__limit-low { + &.c-dial-clip--90 { + clip-path: $dialClip90; + } + + &.c-dial-clip--180 { + clip-path: $dialClip180; + } + } + + &__value { + path, + polygon { + fill: $colorGaugeValue; + } + } + + &__bg { + path { + fill: $colorGaugeBg; + } + } +} + +.c-gauge--dial-needle .c-dial__value { + path { + transition: transform $transitionTimeGauge; + } +} + +/********************************************** METER GAUGE */ +.c-meter { + // Common styles for c-meter + @include abs(); + display: flex; + + &__range { + display: flex; + flex: 0 0 auto; + justify-content: space-between; + } + + &__bg { + background: $colorGaugeBg; + border-radius: $basicCr; + flex: 1 1 auto; + overflow: hidden; + } + + &__value { + // Filled area + position: absolute; + background: $colorGaugeValue; + transition: transform $transitionTimeGauge; + z-index: 1; + } + + .c-gauge__curval { + fill: $colorGaugeMeterTextValue !important; + } + + [class*='limit'] { + position: absolute; + } + + &__limit-high { + background: $colorGaugeLimitHigh; + } + + &__limit-low { + background: $colorGaugeLimitLow; + } +} + +.c-meter { + .c-gauge--meter-vertical &, + .c-gauge--meter-vertical-inverted & { + &__range { + flex-direction: column; + min-width: min-content; + margin-right: $interiorMarginSm; + text-align: right; + } + + &__value { + // Filled area + $lrM: $marginGaugeMeterValue; + left: $lrM; + right: $lrM; + top: 0; + bottom: 0; + } + + [class*='limit'] { + left: 0; + right: 0; + } + } + + .c-gauge--meter-vertical & { + &__limit-low { + bottom: 0; + } + + &__limit-high { + top: 0; + } + } + + .c-gauge--meter-vertical-inverted & { + &__limit-low { + top: 0; + } + + &__limit-high { + bottom: 0; + } + + &__range__low { + order: 1; + } + + &__range__high { + order: 2; + } + } + + .c-gauge--meter-horizontal & { + flex-direction: column; + + &__range { + flex-direction: row; + min-height: min-content; + margin-top: $interiorMarginSm; + order: 2; + + &__high { + order: 2; + } + + &__low { + order: 1; + } + } + + &__bg { + order: 1; + } + + &__value { + // Filled area + $m: $marginGaugeMeterValue; + top: $m; + bottom: $m; + left: 0; + right: 0; + } + + [class*='limit'] { + top: 0; + bottom: 0; + } + + &__limit-low { + left: 0; + } + + &__limit-high { + right: 0; + } + } +} diff --git a/src/plugins/imagery/components/Compass/compass.scss b/src/plugins/imagery/components/Compass/compass.scss index dba4106d2b..5609bf9f60 100644 --- a/src/plugins/imagery/components/Compass/compass.scss +++ b/src/plugins/imagery/components/Compass/compass.scss @@ -14,7 +14,7 @@ $elemBg: rgba(black, 0.7); position: absolute; left: 0; top: 0; - z-index: 1; + z-index: 2; @include userSelectNone; } diff --git a/src/plugins/imagery/components/imagery-view.scss b/src/plugins/imagery/components/imagery-view.scss index 911301dc90..26f9f5fd0a 100644 --- a/src/plugins/imagery/components/imagery-view.scss +++ b/src/plugins/imagery/components/imagery-view.scss @@ -222,7 +222,7 @@ .h-local-controls--overlay-content { position: absolute; left: $interiorMargin; top: $interiorMargin; - z-index: 2; + z-index: 70; background: $colorLocalControlOvrBg; border-radius: $basicCr; max-width: 250px; diff --git a/src/plugins/plugins.js b/src/plugins/plugins.js index 1852b39461..6b20e3c318 100644 --- a/src/plugins/plugins.js +++ b/src/plugins/plugins.js @@ -77,6 +77,7 @@ define([ './userIndicator/plugin', '../../example/exampleUser/plugin', './localStorage/plugin', + './gauge/GaugePlugin', './timelist/plugin' ], function ( _, @@ -135,6 +136,7 @@ define([ UserIndicator, ExampleUser, LocalStorage, + GaugePlugin, TimeList ) { const plugins = {}; @@ -212,6 +214,7 @@ define([ plugins.DeviceClassifier = DeviceClassifier.default; plugins.UserIndicator = UserIndicator.default; plugins.LocalStorage = LocalStorage.default; + plugins.Gauge = GaugePlugin.default; plugins.Timelist = TimeList.default; return plugins; diff --git a/src/styles/_constants-espresso.scss b/src/styles/_constants-espresso.scss index bc9abb1ad8..1aac3734bc 100644 --- a/src/styles/_constants-espresso.scss +++ b/src/styles/_constants-espresso.scss @@ -366,6 +366,16 @@ $legendHoverValueBg: rgba($colorBodyFg, 0.2); $legendTableHeadBg: $colorTabHeaderBg; $colorPlotLimitLineBg: rgba($colorBodyBg, 0.2); +// Gauges +$colorGaugeBg: pullForward($colorBodyBg, 5%); // Gauge radial area background, meter background +$colorGaugeValue: rgba(#fff, 0.3); // Gauge value graphic (radial sweep, bar) color +$colorGaugeTextValue: #fff; // Radial gauge text value +$colorGaugeMeterTextValue: $colorGaugeTextValue; // Meter text value, overlaid on value bar +$colorGaugeRange: $colorBodyFg; // Range text color +$colorGaugeLimitHigh: rgba($colorLimitRedBg, 0.4); +$colorGaugeLimitLow: $colorGaugeLimitHigh; +$transitionTimeGauge: 150ms; // CSS transition time used to smooth needle gauge and meter value transitions +$marginGaugeMeterValue: 10%; // Margin between meter value bar and bounds edges // Time Strip and Lists $colorCurrentBg: rgba($colorStatusAlert, 0.3); $colorCurrentFg: pullForward($colorBodyFg, 20%); diff --git a/src/styles/_constants-maelstrom.scss b/src/styles/_constants-maelstrom.scss index ef3634c459..5102393335 100644 --- a/src/styles/_constants-maelstrom.scss +++ b/src/styles/_constants-maelstrom.scss @@ -370,6 +370,16 @@ $legendHoverValueBg: rgba($colorBodyFg, 0.2); $legendTableHeadBg: rgba($colorBodyFg, 0.15); $colorPlotLimitLineBg: rgba($colorBodyBg, 0.2); +// Gauges +$colorGaugeBg: pullForward($colorBodyBg, 5%); // Gauge radial area background, meter background +$colorGaugeValue: rgba(#fff, 0.3); // Gauge value graphic (radial sweep, bar) color +$colorGaugeTextValue: #fff; // Radial gauge text value +$colorGaugeMeterTextValue: $colorGaugeTextValue; // Meter text value, overlaid on value bar +$colorGaugeRange: $colorBodyFg; // Range text color +$colorGaugeLimitHigh: rgba($colorLimitRedBg, 0.4); +$colorGaugeLimitLow: $colorGaugeLimitHigh; +$transitionTimeGauge: 150ms; // CSS transition time used to smooth needle gauge and meter value transitions +$marginGaugeMeterValue: 10%; // Margin between meter value bar and bounds edges // Time Strip and Lists $colorCurrentBg: rgba($colorStatusAlert, 0.3); $colorCurrentFg: pullForward($colorBodyFg, 20%); diff --git a/src/styles/_constants-snow.scss b/src/styles/_constants-snow.scss index 4d95e607a1..255a637480 100644 --- a/src/styles/_constants-snow.scss +++ b/src/styles/_constants-snow.scss @@ -366,6 +366,16 @@ $legendHoverValueBg: rgba($colorBodyFg, 0.2); $legendTableHeadBg: rgba($colorBodyFg, 0.15); $colorPlotLimitLineBg: rgba($colorBodyBg, 0.4); +// Gauges +$colorGaugeBg: pullForward($colorBodyBg, 20%); // Gauge radial area background, meter background +$colorGaugeValue: rgba(#000, 0.3); // Gauge value graphic (radial sweep, bar) color +$colorGaugeTextValue: pullForward($colorBodyFg, 20%); // Radial gauge text value +$colorGaugeMeterTextValue: $colorGaugeTextValue; // Meter text value, overlaid on value bar +$colorGaugeRange: $colorBodyFg; // Range text color +$colorGaugeLimitHigh: rgba($colorLimitRedBg, 0.2); +$colorGaugeLimitLow: $colorGaugeLimitHigh; +$transitionTimeGauge: 150ms; // CSS transition time used to smooth needle gauge and meter value transitions +$marginGaugeMeterValue: 10%; // Margin between meter value bar and bounds edges // Time Strip and Lists $colorCurrentBg: rgba($colorStatusAlert, 0.3); $colorCurrentFg: pullForward($colorBodyFg, 20%); diff --git a/src/styles/_forms.scss b/src/styles/_forms.scss index 1528be6d5b..a0017a5cb4 100644 --- a/src/styles/_forms.scss +++ b/src/styles/_forms.scss @@ -70,8 +70,24 @@ padding: $formTBPad $formLRPad; text-transform: uppercase; } + + &--sub-grid { + // 3 columns: