Compare commits
	
		
			1 Commits
		
	
	
		
			vulnerabil
			...
			bleeding-n
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 3b7d4e8d0c | 
| @@ -47,7 +47,7 @@ test.use({ | ||||
| }); | ||||
|  | ||||
| test.describe('ExportAsJSON', () => { | ||||
|     test('autoscale off causes no error from undefined user range', async ({ page }) => { | ||||
|     test.only('autoscale off causes no error from undefined user range', async ({ page }) => { | ||||
|         await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|         await setTimeRange(page); | ||||
| @@ -102,7 +102,7 @@ test.describe('ExportAsJSON', () => { | ||||
|             testYTicks(page, ['0.00', '0.50', '1.00', '1.50', '2.00']), | ||||
|             new Promise(r => setTimeout(r, 100)) | ||||
|                 .then(() => canvas.screenshot()) | ||||
|                 .then(shot => expect(shot).toMatchSnapshot('autoscale-canvas-panned.png', { maxDiffPixels: 40 })) | ||||
|                 .then(shot => expect(shot).toMatchSnapshot('autoscale-canvas-panned.png', { maxDiffPixels: 20 })) | ||||
|         ]); | ||||
|     }); | ||||
| }); | ||||
|   | ||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 16 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 26 KiB | 
| @@ -1,279 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * 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. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* | ||||
| Tests to verify log plot functionality. | ||||
| */ | ||||
|  | ||||
| const { test, expect } = require('@playwright/test'); | ||||
|  | ||||
| test.describe('Log plot tests', () => { | ||||
|     test.only('Can create a log plot.', async ({ page }) => { | ||||
|         await makeOverlayPlot(page); | ||||
|         await testRegularTicks(page); | ||||
|         await enableEditMode(page); | ||||
|         await enableLogMode(page); | ||||
|         await testLogTicks(page); | ||||
|         await disableLogMode(page); | ||||
|         await testRegularTicks(page); | ||||
|         await enableLogMode(page); | ||||
|         await testLogTicks(page); | ||||
|         await saveOverlayPlot(page); | ||||
|         await testLogTicks(page); | ||||
|         await testLogPlotPixels(page); | ||||
|  | ||||
|         // refresh page | ||||
|         await page.reload(); | ||||
|  | ||||
|         // test log ticks hold up after refresh | ||||
|         await testLogTicks(page); | ||||
|         await testLogPlotPixels(page); | ||||
|     }); | ||||
|  | ||||
|     test.only('Verify that log mode option is reflected in import/export JSON', async ({ page }) => { | ||||
|         await makeOverlayPlot(page); | ||||
|         await enableEditMode(page); | ||||
|         await enableLogMode(page); | ||||
|         await saveOverlayPlot(page); | ||||
|  | ||||
|         // TODO ...export, delete the overlay, then import it... | ||||
|  | ||||
|         await testLogTicks(page); | ||||
|  | ||||
|         // TODO, the plot is slightly at different position that in the other test, so this fails. | ||||
|         // ...We can fix it by copying all steps from the first test... | ||||
|         // await testLogPlotPixels(page); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| /** | ||||
|  * Makes an overlay plot with a sine wave generator and clicks on the overlay plot in the sidebar so it is the active thing displayed. | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function makeOverlayPlot(page) { | ||||
|     // fresh page with time range from 2022-03-29 22:00:00.000Z to 2022-03-29 22:00:30.000Z | ||||
|     await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|     // Set a specific time range for consistency, otherwise it will change | ||||
|     // on every test to a range based on the current time. | ||||
|  | ||||
|     const timeInputs = page.locator('input.c-input--datetime'); | ||||
|     await timeInputs.first().click(); | ||||
|     await timeInputs.first().fill('2022-03-29 22:00:00.000Z'); | ||||
|  | ||||
|     await timeInputs.nth(1).click(); | ||||
|     await timeInputs.nth(1).fill('2022-03-29 22:00:30.000Z'); | ||||
|  | ||||
|     // create overlay plot | ||||
|  | ||||
|     await page.locator('button.c-create-button').click(); | ||||
|     await page.locator('li:has-text("Overlay Plot")').click(); | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/8caf7072-535b-4af6-8394-edd86e3ea35f?tc.mode=fixed&tc.startBound=1648590633191&tc.endBound=1648592433191&tc.timeSystem=utc&view=plot-overlay' }*/), | ||||
|         page.locator('text=OK').click() | ||||
|     ]); | ||||
|  | ||||
|     // save the overlay plot | ||||
|  | ||||
|     await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click(); | ||||
|     await page.locator('text=Save and Finish Editing').click(); | ||||
|  | ||||
|     // create a sinewave generator | ||||
|  | ||||
|     await page.locator('button.c-create-button').click(); | ||||
|     await page.locator('li:has-text("Sine Wave Generator")').click(); | ||||
|  | ||||
|     // set amplitude to 6, offset 4, period 2 | ||||
|  | ||||
|     await page.locator('div:nth-child(5) .form-row .c-form-row__controls .form-control .field input').click(); | ||||
|     await page.locator('div:nth-child(5) .form-row .c-form-row__controls .form-control .field input').fill('6'); | ||||
|  | ||||
|     await page.locator('div:nth-child(6) .form-row .c-form-row__controls .form-control .field input').click(); | ||||
|     await page.locator('div:nth-child(6) .form-row .c-form-row__controls .form-control .field input').fill('4'); | ||||
|  | ||||
|     await page.locator('div:nth-child(7) .form-row .c-form-row__controls .form-control .field input').click(); | ||||
|     await page.locator('div:nth-child(7) .form-row .c-form-row__controls .form-control .field input').fill('2'); | ||||
|  | ||||
|     // Click OK to make generator | ||||
|  | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/8caf7072-535b-4af6-8394-edd86e3ea35f/6e58b26a-8a73-4df6-b3a6-918decc0bbfa?tc.mode=fixed&tc.startBound=1648590633191&tc.endBound=1648592433191&tc.timeSystem=utc&view=plot-single' }*/), | ||||
|         page.locator('text=OK').click() | ||||
|     ]); | ||||
|  | ||||
|     // click on overlay plot | ||||
|  | ||||
|     await page.locator('text=Open MCT My Items >> span').nth(3).click(); | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/8caf7072-535b-4af6-8394-edd86e3ea35f?tc.mode=fixed&tc.startBound=1648590633191&tc.endBound=1648592433191&tc.timeSystem=utc&view=plot-overlay' }*/), | ||||
|         page.locator('text=Unnamed Overlay Plot').first().click() | ||||
|     ]); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function testRegularTicks(page) { | ||||
|     const yTicks = page.locator('.gl-plot-y-tick-label'); | ||||
|     expect(await yTicks.count()).toBe(7); | ||||
|     await expect(yTicks.nth(0)).toHaveText('-2'); | ||||
|     await expect(yTicks.nth(1)).toHaveText('0'); | ||||
|     await expect(yTicks.nth(2)).toHaveText('2'); | ||||
|     await expect(yTicks.nth(3)).toHaveText('4'); | ||||
|     await expect(yTicks.nth(4)).toHaveText('6'); | ||||
|     await expect(yTicks.nth(5)).toHaveText('8'); | ||||
|     await expect(yTicks.nth(6)).toHaveText('10'); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function testLogTicks(page) { | ||||
|     const yTicks = page.locator('.gl-plot-y-tick-label'); | ||||
|     expect(await yTicks.count()).toBe(28); | ||||
|     await expect(yTicks.nth(0)).toHaveText('-2.98'); | ||||
|     await expect(yTicks.nth(1)).toHaveText('-2.50'); | ||||
|     await expect(yTicks.nth(2)).toHaveText('-2.00'); | ||||
|     await expect(yTicks.nth(3)).toHaveText('-1.51'); | ||||
|     await expect(yTicks.nth(4)).toHaveText('-1.20'); | ||||
|     await expect(yTicks.nth(5)).toHaveText('-1.00'); | ||||
|     await expect(yTicks.nth(6)).toHaveText('-0.80'); | ||||
|     await expect(yTicks.nth(7)).toHaveText('-0.58'); | ||||
|     await expect(yTicks.nth(8)).toHaveText('-0.40'); | ||||
|     await expect(yTicks.nth(9)).toHaveText('-0.20'); | ||||
|     await expect(yTicks.nth(10)).toHaveText('-0.00'); | ||||
|     await expect(yTicks.nth(11)).toHaveText('0.20'); | ||||
|     await expect(yTicks.nth(12)).toHaveText('0.40'); | ||||
|     await expect(yTicks.nth(13)).toHaveText('0.58'); | ||||
|     await expect(yTicks.nth(14)).toHaveText('0.80'); | ||||
|     await expect(yTicks.nth(15)).toHaveText('1.00'); | ||||
|     await expect(yTicks.nth(16)).toHaveText('1.20'); | ||||
|     await expect(yTicks.nth(17)).toHaveText('1.51'); | ||||
|     await expect(yTicks.nth(18)).toHaveText('2.00'); | ||||
|     await expect(yTicks.nth(19)).toHaveText('2.50'); | ||||
|     await expect(yTicks.nth(20)).toHaveText('2.98'); | ||||
|     await expect(yTicks.nth(21)).toHaveText('3.50'); | ||||
|     await expect(yTicks.nth(22)).toHaveText('4.00'); | ||||
|     await expect(yTicks.nth(23)).toHaveText('4.50'); | ||||
|     await expect(yTicks.nth(24)).toHaveText('5.31'); | ||||
|     await expect(yTicks.nth(25)).toHaveText('7.00'); | ||||
|     await expect(yTicks.nth(26)).toHaveText('8.00'); | ||||
|     await expect(yTicks.nth(27)).toHaveText('9.00'); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function enableEditMode(page) { | ||||
|     // turn on edit mode | ||||
|     await page.locator('text=Unnamed Overlay Plot Snapshot >> button').nth(3).click(); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function enableLogMode(page) { | ||||
|     // turn on log mode | ||||
|     await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"]').first().check(); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function disableLogMode(page) { | ||||
|     // turn off log mode | ||||
|     await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"]').first().uncheck(); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function saveOverlayPlot(page) { | ||||
|     // save overlay plot | ||||
|     await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click(); | ||||
|     await page.locator('text=Save and Finish Editing').click(); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function testLogPlotPixels(page) { | ||||
|     const pixelsMatch = await page.evaluate(async () => { | ||||
|         // TODO get canvas pixels at a few locations to make sure they're the correct color, to test that the plot comes out as expected. | ||||
|  | ||||
|         await new Promise((r) => setTimeout(r, 50)); | ||||
|  | ||||
|         // These are some pixels that should be blue points in the log plot. | ||||
|         // If the plot changes shape to an unexpected shape, this will | ||||
|         // likely fail, which is what we want. | ||||
|         // | ||||
|         // I found these pixels by pausing playwright in debug mode at this | ||||
|         // point, and using similar code as below to output the pixel data, then | ||||
|         // I logged those pixels here. | ||||
|         const expectedBluePixels = [ | ||||
|             // TODO these pixel sets only work with the first test, but not the second test. | ||||
|  | ||||
|             // [60, 35], | ||||
|             // [121, 125], | ||||
|             // [156, 377], | ||||
|             // [264, 73], | ||||
|             // [372, 186], | ||||
|             // [576, 73], | ||||
|             // [659, 439], | ||||
|             // [675, 423] | ||||
|  | ||||
|             [60, 35], | ||||
|             [120, 125], | ||||
|             [156, 375], | ||||
|             [264, 73], | ||||
|             [372, 185], | ||||
|             [575, 72], | ||||
|             [659, 437], | ||||
|             [675, 421] | ||||
|         ]; | ||||
|  | ||||
|         // The first canvas in the DOM is the one that has the plot point | ||||
|         // icons (canvas 2d), which is the one we are testing. The second | ||||
|         // one in the DOM is the WebGL canvas with the line. (Why aren't | ||||
|         // they both WebGL?) | ||||
|         const canvas = document.querySelector('canvas'); | ||||
|  | ||||
|         const ctx = canvas.getContext('2d'); | ||||
|  | ||||
|         for (const pixel of expectedBluePixels) { | ||||
|             // XXX Possible optimization: call getImageData only once with | ||||
|             // area including all pixels to be tested. | ||||
|             const data = ctx.getImageData(pixel[0], pixel[1], 1, 1).data; | ||||
|  | ||||
|             // #43b0ffff <-- openmct cyanish-blue with 100% opacity | ||||
|             // if (data[0] !== 0x43 || data[1] !== 0xb0 || data[2] !== 0xff || data[3] !== 0xff) { | ||||
|             if (data[0] === 0 && data[1] === 0 && data[2] === 0 && data[3] === 0) { | ||||
|                 // If any pixel is empty, it means we didn't hit a plot point. | ||||
|                 return false; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return true; | ||||
|     }); | ||||
|  | ||||
|     expect(pixelsMatch).toBe(true); | ||||
| } | ||||
| @@ -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()); | ||||
|   | ||||
| @@ -94,7 +94,6 @@ | ||||
|     "test:debug": "cross-env NODE_ENV=debug karma start --no-single-run", | ||||
|     "test:e2e:ci": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome smoke default condition timeConductor", | ||||
|     "test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome", | ||||
|     "test:e2e:debug": "npm run test:e2e:local -- --debug", | ||||
|     "test:e2e:visual": "percy exec --config ./e2e/.percy.yml -- npx playwright test --config=e2e/playwright-visual.config.js default", | ||||
|     "test:e2e:full": "npx playwright test --config=e2e/playwright-ci.config.js", | ||||
|     "test:watch": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --no-single-run", | ||||
|   | ||||
| @@ -242,7 +242,6 @@ 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()); | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| 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'; | ||||
| @@ -8,13 +7,11 @@ 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, | ||||
| @@ -22,8 +19,7 @@ export const DEFAULT_CONTROLS_MAP = { | ||||
|     'numberfield': NumberField, | ||||
|     'select': SelectField, | ||||
|     'textarea': TextAreaField, | ||||
|     'textfield': TextField, | ||||
|     'toggleSwitch': ToggleSwitchField | ||||
|     'textfield': TextField | ||||
| }; | ||||
|  | ||||
| export default class FormControl { | ||||
| @@ -98,3 +94,4 @@ export default class FormControl { | ||||
|         }; | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -79,12 +79,10 @@ export default { | ||||
|         rowClass() { | ||||
|             let cssClass = this.cssClass; | ||||
|  | ||||
|             if (!this.row.required) { | ||||
|                 return; | ||||
|             if (this.row.required) { | ||||
|                 cssClass = `${cssClass} req`; | ||||
|             } | ||||
|  | ||||
|             cssClass = `${cssClass} req`; | ||||
|  | ||||
|             if (this.visited && this.valid !== undefined) { | ||||
|                 if (this.valid === true) { | ||||
|                     cssClass = `${cssClass} valid`; | ||||
|   | ||||
| @@ -1,55 +0,0 @@ | ||||
| /***************************************************************************** | ||||
| * 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. | ||||
| *****************************************************************************/ | ||||
|  | ||||
| <template> | ||||
| <span class="form-control shell"> | ||||
|     <span | ||||
|         class="field control" | ||||
|         :class="model.cssClass" | ||||
|     > | ||||
|         <input | ||||
|             type="checkbox" | ||||
|             :checked="isChecked" | ||||
|             @input="toggleCheckBox" | ||||
|         > | ||||
|     </span> | ||||
| </span> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import toggleMixin from '../../toggle-check-box-mixin'; | ||||
|  | ||||
| export default { | ||||
|     mixins: [toggleMixin], | ||||
|     props: { | ||||
|         model: { | ||||
|             type: Object, | ||||
|             required: true | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             isChecked: this.model.value | ||||
|         }; | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
| @@ -58,6 +58,7 @@ export default { | ||||
|     }, | ||||
|     methods: { | ||||
|         updateText() { | ||||
|             console.log('updateText', this.field); | ||||
|             const data = { | ||||
|                 model: this.model, | ||||
|                 value: this.field | ||||
|   | ||||
| @@ -1,62 +0,0 @@ | ||||
| /***************************************************************************** | ||||
| * 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. | ||||
| *****************************************************************************/ | ||||
|  | ||||
| <template> | ||||
| <span class="form-control shell"> | ||||
|     <span | ||||
|         class="field control" | ||||
|         :class="model.cssClass" | ||||
|     > | ||||
|         <ToggleSwitch | ||||
|             id="switchId" | ||||
|             :checked="isChecked" | ||||
|             @change="toggleCheckBox" | ||||
|         /> | ||||
|     </span> | ||||
| </span> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import toggleMixin from '../../toggle-check-box-mixin'; | ||||
| import ToggleSwitch from '@/ui/components/ToggleSwitch.vue'; | ||||
|  | ||||
| import uuid from 'uuid'; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|         ToggleSwitch | ||||
|     }, | ||||
|     mixins: [toggleMixin], | ||||
|     props: { | ||||
|         model: { | ||||
|             type: Object, | ||||
|             required: true | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             switchId: `toggleSwitch-${uuid}`, | ||||
|             isChecked: this.model.value | ||||
|         }; | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
| @@ -1,19 +0,0 @@ | ||||
| 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); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| @@ -36,14 +36,13 @@ class InMemorySearchProvider { | ||||
|          */ | ||||
|         this.MAX_CONCURRENT_REQUESTS = 100; | ||||
|         /** | ||||
|          * If max results is not specified in query, use this as default. | ||||
|          */ | ||||
|         * If max results is not specified in query, use this as default. | ||||
|         */ | ||||
|         this.DEFAULT_MAX_RESULTS = 100; | ||||
|  | ||||
|         this.openmct = openmct; | ||||
|  | ||||
|         this.indexedIds = {}; | ||||
|         this.indexedCompositions = {}; | ||||
|         this.idsToIndex = []; | ||||
|         this.pendingIndex = {}; | ||||
|         this.pendingRequests = 0; | ||||
| @@ -59,6 +58,7 @@ class InMemorySearchProvider { | ||||
|         this.onWorkerMessageError = this.onWorkerMessageError.bind(this); | ||||
|         this.onerror = this.onWorkerError.bind(this); | ||||
|         this.startIndexing = this.startIndexing.bind(this); | ||||
|         this.onMutationOfIndexedObject = this.onMutationOfIndexedObject.bind(this); | ||||
|  | ||||
|         this.openmct.on('start', this.startIndexing); | ||||
|         this.openmct.on('destroy', () => { | ||||
| @@ -68,9 +68,6 @@ class InMemorySearchProvider { | ||||
|                 this.worker.port.onmessageerror = null; | ||||
|                 this.worker.port.close(); | ||||
|             } | ||||
|  | ||||
|             this.destroyObservers(this.indexedIds); | ||||
|             this.destroyObservers(this.indexedCompositions); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
| @@ -140,7 +137,7 @@ class InMemorySearchProvider { | ||||
|         }; | ||||
|         modelResults.hits = await Promise.all(event.data.results.map(async (hit) => { | ||||
|             const identifier = this.openmct.objects.parseKeyString(hit.keyString); | ||||
|             const domainObject = await this.openmct.objects.get(identifier); | ||||
|             const domainObject = await this.openmct.objects.get(identifier.key); | ||||
|  | ||||
|             return domainObject; | ||||
|         })); | ||||
| @@ -216,52 +213,29 @@ class InMemorySearchProvider { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     onNameMutation(domainObject, name) { | ||||
|     onMutationOfIndexedObject(domainObject) { | ||||
|         const provider = this; | ||||
|  | ||||
|         domainObject.name = name; | ||||
|         provider.index(domainObject); | ||||
|     } | ||||
|  | ||||
|     onCompositionMutation(domainObject, composition) { | ||||
|         const provider = this; | ||||
|         const indexedComposition = domainObject.composition; | ||||
|         const identifiersToIndex = composition | ||||
|             .filter(identifier => !indexedComposition | ||||
|                 .some(indexedIdentifier => this.openmct.objects | ||||
|                     .areIdsEqual([identifier, indexedIdentifier]))); | ||||
|  | ||||
|         identifiersToIndex.forEach(identifier => { | ||||
|             this.openmct.objects.get(identifier).then(objectToIndex => provider.index(objectToIndex)); | ||||
|         }); | ||||
|         provider.index(domainObject.identifier, domainObject); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Pass a domainObject to the worker to be indexed. | ||||
|      * If the object has composition, schedule those ids for later indexing. | ||||
|      * Watch for object changes and re-index object and children if so | ||||
|      * Pass an id and model to the worker to be indexed.  If the model has | ||||
|      * composition, schedule those ids for later indexing. | ||||
|      * | ||||
|      * @private | ||||
|      * @param domainObject a domainObject | ||||
|      * @param id a model id | ||||
|      * @param model a model | ||||
|      */ | ||||
|     async index(domainObject) { | ||||
|     async index(id, domainObject) { | ||||
|         const provider = this; | ||||
|         const keyString = this.openmct.objects.makeKeyString(domainObject.identifier); | ||||
|  | ||||
|         const keyString = this.openmct.objects.makeKeyString(id); | ||||
|         if (!this.indexedIds[keyString]) { | ||||
|             this.indexedIds[keyString] = this.openmct.objects.observe( | ||||
|                 domainObject, | ||||
|                 'name', | ||||
|                 this.onNameMutation.bind(this, domainObject) | ||||
|             ); | ||||
|             this.indexedCompositions[keyString] = this.openmct.objects.observe( | ||||
|                 domainObject, | ||||
|                 'composition', | ||||
|                 this.onCompositionMutation.bind(this, domainObject) | ||||
|             ); | ||||
|             this.openmct.objects.observe(domainObject, `*`, this.onMutationOfIndexedObject); | ||||
|         } | ||||
|  | ||||
|         if ((keyString !== 'ROOT')) { | ||||
|         this.indexedIds[keyString] = true; | ||||
|  | ||||
|         if ((id.key !== 'ROOT')) { | ||||
|             if (this.worker) { | ||||
|                 this.worker.port.postMessage({ | ||||
|                     request: 'index', | ||||
| @@ -273,12 +247,15 @@ class InMemorySearchProvider { | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         const composition = this.openmct.composition.get(domainObject); | ||||
|         const composition = this.openmct.composition.registry.find(foundComposition => { | ||||
|             return foundComposition.appliesTo(domainObject); | ||||
|         }); | ||||
|  | ||||
|         if (composition !== undefined) { | ||||
|             const children = await composition.load(); | ||||
|  | ||||
|             children.forEach(child => provider.scheduleForIndexing(child.identifier)); | ||||
|         if (composition) { | ||||
|             const childIdentifiers = await composition.load(domainObject); | ||||
|             childIdentifiers.forEach(function (childIdentifier) { | ||||
|                 provider.scheduleForIndexing(childIdentifier); | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -294,12 +271,12 @@ class InMemorySearchProvider { | ||||
|         const provider = this; | ||||
|  | ||||
|         this.pendingRequests += 1; | ||||
|         const domainObject = await this.openmct.objects.get(keyString); | ||||
|         const identifier = await this.openmct.objects.parseKeyString(keyString); | ||||
|         const domainObject = await this.openmct.objects.get(identifier.key); | ||||
|         delete provider.pendingIndex[keyString]; | ||||
|  | ||||
|         try { | ||||
|             if (domainObject) { | ||||
|                 await provider.index(domainObject); | ||||
|                 await provider.index(identifier, domainObject); | ||||
|             } | ||||
|         } catch (error) { | ||||
|             console.warn('Failed to index domain object ' + keyString, error); | ||||
| @@ -328,9 +305,9 @@ class InMemorySearchProvider { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * A local version of the same SharedWorker function | ||||
|      * if we don't have SharedWorkers available (e.g., iOS) | ||||
|      */ | ||||
|     * A local version of the same SharedWorker function | ||||
|     * if we don't have SharedWorkers available (e.g., iOS) | ||||
|     */ | ||||
|     localIndexItem(keyString, model) { | ||||
|         this.localIndexedItems[keyString] = { | ||||
|             type: model.type, | ||||
| @@ -370,16 +347,6 @@ class InMemorySearchProvider { | ||||
|         }; | ||||
|         this.onWorkerMessage(eventToReturn); | ||||
|     } | ||||
|  | ||||
|     destroyObservers(observers) { | ||||
|         Object.entries(observers).forEach(([keyString, unobserve]) => { | ||||
|             if (typeof unobserve === 'function') { | ||||
|                 unobserve(); | ||||
|             } | ||||
|  | ||||
|             delete observers[keyString]; | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default InMemorySearchProvider; | ||||
|   | ||||
| @@ -105,18 +105,13 @@ describe("The Object API Search Function", () => { | ||||
|  | ||||
|         beforeEach((done) => { | ||||
|             openmct = createOpenMct(); | ||||
|             const defaultObjectProvider = openmct.objects.getProvider({ | ||||
|                 key: '', | ||||
|                 namespace: '' | ||||
|             }); | ||||
|             openmct.objects.addProvider('foo', defaultObjectProvider); | ||||
|             spyOn(openmct.objects.inMemorySearchProvider, "query").and.callThrough(); | ||||
|             spyOn(openmct.objects.inMemorySearchProvider, "localSearch").and.callThrough(); | ||||
|  | ||||
|             openmct.on('start', async () => { | ||||
|                 mockIdentifier1 = { | ||||
|                     key: 'some-object', | ||||
|                     namespace: 'foo' | ||||
|                     namespace: 'some-namespace' | ||||
|                 }; | ||||
|                 mockDomainObject1 = { | ||||
|                     type: 'clock', | ||||
| @@ -125,7 +120,7 @@ describe("The Object API Search Function", () => { | ||||
|                 }; | ||||
|                 mockIdentifier2 = { | ||||
|                     key: 'some-other-object', | ||||
|                     namespace: 'foo' | ||||
|                     namespace: 'some-namespace' | ||||
|                 }; | ||||
|                 mockDomainObject2 = { | ||||
|                     type: 'clock', | ||||
| @@ -134,16 +129,16 @@ describe("The Object API Search Function", () => { | ||||
|                 }; | ||||
|                 mockIdentifier3 = { | ||||
|                     key: 'yet-another-object', | ||||
|                     namespace: 'foo' | ||||
|                     namespace: 'some-namespace' | ||||
|                 }; | ||||
|                 mockDomainObject3 = { | ||||
|                     type: 'clock', | ||||
|                     name: 'redBear', | ||||
|                     identifier: mockIdentifier3 | ||||
|                 }; | ||||
|                 await openmct.objects.inMemorySearchProvider.index(mockDomainObject1); | ||||
|                 await openmct.objects.inMemorySearchProvider.index(mockDomainObject2); | ||||
|                 await openmct.objects.inMemorySearchProvider.index(mockDomainObject3); | ||||
|                 await openmct.objects.inMemorySearchProvider.index(mockIdentifier1, mockDomainObject1); | ||||
|                 await openmct.objects.inMemorySearchProvider.index(mockIdentifier2, mockDomainObject2); | ||||
|                 await openmct.objects.inMemorySearchProvider.index(mockIdentifier3, mockDomainObject3); | ||||
|                 done(); | ||||
|             }); | ||||
|             openmct.startHeadless(); | ||||
| @@ -180,9 +175,9 @@ describe("The Object API Search Function", () => { | ||||
|             beforeEach(async () => { | ||||
|                 openmct.objects.inMemorySearchProvider.worker = null; | ||||
|                 // reindex locally | ||||
|                 await openmct.objects.inMemorySearchProvider.index(mockDomainObject1); | ||||
|                 await openmct.objects.inMemorySearchProvider.index(mockDomainObject2); | ||||
|                 await openmct.objects.inMemorySearchProvider.index(mockDomainObject3); | ||||
|                 await openmct.objects.inMemorySearchProvider.index(mockIdentifier1, mockDomainObject1); | ||||
|                 await openmct.objects.inMemorySearchProvider.index(mockIdentifier2, mockDomainObject2); | ||||
|                 await openmct.objects.inMemorySearchProvider.index(mockIdentifier3, mockDomainObject3); | ||||
|             }); | ||||
|             it("calls local search", () => { | ||||
|                 openmct.objects.search('foo'); | ||||
|   | ||||
| @@ -172,7 +172,6 @@ export class TelemetryCollection extends EventEmitter { | ||||
|      * @private | ||||
|      */ | ||||
|     _processNewTelemetry(telemetryData) { | ||||
|         performance.mark('tlm:process:start'); | ||||
|         if (telemetryData === undefined) { | ||||
|             return; | ||||
|         } | ||||
| @@ -185,8 +184,8 @@ export class TelemetryCollection extends EventEmitter { | ||||
|  | ||||
|         for (let datum of data) { | ||||
|             parsedValue = this.parseTime(datum); | ||||
|             beforeStartOfBounds = parsedValue <= this.lastBounds.start; | ||||
|             afterEndOfBounds = parsedValue >= this.lastBounds.end; | ||||
|             beforeStartOfBounds = parsedValue < this.lastBounds.start; | ||||
|             afterEndOfBounds = parsedValue > this.lastBounds.end; | ||||
|  | ||||
|             if (!afterEndOfBounds && !beforeStartOfBounds) { | ||||
|                 let isDuplicate = false; | ||||
| @@ -353,7 +352,6 @@ export class TelemetryCollection extends EventEmitter { | ||||
|      * @todo handle subscriptions more granually | ||||
|      */ | ||||
|     _reset() { | ||||
|         performance.mark('tlm:reset'); | ||||
|         this.boundedTelemetry = []; | ||||
|         this.futureBuffer = []; | ||||
|  | ||||
|   | ||||
| @@ -57,7 +57,7 @@ | ||||
|             /> | ||||
|  | ||||
|             <drop-hint | ||||
|                 :key="'hint-' + i" | ||||
|                 :key="i" | ||||
|                 class="c-fl-frame__drop-hint" | ||||
|                 :index="i" | ||||
|                 :allow-drop="allowDrop" | ||||
| @@ -66,7 +66,7 @@ | ||||
|  | ||||
|             <resize-handle | ||||
|                 v-if="(i !== frames.length - 1)" | ||||
|                 :key="'handle-' + i" | ||||
|                 :key="i" | ||||
|                 :index="i" | ||||
|                 :orientation="rowsLayout ? 'horizontal' : 'vertical'" | ||||
|                 :is-editing="isEditing" | ||||
|   | ||||
| @@ -90,9 +90,6 @@ 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) | ||||
|   | ||||
| @@ -51,29 +51,41 @@ export default class EditPropertiesAction extends PropertiesAction { | ||||
|     /** | ||||
|      * @private | ||||
|      */ | ||||
|     _onSave(changes) { | ||||
|         try { | ||||
|             Object.entries(changes).forEach(([key, value]) => { | ||||
|                 const properties = key.split('.'); | ||||
|                 let object = this.domainObject; | ||||
|                 const propertiesLength = properties.length; | ||||
|                 properties.forEach((property, index) => { | ||||
|                     const isComplexProperty = propertiesLength > 1 && index !== propertiesLength - 1; | ||||
|                     if (isComplexProperty && object[property] !== null) { | ||||
|                         object = object[property]; | ||||
|                     } else { | ||||
|                         object[property] = value; | ||||
|                     } | ||||
|                 }); | ||||
|  | ||||
|                 object = value; | ||||
|                 this.openmct.objects.mutate(this.domainObject, key, value); | ||||
|                 this.openmct.notifications.info('Save successful'); | ||||
|     async _onSave(changes) { | ||||
|         Object.entries(changes).forEach(([key, value]) => { | ||||
|             const properties = key.split('.'); | ||||
|             let object = this.domainObject; | ||||
|             const propertiesLength = properties.length; | ||||
|             properties.forEach((property, index) => { | ||||
|                 const isComplexProperty = propertiesLength > 1 && index !== propertiesLength - 1; | ||||
|                 if (isComplexProperty && object[property] !== null) { | ||||
|                     object = object[property]; | ||||
|                 } else { | ||||
|                     object[property] = value; | ||||
|                 } | ||||
|             }); | ||||
|         } catch (error) { | ||||
|  | ||||
|             object = value; | ||||
|         }); | ||||
|  | ||||
|         this.domainObject.modified = Date.now(); | ||||
|  | ||||
|         // Show saving progress dialog | ||||
|         let dialog = this.openmct.overlays.progressDialog({ | ||||
|             progressPerc: 'unknown', | ||||
|             message: 'Do not navigate away from this page or close this browser tab while this message is displayed.', | ||||
|             iconClass: 'info', | ||||
|             title: 'Saving' | ||||
|         }); | ||||
|  | ||||
|         const success = await this.openmct.objects.save(this.domainObject); | ||||
|         if (success) { | ||||
|             this.openmct.notifications.info('Save successful'); | ||||
|         } else { | ||||
|             this.openmct.notifications.error('Error saving objects'); | ||||
|             console.error(error); | ||||
|         } | ||||
|  | ||||
|         dialog.dismiss(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -1,199 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * 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: `<GaugeFormController :model="model" @onChange="onChange"></GaugeFormController>` | ||||
|                 }); | ||||
|  | ||||
|                 return rowComponent; | ||||
|             } | ||||
|         }; | ||||
|     } | ||||
| } | ||||
| @@ -1,805 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * 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); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
| @@ -1,67 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * 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) { | ||||
|             if (domainObject.type === 'gauge') { | ||||
|                 return true; | ||||
|             } | ||||
|         }, | ||||
|         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: '<gauge-component></gauge-component>' | ||||
|                     }); | ||||
|                 }, | ||||
|                 destroy: function (element) { | ||||
|                     component.$destroy(); | ||||
|                     component = undefined; | ||||
|                 } | ||||
|             }; | ||||
|         }, | ||||
|         priority: function () { | ||||
|             return 1; | ||||
|         } | ||||
|     }; | ||||
| } | ||||
| @@ -1,467 +0,0 @@ | ||||
| /***************************************************************************** | ||||
| * 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. | ||||
| *****************************************************************************/ | ||||
| <template> | ||||
| <div | ||||
|     class="c-gauge" | ||||
|     :class="`c-gauge--${gaugeType}`" | ||||
| > | ||||
|     <div class="c-gauge__wrapper"> | ||||
|         <template v-if="typeDial"> | ||||
|             <svg | ||||
|                 class="c-gauge__range" | ||||
|                 viewBox="0 0 512 512" | ||||
|             > | ||||
|                 <text | ||||
|                     v-if="displayMinMax" | ||||
|                     font-size="35" | ||||
|                     transform="translate(105 455) rotate(-45)" | ||||
|                 >{{ rangeLow }}</text> | ||||
|                 <text | ||||
|                     v-if="displayMinMax" | ||||
|                     font-size="35" | ||||
|                     transform="translate(407 455) rotate(45)" | ||||
|                     text-anchor="end" | ||||
|                 >{{ rangeHigh }}</text> | ||||
|             </svg> | ||||
|  | ||||
|             <svg | ||||
|                 v-if="displayCurVal" | ||||
|                 class="c-gauge__curval" | ||||
|                 :viewBox="curValViewBox" | ||||
|             > | ||||
|                 <text | ||||
|                     class="c-gauge__curval-text" | ||||
|                     lengthAdjust="spacing" | ||||
|                     text-anchor="middle" | ||||
|                 >{{ curVal }}</text> | ||||
|             </svg> | ||||
|  | ||||
|             <div class="c-dial"> | ||||
|                 <svg | ||||
|                     class="c-dial__bg" | ||||
|                     viewBox="0 0 512 512" | ||||
|                 > | ||||
|                     <path d="M256,0C114.6,0,0,114.6,0,256S114.6,512,256,512,512,397.4,512,256,397.4,0,256,0Zm0,412A156,156,0,1,1,412,256,155.9,155.9,0,0,1,256,412Z" /> | ||||
|                 </svg> | ||||
|  | ||||
|                 <svg | ||||
|                     v-if="limitHigh && dialHighLimitDeg < 270" | ||||
|                     class="c-dial__limit-high" | ||||
|                     viewBox="0 0 512 512" | ||||
|                     :class="{ | ||||
|                         'c-high-limit-clip--90': dialHighLimitDeg > 90, | ||||
|                         'c-high-limit-clip--180': dialHighLimitDeg >= 180 | ||||
|                     }" | ||||
|                 > | ||||
|                     <path | ||||
|                         d="M100,256A156,156,0,1,1,366.3,366.3L437,437a255.2,255.2,0,0,0,75-181C512,114.6,397.4,0,256,0S0,114.6,0,256A255.2,255.2,0,0,0,75,437l70.7-70.7A155.5,155.5,0,0,1,100,256Z" | ||||
|                         :style="`transform: rotate(${dialHighLimitDeg}deg)`" | ||||
|                     /> | ||||
|                 </svg> | ||||
|  | ||||
|                 <svg | ||||
|                     v-if="limitLow && dialLowLimitDeg < 270" | ||||
|                     class="c-dial__limit-low" | ||||
|                     viewBox="0 0 512 512" | ||||
|                     :class="{ | ||||
|                         'c-dial-clip--90': dialLowLimitDeg < 90, | ||||
|                         'c-dial-clip--180': dialLowLimitDeg >= 90 && dialLowLimitDeg < 180 | ||||
|                     }" | ||||
|                 > | ||||
|                     <path | ||||
|                         d="M256,100c86.2,0,156,69.8,156,156s-69.8,156-156,156c-43.1,0-82.1-17.5-110.3-45.7L75,437 c46.3,46.3,110.3,75,181,75c141.4,0,256-114.6,256-256S397.4,0,256,0C185.3,0,121.3,28.7,75,75l70.7,70.7 C173.9,117.5,212.9,100,256,100z" | ||||
|                         :style="`transform: rotate(${dialLowLimitDeg}deg)`" | ||||
|                     /> | ||||
|                 </svg> | ||||
|  | ||||
|                 <svg | ||||
|                     class="c-dial__value" | ||||
|                     viewBox="0 0 512 512" | ||||
|                     :class="{ | ||||
|                         'c-dial-clip--90': degValue < 90 && typeFilledDial, | ||||
|                         'c-dial-clip--180': degValue >= 90 && degValue < 180 && typeFilledDial | ||||
|                     }" | ||||
|                 > | ||||
|                     <path | ||||
|                         v-if="typeFilledDial && degValue > 0" | ||||
|                         d="M256,31A224.3,224.3,0,0,0,98.3,95.5l48.4,49.2a156,156,0,1,1-1,221.6L96.9,415.1A224.4,224.4,0,0,0,256,481c124.3,0,225-100.7,225-225S380.3,31,256,31Z" | ||||
|                         :style="`transform: rotate(${degValue}deg)`" | ||||
|                     /> | ||||
|                     <path | ||||
|                         v-if="typeNeedleDial && valueInBounds" | ||||
|                         d="M256,86c-93.9,0-170,76.1-170,170c0,43.9,16.6,83.9,43.9,114.1l-38.7,38.7c-3.3,3.3-3.3,8.7,0,12s8.7,3.3,12,0 l38.7-38.7C172.1,409.4,212.1,426,256,426c93.9,0,170-76.1,170-170S349.9,86,256,86z M256,411.7c-86,0-155.7-69.7-155.7-155.7 S170,100.3,256,100.3S411.7,170,411.7,256S342,411.7,256,411.7z" | ||||
|                         :style="`transform: rotate(${degValue}deg)`" | ||||
|                     /> | ||||
|                 </svg> | ||||
|             </div> | ||||
|         </template> | ||||
|  | ||||
|         <template v-if="typeMeter"> | ||||
|             <div class="c-meter"> | ||||
|                 <div | ||||
|                     v-if="displayMinMax" | ||||
|                     class="c-gauge__range c-meter__range" | ||||
|                 > | ||||
|                     <div class="c-meter__range__high">{{ rangeHigh }}</div> | ||||
|                     <div class="c-meter__range__low">{{ rangeLow }}</div> | ||||
|                 </div> | ||||
|                 <div class="c-meter__bg"> | ||||
|                     <template v-if="typeMeterVertical"> | ||||
|                         <div | ||||
|                             class="c-meter__value" | ||||
|                             :style="`transform: translateY(${meterValueToPerc}%)`" | ||||
|                         ></div> | ||||
|  | ||||
|                         <div | ||||
|                             v-if="limitHigh && meterHighLimitPerc > 0" | ||||
|                             class="c-meter__limit-high" | ||||
|                             :style="`height: ${meterHighLimitPerc}%`" | ||||
|                         ></div> | ||||
|  | ||||
|                         <div | ||||
|                             v-if="limitLow && meterLowLimitPerc > 0" | ||||
|                             class="c-meter__limit-low" | ||||
|                             :style="`height: ${meterLowLimitPerc}%`" | ||||
|                         ></div> | ||||
|                     </template> | ||||
|  | ||||
|                     <template v-if="typeMeterHorizontal"> | ||||
|                         <div | ||||
|                             class="c-meter__value" | ||||
|                             :style="`transform: translateX(${meterValueToPerc * -1}%)`" | ||||
|                         ></div> | ||||
|  | ||||
|                         <div | ||||
|                             v-if="limitHigh && meterHighLimitPerc > 0" | ||||
|                             class="c-meter__limit-high" | ||||
|                             :style="`width: ${meterHighLimitPerc}%`" | ||||
|                         ></div> | ||||
|  | ||||
|                         <div | ||||
|                             v-if="limitLow && meterLowLimitPerc > 0" | ||||
|                             class="c-meter__limit-low" | ||||
|                             :style="`width: ${meterLowLimitPerc}%`" | ||||
|                         ></div> | ||||
|                     </template> | ||||
|  | ||||
|                     <svg | ||||
|                         v-if="displayCurVal" | ||||
|                         class="c-gauge__curval" | ||||
|                         :viewBox="curValViewBox" | ||||
|                         preserveAspectRatio="xMidYMid meet" | ||||
|                     > | ||||
|                         <text | ||||
|                             class="c-gauge__curval-text" | ||||
|                             text-anchor="middle" | ||||
|                             lengthAdjust="spacing" | ||||
|                         >{{ curVal }}</text> | ||||
|                     </svg> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </template> | ||||
|     </div> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| const LIMIT_PADDING_IN_PERCENT = 10; | ||||
|  | ||||
| export default { | ||||
|     name: 'Gauge', | ||||
|     inject: ['openmct', 'domainObject', 'composition'], | ||||
|     data() { | ||||
|         let gaugeController = this.domainObject.configuration.gaugeController; | ||||
|  | ||||
|         return { | ||||
|             curVal: 0, | ||||
|             digits: 3, | ||||
|             precision: gaugeController.precision, | ||||
|             displayMinMax: gaugeController.isDisplayMinMax, | ||||
|             displayCurVal: gaugeController.isDisplayCurVal, | ||||
|             limitHigh: gaugeController.limitHigh, | ||||
|             limitLow: gaugeController.limitLow, | ||||
|             rangeHigh: gaugeController.max, | ||||
|             rangeLow: gaugeController.min, | ||||
|             gaugeType: gaugeController.gaugeType, | ||||
|             activeTimeSystem: this.openmct.time.timeSystem() | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         degValue() { | ||||
|             return this.percentToDegrees(this.valToPercent(this.curVal)); | ||||
|         }, | ||||
|         dialHighLimitDeg() { | ||||
|             return this.percentToDegrees(this.valToPercent(this.limitHigh)); | ||||
|         }, | ||||
|         dialLowLimitDeg() { | ||||
|             return this.percentToDegrees(this.valToPercent(this.limitLow)); | ||||
|         }, | ||||
|         curValViewBox() { | ||||
|             const DIGITS_RATIO = 10; | ||||
|             const VIEWBOX_STR = '0 0 X 15'; | ||||
|  | ||||
|             return VIEWBOX_STR.replace('X', this.digits * DIGITS_RATIO); | ||||
|         }, | ||||
|         typeDial() { | ||||
|             return this.matchGaugeType('dial'); | ||||
|         }, | ||||
|         typeFilledDial() { | ||||
|             return this.matchGaugeType('dial-filled'); | ||||
|         }, | ||||
|         typeNeedleDial() { | ||||
|             return this.matchGaugeType('dial-needle'); | ||||
|         }, | ||||
|         typeMeter() { | ||||
|             return this.matchGaugeType('meter'); | ||||
|         }, | ||||
|         typeMeterHorizontal() { | ||||
|             return this.matchGaugeType('horizontal'); | ||||
|         }, | ||||
|         typeMeterVertical() { | ||||
|             return this.matchGaugeType('vertical'); | ||||
|         }, | ||||
|         typeMeterInverted() { | ||||
|             return this.matchGaugeType('inverted'); | ||||
|         }, | ||||
|         meterValueToPerc() { | ||||
|             const meterDirection = (this.typeMeterInverted) ? -1 : 1; | ||||
|  | ||||
|             if (this.curVal <= this.rangeLow) { | ||||
|                 return meterDirection * 100; | ||||
|             } | ||||
|  | ||||
|             if (this.curVal >= this.rangeHigh) { | ||||
|                 return 0; | ||||
|             } | ||||
|  | ||||
|             return this.valToPercentMeter(this.curVal) * meterDirection; | ||||
|         }, | ||||
|         meterHighLimitPerc() { | ||||
|             return this.valToPercentMeter(this.limitHigh); | ||||
|         }, | ||||
|         meterLowLimitPerc() { | ||||
|             return 100 - this.valToPercentMeter(this.limitLow); | ||||
|         }, | ||||
|         valueInBounds() { | ||||
|             return (this.curVal >= this.rangeLow && this.curVal <= this.rangeHigh); | ||||
|         }, | ||||
|         timeFormatter() { | ||||
|             const timeSystem = this.activeTimeSystem; | ||||
|             const metadataValue = this.metadata.value(timeSystem.key) || { format: timeSystem.key }; | ||||
|  | ||||
|             return this.openmct.telemetry.getValueFormatter(metadataValue); | ||||
|         } | ||||
|     }, | ||||
|     watch: { | ||||
|         curVal(newCurValue) { | ||||
|             if (this.digits < newCurValue.toString().length) { | ||||
|                 this.digits = newCurValue.toString().length; | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.composition.on('add', this.addedToComposition); | ||||
|         this.composition.on('remove', this.removeTelemetryObject); | ||||
|  | ||||
|         this.composition.load(); | ||||
|  | ||||
|         this.openmct.time.on('bounds', this.refreshData); | ||||
|         this.openmct.time.on('timeSystem', this.setTimeSystem); | ||||
|     }, | ||||
|     destroyed() { | ||||
|         this.composition.off('add', this.addedToComposition); | ||||
|         this.composition.off('remove', this.removeTelemetryObject); | ||||
|  | ||||
|         if (this.unsubscribe) { | ||||
|             this.unsubscribe(); | ||||
|         } | ||||
|  | ||||
|         this.openmct.time.off('bounds', this.refreshData); | ||||
|         this.openmct.time.off('timeSystem', this.setTimeSystem); | ||||
|     }, | ||||
|     methods: { | ||||
|         addTelemetryObjectAndSubscribe(domainObject) { | ||||
|             this.telemetryObject = domainObject; | ||||
|             this.request(); | ||||
|             this.subscribe(); | ||||
|         }, | ||||
|         addedToComposition(domainObject) { | ||||
|             if (this.telemetryObject) { | ||||
|                 this.confirmRemoval(domainObject); | ||||
|             } else { | ||||
|                 this.addTelemetryObjectAndSubscribe(domainObject); | ||||
|             } | ||||
|         }, | ||||
|         confirmRemoval(domainObject) { | ||||
|             const dialog = this.openmct.overlays.dialog({ | ||||
|                 iconClass: 'alert', | ||||
|                 message: 'This action will replace the current telemetry source. Do you want to continue?', | ||||
|                 buttons: [ | ||||
|                     { | ||||
|                         label: 'Ok', | ||||
|                         emphasis: true, | ||||
|                         callback: () => { | ||||
|                             this.removeFromComposition(); | ||||
|                             this.removeTelemetryObject(); | ||||
|                             this.addTelemetryObjectAndSubscribe(domainObject); | ||||
|                             dialog.dismiss(); | ||||
|                         } | ||||
|                     }, | ||||
|                     { | ||||
|                         label: 'Cancel', | ||||
|                         callback: () => { | ||||
|                             this.removeFromComposition(domainObject); | ||||
|                             dialog.dismiss(); | ||||
|                         } | ||||
|                     } | ||||
|                 ] | ||||
|             }); | ||||
|         }, | ||||
|         matchGaugeType(str) { | ||||
|             return this.gaugeType.indexOf(str) !== -1; | ||||
|         }, | ||||
|         percentToDegrees(vPercent) { | ||||
|             return this.round((vPercent / 100) * 270, 2); | ||||
|         }, | ||||
|         removeFromComposition(telemetryObject = this.telemetryObject) { | ||||
|             let composition = this.domainObject.composition.filter(id => | ||||
|                 !this.openmct.objects.areIdsEqual(id, telemetryObject.identifier) | ||||
|             ); | ||||
|  | ||||
|             this.openmct.objects.mutate(this.domainObject, 'composition', composition); | ||||
|         }, | ||||
|         refreshData(bounds, isTick) { | ||||
|             if (!isTick) { | ||||
|                 this.request(); | ||||
|             } | ||||
|         }, | ||||
|         removeTelemetryObject() { | ||||
|             if (this.unsubscribe) { | ||||
|                 this.unsubscribe(); | ||||
|                 this.unsubscribe = null; | ||||
|             } | ||||
|  | ||||
|             this.metadata = null; | ||||
|             this.formats = null; | ||||
|             this.valueKey = null; | ||||
|             this.limitHigh = null; | ||||
|             this.limitLow = null; | ||||
|             this.rangeHigh = null; | ||||
|             this.rangeLow = null; | ||||
|         }, | ||||
|         request(domainObject = this.telemetryObject) { | ||||
|             this.metadata = this.openmct.telemetry.getMetadata(domainObject); | ||||
|             this.formats = this.openmct.telemetry.getFormatMap(this.metadata); | ||||
|             const LimitEvaluator = this.openmct.telemetry.getLimits(domainObject); | ||||
|             LimitEvaluator.limits().then(this.updateLimits); | ||||
|  | ||||
|             this.valueKey = this | ||||
|                 .metadata | ||||
|                 .valuesForHints(['range'])[0].source; | ||||
|  | ||||
|             this.openmct | ||||
|                 .telemetry | ||||
|                 .request(domainObject, { strategy: 'latest' }) | ||||
|                 .then(values => { | ||||
|                     const length = values.length; | ||||
|                     this.updateValue(values[length - 1]); | ||||
|                 }); | ||||
|         }, | ||||
|         round(val, decimals = this.precision) { | ||||
|             let precision = Math.pow(10, decimals); | ||||
|  | ||||
|             return Math.round(val * precision) / precision; | ||||
|         }, | ||||
|         setTimeSystem(timeSystem) { | ||||
|             this.activeTimeSystem = timeSystem; | ||||
|         }, | ||||
|         subscribe(domainObject = this.telemetryObject) { | ||||
|             this.unsubscribe = this.openmct | ||||
|                 .telemetry | ||||
|                 .subscribe(domainObject, this.updateValue.bind(this)); | ||||
|         }, | ||||
|         updateLimits(telemetryLimit) { | ||||
|             if (!telemetryLimit || !this.domainObject.configuration.gaugeController.isUseTelemetryLimits) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             let limits = { | ||||
|                 high: 0, | ||||
|                 low: 0 | ||||
|             }; | ||||
|             if (telemetryLimit.CRITICAL) { | ||||
|                 limits = telemetryLimit.CRITICAL; | ||||
|             } else if (telemetryLimit.DISTRESS) { | ||||
|                 limits = telemetryLimit.DISTRESS; | ||||
|             } else if (telemetryLimit.SEVERE) { | ||||
|                 limits = telemetryLimit.SEVERE; | ||||
|             } else if (telemetryLimit.WARNING) { | ||||
|                 limits = telemetryLimit.WARNING; | ||||
|             } else if (telemetryLimit.WATCH) { | ||||
|                 limits = telemetryLimit.WATCH; | ||||
|             } else { | ||||
|                 this.openmct.notifications.error('No limits definition for given telemetry'); | ||||
|             } | ||||
|  | ||||
|             this.limitHigh = this.round(limits.high[this.valueKey]); | ||||
|             this.limitLow = this.round(limits.low[this.valueKey]); | ||||
|             this.rangeHigh = this.round(this.limitHigh + this.limitHigh * LIMIT_PADDING_IN_PERCENT / 100); | ||||
|             this.rangeLow = this.round(this.limitLow - Math.abs(this.limitLow * LIMIT_PADDING_IN_PERCENT / 100)); | ||||
|         }, | ||||
|         updateValue(datum) { | ||||
|             this.datum = datum; | ||||
|  | ||||
|             if (this.isRendering) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             const { start, end } = this.openmct.time.bounds(); | ||||
|             const parsedValue = this.timeFormatter.parse(this.datum); | ||||
|  | ||||
|             const beforeStartOfBounds = parsedValue < start; | ||||
|             const afterEndOfBounds = parsedValue > end; | ||||
|             if (afterEndOfBounds || beforeStartOfBounds) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             this.isRendering = true; | ||||
|             requestAnimationFrame(() => { | ||||
|                 this.isRendering = false; | ||||
|  | ||||
|                 this.curVal = this.round(this.formats[this.valueKey].format(this.datum), this.precision); | ||||
|             }); | ||||
|         }, | ||||
|         valToPercent(vValue) { | ||||
|             // Used by dial | ||||
|             if (vValue >= this.rangeHigh && this.typeFilledDial) { | ||||
|                 // Don't peg at 100% if the gaugeType isn't a filled shape | ||||
|                 return 100; | ||||
|             } | ||||
|  | ||||
|             return ((vValue - this.rangeLow) / (this.rangeHigh - this.rangeLow)) * 100; | ||||
|         }, | ||||
|         valToPercentMeter(vValue) { | ||||
|             return this.round((this.rangeHigh - vValue) / (this.rangeHigh - this.rangeLow) * 100, 2); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
| @@ -1,170 +0,0 @@ | ||||
| /***************************************************************************** | ||||
| * 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. | ||||
| *****************************************************************************/ | ||||
|  | ||||
| <template> | ||||
| <span class="form-control"> | ||||
|     <span | ||||
|         class="field control" | ||||
|         :class="model.cssClass" | ||||
|     > | ||||
|         <ToggleSwitch | ||||
|             :checked="isUseTelemetryLimits" | ||||
|             label="Use telemetry limits for minimum and maximum ranges" | ||||
|             @change="toggleUseTelemetryLimits" | ||||
|         /> | ||||
|  | ||||
|         <div | ||||
|             v-if="!isUseTelemetryLimits" | ||||
|             class="c-form--sub-grid" | ||||
|         > | ||||
|             <div class="c-form__row"> | ||||
|                 <span class="req-indicator req"> | ||||
|                 </span> | ||||
|                 <label>Range minimum value</label> | ||||
|                 <input | ||||
|                     ref="min" | ||||
|                     v-model.number="min" | ||||
|                     data-field-name="min" | ||||
|                     type="number" | ||||
|                     @input="onChange" | ||||
|                 > | ||||
|             </div> | ||||
|  | ||||
|             <div class="c-form__row"> | ||||
|                 <span class="req-indicator"> | ||||
|                 </span> | ||||
|                 <label>Range low limit</label> | ||||
|                 <input | ||||
|                     ref="limitLow" | ||||
|                     v-model.number="limitLow" | ||||
|                     data-field-name="limitLow" | ||||
|                     type="number" | ||||
|                     @input="onChange" | ||||
|                 > | ||||
|             </div> | ||||
|  | ||||
|             <div class="c-form__row"> | ||||
|                 <span class="req-indicator req"> | ||||
|                 </span> | ||||
|                 <label>Range maximum value</label> | ||||
|                 <input | ||||
|                     ref="max" | ||||
|                     v-model.number="max" | ||||
|                     data-field-name="max" | ||||
|                     type="number" | ||||
|                     @input="onChange" | ||||
|                 > | ||||
|             </div> | ||||
|  | ||||
|             <div class="c-form__row"> | ||||
|                 <span class="req-indicator"> | ||||
|                 </span> | ||||
|                 <label>Range high limit</label> | ||||
|                 <input | ||||
|                     ref="limitHigh" | ||||
|                     v-model.number="limitHigh" | ||||
|                     data-field-name="limitHigh" | ||||
|                     type="number" | ||||
|                     @input="onChange" | ||||
|                 > | ||||
|             </div> | ||||
|         </div> | ||||
|     </span> | ||||
| </span> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import ToggleSwitch from '@/ui/components/ToggleSwitch.vue'; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|         ToggleSwitch | ||||
|     }, | ||||
|     props: { | ||||
|         model: { | ||||
|             type: Object, | ||||
|             required: true | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             isUseTelemetryLimits: this.model.value.isUseTelemetryLimits, | ||||
|             isDisplayMinMax: this.model.value.isDisplayMinMax, | ||||
|             isDisplayCurVal: this.model.value.isDisplayCurVal, | ||||
|             limitHigh: this.model.value.limitHigh, | ||||
|             limitLow: this.model.value.limitLow, | ||||
|             max: this.model.value.max, | ||||
|             min: this.model.value.min | ||||
|         }; | ||||
|     }, | ||||
|     methods: { | ||||
|         onChange(event) { | ||||
|             const data = { | ||||
|                 model: this.model, | ||||
|                 value: { | ||||
|                     gaugeType: this.model.value.gaugeType, | ||||
|                     isDisplayMinMax: this.isDisplayMinMax, | ||||
|                     isDisplayCurVal: this.isDisplayCurVal, | ||||
|                     isUseTelemetryLimits: this.isUseTelemetryLimits, | ||||
|                     limitLow: this.limitLow, | ||||
|                     limitHigh: this.limitHigh, | ||||
|                     max: this.max, | ||||
|                     min: this.min, | ||||
|                     precision: this.model.value.precision | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
|             if (event) { | ||||
|                 const target = event.target; | ||||
|                 const targetIndicator = target.parentElement.querySelector('.req-indicator'); | ||||
|                 if (targetIndicator.classList.contains('req')) { | ||||
|                     targetIndicator.classList.add('visited'); | ||||
|                 } | ||||
|  | ||||
|                 this.model.validate(data, (valid) => { | ||||
|                     Object.entries(valid).forEach(([key, isValid]) => { | ||||
|                         const element = this.$refs[key]; | ||||
|                         const reqIndicatorElement = element.parentElement.querySelector('.req-indicator'); | ||||
|                         reqIndicatorElement.classList.toggle('invalid', !isValid); | ||||
|  | ||||
|                         if (reqIndicatorElement.classList.contains('req') && (!isValid || reqIndicatorElement.classList.contains('visited'))) { | ||||
|                             reqIndicatorElement.classList.toggle('valid', isValid); | ||||
|                         } | ||||
|                     }); | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             this.$emit('onChange', data); | ||||
|         }, | ||||
|         toggleUseTelemetryLimits() { | ||||
|             this.isUseTelemetryLimits = !this.isUseTelemetryLimits; | ||||
|  | ||||
|             this.onChange(); | ||||
|         }, | ||||
|         toggleMinMax() { | ||||
|             this.isDisplayMinMax = !this.isDisplayMinMax; | ||||
|  | ||||
|             this.onChange(); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
| @@ -1,269 +0,0 @@ | ||||
| $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; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -14,7 +14,7 @@ $elemBg: rgba(black, 0.7); | ||||
|     position: absolute; | ||||
|     left: 0; | ||||
|     top: 0; | ||||
|     z-index: 2; | ||||
|     z-index: 1; | ||||
|     @include userSelectNone; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -163,13 +163,10 @@ | ||||
|         </div> | ||||
|     </div> | ||||
|     <div | ||||
|         v-if="displayThumbnails" | ||||
|         class="c-imagery__thumbs-wrapper" | ||||
|         :class="[ | ||||
|             { 'is-paused': isPaused && !isFixed }, | ||||
|             { 'is-autoscroll-off': !resizingWindow && !autoScroll && !isPaused }, | ||||
|             { 'is-small-thumbs': displayThumbnailsSmall }, | ||||
|             { 'hide': !displayThumbnails } | ||||
|             { 'is-autoscroll-off': !resizingWindow && !autoScroll && !isPaused } | ||||
|         ]" | ||||
|     > | ||||
|         <div | ||||
| @@ -182,7 +179,6 @@ | ||||
|                 :key="image.url + image.time" | ||||
|                 class="c-imagery__thumb c-thumb" | ||||
|                 :class="{ selected: focusedImageIndex === index && isPaused }" | ||||
|                 :title="image.formattedTime" | ||||
|                 @click="thumbnailClicked(index)" | ||||
|             > | ||||
|                 <a | ||||
| @@ -236,8 +232,6 @@ const ARROW_LEFT = 37; | ||||
| const SCROLL_LATENCY = 250; | ||||
|  | ||||
| const ZOOM_SCALE_DEFAULT = 1; | ||||
| const SHOW_THUMBS_THRESHOLD_HEIGHT = 200; | ||||
| const SHOW_THUMBS_FULLSIZE_THRESHOLD_HEIGHT = 600; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
| @@ -278,7 +272,6 @@ export default { | ||||
|             imageContainerHeight: undefined, | ||||
|             sizedImageWidth: 0, | ||||
|             sizedImageHeight: 0, | ||||
|             viewHeight: 0, | ||||
|             lockCompass: true, | ||||
|             resizingWindow: false, | ||||
|             timeContext: undefined, | ||||
| @@ -297,8 +290,7 @@ export default { | ||||
|             imageTranslateY: 0, | ||||
|             pan: undefined, | ||||
|             animateZoom: true, | ||||
|             imagePanned: false, | ||||
|             forceShowThumbnails: false | ||||
|             imagePanned: false | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
| @@ -314,15 +306,6 @@ export default { | ||||
|  | ||||
|             return compassRoseSizingClasses; | ||||
|         }, | ||||
|         displayThumbnails() { | ||||
|             return ( | ||||
|                 this.forceShowThumbnails | ||||
|                 || this.viewHeight >= SHOW_THUMBS_THRESHOLD_HEIGHT | ||||
|             ); | ||||
|         }, | ||||
|         displayThumbnailsSmall() { | ||||
|             return this.viewHeight > SHOW_THUMBS_THRESHOLD_HEIGHT && this.viewHeight <= SHOW_THUMBS_FULLSIZE_THRESHOLD_HEIGHT; | ||||
|         }, | ||||
|         time() { | ||||
|             return this.formatTime(this.focusedImage); | ||||
|         }, | ||||
| @@ -600,9 +583,6 @@ export default { | ||||
|  | ||||
|     }, | ||||
|     methods: { | ||||
|         calculateViewHeight() { | ||||
|             this.viewHeight = this.$el.clientHeight; | ||||
|         }, | ||||
|         setTimeContext() { | ||||
|             this.stopFollowingTimeContext(); | ||||
|             this.timeContext = this.openmct.time.getContextForView(this.objectPath); | ||||
| @@ -976,7 +956,6 @@ export default { | ||||
|             } | ||||
|  | ||||
|             this.setSizedImageDimensions(); | ||||
|             this.calculateViewHeight(); | ||||
|         }, | ||||
|         setSizedImageDimensions() { | ||||
|             this.focusedImageNaturalAspectRatio = this.$refs.focusedImage.naturalWidth / this.$refs.focusedImage.naturalHeight; | ||||
| @@ -1005,8 +984,6 @@ export default { | ||||
|                 this.scrollToRight('reset'); | ||||
|             } | ||||
|  | ||||
|             this.calculateViewHeight(); | ||||
|  | ||||
|             this.$nextTick(() => { | ||||
|                 this.resizingWindow = false; | ||||
|             }); | ||||
|   | ||||
| @@ -137,9 +137,9 @@ | ||||
|         animation-name: fade-out; | ||||
|         animation-timing-function: ease-in; | ||||
|         animation-iteration-count: 1; | ||||
|         animation-fill-mode: forwards; | ||||
|         animation-fill-mode: forwards;    | ||||
|     } | ||||
|  | ||||
|      | ||||
|  | ||||
|     &__thumbs-wrapper { | ||||
|         display: flex; // Uses row layout | ||||
| @@ -161,11 +161,17 @@ | ||||
|         flex: 0 1 auto; | ||||
|         display: flex; | ||||
|         flex-direction: row; | ||||
|         height: 145px; | ||||
|         height: 135px; | ||||
|         overflow-x: auto; | ||||
|         overflow-y: hidden; | ||||
|         margin-bottom: 1px; | ||||
|         padding-bottom: $interiorMarginSm; | ||||
|  | ||||
|         .c-thumb:last-child { | ||||
|             // Hilite the lastest thumb | ||||
|             background: $colorBodyFg; | ||||
|             color: $colorBodyBg; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     &__auto-scroll-resume-button { | ||||
| @@ -178,12 +184,10 @@ | ||||
|  | ||||
| /*************************************** THUMBS */ | ||||
| .c-thumb { | ||||
|     $w: $imageThumbsD; | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     padding: 4px; | ||||
|     min-width: $w; | ||||
|     width: $w; | ||||
|     width: $imageThumbsD; | ||||
|  | ||||
|     &:hover { | ||||
|         background: $colorThumbHoverBg; | ||||
| @@ -205,19 +209,11 @@ | ||||
|     } | ||||
| } | ||||
|  | ||||
| .is-small-thumbs { | ||||
| .l-layout, | ||||
| .c-fl { | ||||
|     .c-imagery__thumbs-scroll-area { | ||||
|         height: 60px; // Allow room for scrollbar | ||||
|     } | ||||
|  | ||||
|     .c-thumb { | ||||
|         $w: $imageThumbsD / 2; | ||||
|         min-width: $w; | ||||
|         width: $w; | ||||
|  | ||||
|         &__timestamp { | ||||
|             display: none; | ||||
|         } | ||||
|         //  When Imagery is in a layout, hide the thumbs area | ||||
|         display: none; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -226,7 +222,7 @@ | ||||
|     .h-local-controls--overlay-content { | ||||
|         position: absolute; | ||||
|         left: $interiorMargin; top: $interiorMargin; | ||||
|         z-index: 70; | ||||
|         z-index: 2; | ||||
|         background: $colorLocalControlOvrBg; | ||||
|         border-radius: $basicCr; | ||||
|         max-width: 250px; | ||||
|   | ||||
| @@ -344,8 +344,6 @@ describe("The Imagery View Layouts", () => { | ||||
|             ); | ||||
|             openmct.install(clearDataPlugin); | ||||
|             clearDataAction = openmct.actions.getAction('clear-data-action'); | ||||
|             // force show the thumbnails | ||||
|             imageryView._getInstance().$children[0].forceShowThumbnails = true; | ||||
|  | ||||
|             return Vue.nextTick(); | ||||
|         }); | ||||
| @@ -525,10 +523,7 @@ describe("The Imagery View Layouts", () => { | ||||
|             expect(clearDataAction).toBeDefined(); | ||||
|         }); | ||||
|  | ||||
|         it('on clearData action should clear data for object is selected', async (done) => { | ||||
|             // force show the thumbnails | ||||
|             imageryView._getInstance().$children[0].forceShowThumbnails = true; | ||||
|             await Vue.nextTick(); | ||||
|         it('on clearData action should clear data for object is selected', (done) => { | ||||
|             expect(parent.querySelectorAll('.c-imagery__thumb').length).not.toBe(0); | ||||
|             openmct.objectViews.on('clearData', async (_domainObject) => { | ||||
|                 await Vue.nextTick(); | ||||
|   | ||||
| @@ -20,7 +20,10 @@ | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
| import PerformancePlugin from './plugin.js'; | ||||
| import { createOpenMct, resetApplicationState } from 'utils/testing'; | ||||
| import { | ||||
|     createOpenMct, | ||||
|     resetApplicationState | ||||
| } from 'utils/testing'; | ||||
|  | ||||
| describe('the plugin', () => { | ||||
|     let openmct; | ||||
| @@ -28,8 +31,9 @@ describe('the plugin', () => { | ||||
|     let child; | ||||
|  | ||||
|     let performanceIndicator; | ||||
|     let countFramesPromise; | ||||
|  | ||||
|     beforeEach(done => { | ||||
|     beforeEach((done) => { | ||||
|         openmct = createOpenMct(); | ||||
|  | ||||
|         element = document.createElement('div'); | ||||
| @@ -38,9 +42,11 @@ describe('the plugin', () => { | ||||
|  | ||||
|         openmct.install(new PerformancePlugin()); | ||||
|  | ||||
|         countFramesPromise = countFrames(); | ||||
|  | ||||
|         openmct.on('start', done); | ||||
|  | ||||
|         performanceIndicator = openmct.indicators.indicatorObjects.find(indicator => { | ||||
|         performanceIndicator = openmct.indicators.indicatorObjects.find((indicator) => { | ||||
|             return indicator.text && indicator.text() === '~ fps'; | ||||
|         }); | ||||
|  | ||||
| @@ -55,21 +61,25 @@ describe('the plugin', () => { | ||||
|         expect(performanceIndicator).toBeDefined(); | ||||
|     }); | ||||
|  | ||||
|     it('calculates an fps value', async () => { | ||||
|         await loopForABit(); | ||||
|         // eslint-disable-next-line | ||||
|         expect(parseInt(performanceIndicator.text().split(' fps')[0])).toBeGreaterThan(0); | ||||
|     it('correctly calculates fps', () => { | ||||
|         return countFramesPromise.then((frames) => { | ||||
|             expect(performanceIndicator.text()).toEqual(`${frames} fps`); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     function loopForABit() { | ||||
|     function countFrames() { | ||||
|         let startTime = performance.now(); | ||||
|         let frames = 0; | ||||
|  | ||||
|         return new Promise(resolve => { | ||||
|             requestAnimationFrame(function loop() { | ||||
|                 if (++frames === 240) { | ||||
|                     resolve(); | ||||
|         return new Promise((resolve) => { | ||||
|             requestAnimationFrame(function incrementCount() { | ||||
|                 let now = performance.now(); | ||||
|  | ||||
|                 if ((now - startTime) < 1000) { | ||||
|                     frames++; | ||||
|                     requestAnimationFrame(incrementCount); | ||||
|                 } else { | ||||
|                     requestAnimationFrame(loop); | ||||
|                     resolve(frames); | ||||
|                 } | ||||
|             }); | ||||
|         }); | ||||
|   | ||||
| @@ -30,8 +30,8 @@ | ||||
|         class="gl-plot-tick-wrapper" | ||||
|     > | ||||
|         <div | ||||
|             v-for="(tick, i) in ticks" | ||||
|             :key="'tick-left' + i" | ||||
|             v-for="tick in ticks" | ||||
|             :key="tick.value" | ||||
|             class="gl-plot-tick gl-plot-x-tick-label" | ||||
|             :style="{ | ||||
|                 left: (100 * (tick.value - min) / interval) + '%' | ||||
| @@ -46,8 +46,8 @@ | ||||
|         class="gl-plot-tick-wrapper" | ||||
|     > | ||||
|         <div | ||||
|             v-for="(tick, i) in ticks" | ||||
|             :key="'tick-top' + i" | ||||
|             v-for="tick in ticks" | ||||
|             :key="tick.value" | ||||
|             class="gl-plot-tick gl-plot-y-tick-label" | ||||
|             :style="{ top: (100 * (max - tick.value) / interval) + '%' }" | ||||
|             :title="tick.fullText || tick.text" | ||||
| @@ -59,8 +59,8 @@ | ||||
|     <!-- grid lines follow --> | ||||
|     <template v-if="position === 'right'"> | ||||
|         <div | ||||
|             v-for="(tick, i) in ticks" | ||||
|             :key="'tick-right' + i" | ||||
|             v-for="tick in ticks" | ||||
|             :key="tick.value" | ||||
|             class="gl-plot-hash hash-v" | ||||
|             :style="{ | ||||
|                 right: (100 * (max - tick.value) / interval) + '%', | ||||
| @@ -71,8 +71,8 @@ | ||||
|     </template> | ||||
|     <template v-if="position === 'bottom'"> | ||||
|         <div | ||||
|             v-for="(tick, i) in ticks" | ||||
|             :key="'tick-bottom' + i" | ||||
|             v-for="tick in ticks" | ||||
|             :key="tick.value" | ||||
|             class="gl-plot-hash hash-h" | ||||
|             :style="{ bottom: (100 * (tick.value - min) / interval) + '%', width: '100%' }" | ||||
|         > | ||||
| @@ -83,7 +83,7 @@ | ||||
|  | ||||
| <script> | ||||
| import eventHelpers from "./lib/eventHelpers"; | ||||
| import { ticks, getLogTicks, getFormattedTicks } from "./tickUtils"; | ||||
| import { ticks, getFormattedTicks } from "./tickUtils"; | ||||
| import configStore from "./configuration/ConfigStore"; | ||||
|  | ||||
| export default { | ||||
| @@ -96,13 +96,6 @@ export default { | ||||
|             }, | ||||
|             required: true | ||||
|         }, | ||||
|         // Make it a prop, then later we can allow user to change it via UI input | ||||
|         tickCount: { | ||||
|             type: Number, | ||||
|             default() { | ||||
|                 return 6; | ||||
|             } | ||||
|         }, | ||||
|         position: { | ||||
|             required: true, | ||||
|             type: String, | ||||
| @@ -125,6 +118,7 @@ export default { | ||||
|  | ||||
|         this.axis = this.getAxisFromConfig(); | ||||
|  | ||||
|         this.tickCount = 4; | ||||
|         this.tickUpdate = false; | ||||
|         this.listenTo(this.axis, 'change:displayRange', this.updateTicks, this); | ||||
|         this.listenTo(this.axis, 'change:format', this.updateTicks, this); | ||||
| @@ -190,12 +184,7 @@ export default { | ||||
|                 }, this); | ||||
|             } | ||||
|  | ||||
|             if (this.axisType === 'yAxis' && this.axis.get('logMode')) { | ||||
|                 return getLogTicks(range.min, range.max, number, 4); | ||||
|                 // return getLogTicks2(range.min, range.max, number); | ||||
|             } else { | ||||
|                 return ticks(range.min, range.max, number); | ||||
|             } | ||||
|             return ticks(range.min, range.max, number); | ||||
|         }, | ||||
|  | ||||
|         updateTicksForceRegeneration() { | ||||
|   | ||||
| @@ -23,7 +23,7 @@ | ||||
| import MCTChartSeriesElement from './MCTChartSeriesElement'; | ||||
|  | ||||
| export default class MCTChartLineStepAfter extends MCTChartSeriesElement { | ||||
|     removePoint(index) { | ||||
|     removePoint(point, index, count) { | ||||
|         if (index > 0 && index / 2 < this.count) { | ||||
|             this.buffer[index + 1] = this.buffer[index - 1]; | ||||
|         } | ||||
|   | ||||
| @@ -85,10 +85,11 @@ export default class MCTChartSeriesElement { | ||||
|  | ||||
|         this.removeSegments(removalPoint, vertexCount); | ||||
|  | ||||
|         // TODO useless makePoint call? | ||||
|         this.makePoint(point, series); | ||||
|         this.removePoint(removalPoint); | ||||
|  | ||||
|         this.removePoint( | ||||
|             this.makePoint(point, series), | ||||
|             removalPoint, | ||||
|             vertexCount | ||||
|         ); | ||||
|         this.count -= (vertexCount / 2); | ||||
|     } | ||||
|  | ||||
| @@ -108,7 +109,11 @@ export default class MCTChartSeriesElement { | ||||
|         const insertionPoint = this.startIndexForPointAtIndex(index); | ||||
|         this.growIfNeeded(pointsRequired); | ||||
|         this.makeInsertionPoint(insertionPoint, pointsRequired); | ||||
|         this.addPoint(this.makePoint(point, series), insertionPoint); | ||||
|         this.addPoint( | ||||
|             this.makePoint(point, series), | ||||
|             insertionPoint, | ||||
|             pointsRequired | ||||
|         ); | ||||
|         this.count += (pointsRequired / 2); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -71,7 +71,6 @@ export default class Model extends EventEmitter { | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @abstract | ||||
|      * @param {ModelOptions<T, O>} options | ||||
|      */ | ||||
|     initialize(options) { | ||||
|   | ||||
| @@ -23,7 +23,6 @@ import _ from 'lodash'; | ||||
| import Model from "./Model"; | ||||
| import { MARKER_SHAPES } from '../draw/MarkerShapes'; | ||||
| import configStore from "../configuration/ConfigStore"; | ||||
| import { symlog } from '../mathUtils'; | ||||
|  | ||||
| /** | ||||
|  * Plot series handle interpreting telemetry metadata for a single telemetry | ||||
| @@ -64,8 +63,6 @@ import { symlog } from '../mathUtils'; | ||||
|  * @extends {Model<PlotSeriesModelType, PlotSeriesModelOptions>} | ||||
|  */ | ||||
| export default class PlotSeries extends Model { | ||||
|     logMode = false; | ||||
|  | ||||
|     /** | ||||
|      @param {import('./Model').ModelOptions<PlotSeriesModelType, PlotSeriesModelOptions>} options | ||||
|      */ | ||||
| @@ -73,8 +70,6 @@ export default class PlotSeries extends Model { | ||||
|  | ||||
|         super(options); | ||||
|  | ||||
|         this.logMode = options.collection.plot.model.yAxis.logMode; | ||||
|  | ||||
|         this.listenTo(this, 'change:xKey', this.onXKeyChange, this); | ||||
|         this.listenTo(this, 'change:yKey', this.onYKeyChange, this); | ||||
|         this.persistedConfig = options.persistedConfig; | ||||
| @@ -234,7 +229,6 @@ export default class PlotSeries extends Model { | ||||
|             this.getXVal = format.parse.bind(format); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Update y formatter on change, default to stepAfter interpolation if | ||||
|      * y range is an enumeration. | ||||
| @@ -258,11 +252,7 @@ export default class PlotSeries extends Model { | ||||
|         }.bind(this); | ||||
|         this.set('unit', valueMetadata.unit); | ||||
|         const format = this.formats[newKey]; | ||||
|         this.getYVal = (value) => { | ||||
|             const y = format.parse(value); | ||||
|  | ||||
|             return this.logMode ? symlog(y, 10) : y; | ||||
|         }; | ||||
|         this.getYVal = format.parse.bind(format); | ||||
|     } | ||||
|  | ||||
|     formatX(point) { | ||||
| @@ -530,8 +520,7 @@ export default class PlotSeries extends Model { | ||||
|  | ||||
|     /** | ||||
|      * Update the series data with the given value. | ||||
|      * This return type definition is totally wrong, only covers sinwave generator. It needs to be generic. | ||||
|      * @return-example {Array<{ | ||||
|      * @returns {Array<{ | ||||
|             cos: number | ||||
|             sin: number | ||||
|             mctLimitState: { | ||||
|   | ||||
| @@ -19,7 +19,6 @@ | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
| import { antisymlog, symlog } from '../mathUtils'; | ||||
| import Model from './Model'; | ||||
|  | ||||
| /** | ||||
| @@ -31,7 +30,7 @@ import Model from './Model'; | ||||
|  * | ||||
|  * `autoscale`: boolean, whether or not to autoscale. | ||||
|  * `autoscalePadding`: float, percent of padding to display in plots. | ||||
|  * `displayRange`: the current display range for the axis. | ||||
|  * `displayRange`: the current display range for the x Axis. | ||||
|  * `format`: the formatter for the axis. | ||||
|  * `frozen`: boolean, if true, displayRange will not be updated automatically. | ||||
|  *           Used to temporarily disable automatic updates during user interaction. | ||||
| @@ -54,7 +53,6 @@ export default class YAxisModel extends Model { | ||||
|         this.listenTo(this, 'change:stats', this.calculateAutoscaleExtents, this); | ||||
|         this.listenTo(this, 'change:autoscale', this.toggleAutoscale, this); | ||||
|         this.listenTo(this, 'change:autoscalePadding', this.updatePadding, this); | ||||
|         this.listenTo(this, 'change:logMode', this.onLogModeChange, this); | ||||
|         this.listenTo(this, 'change:frozen', this.toggleFreeze, this); | ||||
|         this.listenTo(this, 'change:range', this.updateDisplayRange, this); | ||||
|         this.updateDisplayRange(this.get('range')); | ||||
| @@ -75,6 +73,11 @@ export default class YAxisModel extends Model { | ||||
|         this.seriesCollection.forEach(this.trackSeries, this); | ||||
|         this.updateFromSeries(this.seriesCollection); | ||||
|     } | ||||
|     updateDisplayRange(range) { | ||||
|         if (!this.get('autoscale')) { | ||||
|             this.set('displayRange', range); | ||||
|         } | ||||
|     } | ||||
|     toggleFreeze(frozen) { | ||||
|         if (!frozen) { | ||||
|             this.toggleAutoscale(this.get('autoscale')); | ||||
| @@ -162,99 +165,31 @@ export default class YAxisModel extends Model { | ||||
|         this.resetStats(); | ||||
|         this.updateFromSeries(this.seriesCollection); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * This is called in order to map the user-provided `range` to the | ||||
|      * `displayRange` that we actually use for plot display. | ||||
|      * | ||||
|      * @param {import('./XAxisModel').NumberRange} range | ||||
|      */ | ||||
|     updateDisplayRange(range) { | ||||
|         if (this.get('autoscale')) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         const _range = { ...range }; | ||||
|  | ||||
|         if (this.get('logMode')) { | ||||
|             _range.min = symlog(range.min, 10); | ||||
|             _range.max = symlog(range.max, 10); | ||||
|         } | ||||
|  | ||||
|         this.set('displayRange', _range); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @param {boolean} autoscale | ||||
|      */ | ||||
|     toggleAutoscale(autoscale) { | ||||
|         if (autoscale && this.has('stats')) { | ||||
|             this.set('displayRange', this.applyPadding(this.get('stats'))); | ||||
|  | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         const range = this.get('range'); | ||||
|  | ||||
|         if (range) { | ||||
|             // If we already have a user-defined range, make sure it maps to the | ||||
|             // range we'll actually use for the ticks. | ||||
|  | ||||
|             const _range = { ...range }; | ||||
|  | ||||
|             if (this.get('logMode')) { | ||||
|                 _range.min = symlog(range.min, 10); | ||||
|                 _range.max = symlog(range.max, 10); | ||||
|             } | ||||
|  | ||||
|             this.set('displayRange', _range); | ||||
|         } else { | ||||
|             // Otherwise use the last known displayRange as the initial | ||||
|             // values for the user-defined range, so that we don't end up | ||||
|             // with any error from an undefined user range. | ||||
|             const range = this.get('range'); | ||||
|  | ||||
|             const _range = this.get('displayRange'); | ||||
|  | ||||
|             if (this.get('logMode')) { | ||||
|                 _range.min = antisymlog(_range.min, 10); | ||||
|                 _range.max = antisymlog(_range.max, 10); | ||||
|             if (range) { | ||||
|                 // If we already have a user-defined range, make sure it maps to the | ||||
|                 // range we'll actually use for the ticks. | ||||
|                 this.set('displayRange', range); | ||||
|             } else { | ||||
|                 // Otherwise use the last known displayRange as the initial | ||||
|                 // values for the user-defined range, so that we don't end up | ||||
|                 // with any error from an undefined user range. | ||||
|                 this.set('range', this.get('displayRange')); | ||||
|             } | ||||
|  | ||||
|             this.set('range', _range); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** @param {boolean} logMode */ | ||||
|     onLogModeChange(logMode) { | ||||
|         const range = this.get('displayRange'); | ||||
|  | ||||
|         if (logMode) { | ||||
|             range.min = symlog(range.min, 10); | ||||
|             range.max = symlog(range.max, 10); | ||||
|         } else { | ||||
|             range.min = antisymlog(range.min, 10); | ||||
|             range.max = antisymlog(range.max, 10); | ||||
|         } | ||||
|  | ||||
|         this.set('displayRange', range); | ||||
|  | ||||
|         this.resetSeries(); | ||||
|     } | ||||
|     resetSeries() { | ||||
|         this.plot.series.forEach((plotSeries) => { | ||||
|             plotSeries.logMode = this.get('logMode'); | ||||
|             plotSeries.reset(plotSeries.getSeriesData()); | ||||
|         }); | ||||
|         // Update the series collection labels and formatting | ||||
|         this.updateFromSeries(this.seriesCollection); | ||||
|     } | ||||
|     /** | ||||
|      * Update yAxis format, values, and label from known series. | ||||
|      * @param {import('./SeriesCollection').default} seriesCollection | ||||
|      */ | ||||
|     updateFromSeries(seriesCollection) { | ||||
|         const plotModel = this.plot.get('domainObject'); | ||||
|         const label = plotModel.configuration?.yAxis?.label; | ||||
|         const label = plotModel?.configuration?.yAxis?.label; | ||||
|         const sampleSeries = seriesCollection.first(); | ||||
|         if (!sampleSeries) { | ||||
|             if (!label) { | ||||
| @@ -267,13 +202,7 @@ export default class YAxisModel extends Model { | ||||
|         const yKey = sampleSeries.get('yKey'); | ||||
|         const yMetadata = sampleSeries.metadata.value(yKey); | ||||
|         const yFormat = sampleSeries.formats[yKey]; | ||||
|  | ||||
|         if (this.get('logMode')) { | ||||
|             this.set('format', (n) => yFormat.format(antisymlog(n, 10))); | ||||
|         } else { | ||||
|             this.set('format', (n) => yFormat.format(n)); | ||||
|         } | ||||
|  | ||||
|         this.set('format', yFormat.format.bind(yFormat)); | ||||
|         this.set('values', yMetadata.values); | ||||
|         if (!label) { | ||||
|             const labelName = seriesCollection | ||||
| @@ -326,7 +255,6 @@ export default class YAxisModel extends Model { | ||||
|         return { | ||||
|             frozen: false, | ||||
|             autoscale: true, | ||||
|             logMode: options.model?.logMode ?? false, | ||||
|             autoscalePadding: 0.1 | ||||
|  | ||||
|             // 'range' is not specified here, it is undefined at first. When the | ||||
| @@ -341,7 +269,6 @@ export default class YAxisModel extends Model { | ||||
| /** | ||||
| @typedef {import('./XAxisModel').AxisModelType & { | ||||
|     autoscale: boolean | ||||
|     logMode: boolean | ||||
|     autoscalePadding: number | ||||
|     stats?: import('./XAxisModel').NumberRange | ||||
|     values: Array<TODO> | ||||
|   | ||||
| @@ -45,22 +45,14 @@ | ||||
|                 >Label</div> | ||||
|                 <div class="grid-cell value">{{ label ? label : "Not defined" }}</div> | ||||
|             </li> | ||||
|             <li class="grid-row"> | ||||
|                 <div | ||||
|                     class="grid-cell label" | ||||
|                     title="Enable log mode." | ||||
|                 >Log mode</div> | ||||
|                 <div class="grid-cell value"> | ||||
|                     {{ logMode ? "Enabled" : "Disabled" }} | ||||
|                 </div> | ||||
|             </li> | ||||
|             <li class="grid-row"> | ||||
|                 <div | ||||
|                     class="grid-cell label" | ||||
|                     title="Automatically scale the Y axis to keep all values in view." | ||||
|                 >Auto scale</div> | ||||
|                 >Autoscale</div> | ||||
|                 <div class="grid-cell value"> | ||||
|                     {{ autoscale ? "Enabled: " + autoscalePadding : "Disabled" }} | ||||
|                     {{ autoscale ? "Enabled: " : "Disabled" }} | ||||
|                     {{ autoscale ? autoscalePadding : "" }} | ||||
|                 </div> | ||||
|             </li> | ||||
|             <li | ||||
| @@ -150,7 +142,6 @@ export default { | ||||
|             config: {}, | ||||
|             label: '', | ||||
|             autoscale: '', | ||||
|             logMode: false, | ||||
|             autoscalePadding: '', | ||||
|             rangeMin: '', | ||||
|             rangeMax: '', | ||||
| @@ -181,7 +172,6 @@ export default { | ||||
|         initConfiguration() { | ||||
|             this.label = this.config.yAxis.get('label'); | ||||
|             this.autoscale = this.config.yAxis.get('autoscale'); | ||||
|             this.logMode = this.config.yAxis.get('logMode'); | ||||
|             this.autoscalePadding = this.config.yAxis.get('autoscalePadding'); | ||||
|             const range = this.config.yAxis.get('range'); | ||||
|             if (range) { | ||||
|   | ||||
| @@ -14,22 +14,9 @@ | ||||
|                 @change="updateForm('label')" | ||||
|             ></div> | ||||
|         </li> | ||||
|         <li class="grid-row"> | ||||
|             <div | ||||
|                 class="grid-cell label" | ||||
|                 title="Enable log mode." | ||||
|             > | ||||
|                 Log mode | ||||
|             </div> | ||||
|             <div class="grid-cell value"> | ||||
|                 <!-- eslint-disable-next-line vue/html-self-closing --> | ||||
|                 <input | ||||
|                     v-model="logMode" | ||||
|                     type="checkbox" | ||||
|                     @change="updateForm('logMode')" | ||||
|                 /> | ||||
|             </div> | ||||
|         </li> | ||||
|     </ul> | ||||
|     <ul class="l-inspector-part"> | ||||
|         <h2>Y Axis Scaling</h2> | ||||
|         <li class="grid-row"> | ||||
|             <div | ||||
|                 class="grid-cell label" | ||||
| @@ -118,7 +105,6 @@ export default { | ||||
|         return { | ||||
|             label: '', | ||||
|             autoscale: '', | ||||
|             logMode: false, | ||||
|             autoscalePadding: '', | ||||
|             rangeMin: '', | ||||
|             rangeMax: '', | ||||
| @@ -131,35 +117,38 @@ export default { | ||||
|     }, | ||||
|     methods: { | ||||
|         initialize: function () { | ||||
|             this.fields = { | ||||
|                 label: { | ||||
|             this.fields = [ | ||||
|                 { | ||||
|                     modelProp: 'label', | ||||
|                     objectPath: 'configuration.yAxis.label' | ||||
|                 }, | ||||
|                 autoscale: { | ||||
|                 { | ||||
|                     modelProp: 'autoscale', | ||||
|                     coerce: Boolean, | ||||
|                     objectPath: 'configuration.yAxis.autoscale' | ||||
|                 }, | ||||
|                 autoscalePadding: { | ||||
|                 { | ||||
|                     modelProp: 'autoscalePadding', | ||||
|                     coerce: Number, | ||||
|                     objectPath: 'configuration.yAxis.autoscalePadding' | ||||
|                 }, | ||||
|                 logMode: { | ||||
|                     coerce: Boolean, | ||||
|                     objectPath: 'configuration.yAxis.logMode' | ||||
|                 }, | ||||
|                 range: { | ||||
|                 { | ||||
|                     modelProp: 'range', | ||||
|                     objectPath: 'configuration.yAxis.range', | ||||
|                     coerce: function coerceRange(range) { | ||||
|                         const newRange = { | ||||
|                             min: -1, | ||||
|                             max: 1 | ||||
|                         }; | ||||
|                         if (!range) { | ||||
|                             return { | ||||
|                                 min: 0, | ||||
|                                 max: 0 | ||||
|                             }; | ||||
|                         } | ||||
|  | ||||
|                         if (range && typeof range.min !== 'undefined' && range.min !== null) { | ||||
|                         const newRange = {}; | ||||
|                         if (typeof range.min !== 'undefined' && range.min !== null) { | ||||
|                             newRange.min = Number(range.min); | ||||
|                         } | ||||
|  | ||||
|                         if (range && typeof range.max !== 'undefined' && range.max !== null) { | ||||
|                         if (typeof range.max !== 'undefined' && range.max !== null) { | ||||
|                             newRange.max = Number(range.max); | ||||
|                         } | ||||
|  | ||||
| @@ -191,12 +180,11 @@ export default { | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             }; | ||||
|             ]; | ||||
|         }, | ||||
|         initFormValues() { | ||||
|             this.label = this.yAxis.get('label'); | ||||
|             this.autoscale = this.yAxis.get('autoscale'); | ||||
|             this.logMode = this.yAxis.get('logMode'); | ||||
|             this.autoscalePadding = this.yAxis.get('autoscalePadding'); | ||||
|             const range = this.yAxis.get('range') ?? this.yAxis.get('displayRange'); | ||||
|             this.rangeMin = range?.min; | ||||
| @@ -214,7 +202,7 @@ export default { | ||||
|             } | ||||
|  | ||||
|             let oldVal = this.yAxis.get(formKey); | ||||
|             const formField = this.fields[formKey]; | ||||
|             const formField = this.fields.find((field) => field.modelProp === formKey); | ||||
|  | ||||
|             const validationError = formField.validate?.(newVal, this.yAxis); | ||||
|             this.validationErrors[formKey] = validationError; | ||||
|   | ||||
| @@ -1,44 +0,0 @@ | ||||
| /** The natural number `e`. */ | ||||
| export const e = Math.exp(1); | ||||
|  | ||||
| /** | ||||
| Returns the logarithm of a number, using the given base or the natural number | ||||
| `e` as base if not specified. | ||||
| @param {number} n | ||||
| @param {number=} base log base, defaults to e | ||||
| */ | ||||
| export function log(n, base = e) { | ||||
|     if (base === e) { | ||||
|         return Math.log(n); | ||||
|     } | ||||
|  | ||||
|     return Math.log(n) / Math.log(base); | ||||
| } | ||||
|  | ||||
| /** | ||||
| Returns the inverse of the logarithm of a number, using the given base or the | ||||
| natural number `e` as base if not specified. | ||||
| @param {number} n | ||||
| @param {number=} base log base, defaults to e | ||||
| */ | ||||
| export function antilog(n, base = e) { | ||||
|     return Math.pow(base, n); | ||||
| } | ||||
|  | ||||
| /** | ||||
| A symmetric logarithm function. See https://github.com/nasa/openmct/issues/2297#issuecomment-1032914258 | ||||
| @param {number} n | ||||
| @param {number=} base log base, defaults to e | ||||
| */ | ||||
| export function symlog(n, base = e) { | ||||
|     return Math.sign(n) * log(Math.abs(n) + 1, base); | ||||
| } | ||||
|  | ||||
| /** | ||||
| An inverse symmetric logarithm function. See https://github.com/nasa/openmct/issues/2297#issuecomment-1032914258 | ||||
| @param {number} n | ||||
| @param {number=} base log base, defaults to e | ||||
| */ | ||||
| export function antisymlog(n, base = e) { | ||||
|     return Math.sign(n) * (antilog(Math.abs(n), base) - 1); | ||||
| } | ||||
| @@ -389,7 +389,7 @@ describe("the plugin", function () { | ||||
|                 expect(xAxisElement.length).toBe(1); | ||||
|  | ||||
|                 let ticks = xAxisElement[0].querySelectorAll(".gl-plot-tick"); | ||||
|                 expect(ticks.length).toBe(9); | ||||
|                 expect(ticks.length).toBe(5); | ||||
|  | ||||
|                 done(); | ||||
|             }); | ||||
| @@ -694,7 +694,7 @@ describe("the plugin", function () { | ||||
|  | ||||
|             Vue.nextTick(() => { | ||||
|                 let ticks = xAxisElement[0].querySelectorAll(".gl-plot-tick"); | ||||
|                 expect(ticks.length).toBe(9); | ||||
|                 expect(ticks.length).toBe(5); | ||||
|  | ||||
|                 done(); | ||||
|             }); | ||||
| @@ -1086,9 +1086,7 @@ describe("the plugin", function () { | ||||
|                 expandControl.dispatchEvent(clickEvent); | ||||
|  | ||||
|                 const yAxisProperties = editOptionsEl.querySelectorAll("div.grid-properties:first-of-type .l-inspector-part"); | ||||
|  | ||||
|                 // TODO better test | ||||
|                 expect(yAxisProperties.length).toEqual(2); | ||||
|                 expect(yAxisProperties.length).toEqual(3); | ||||
|             }); | ||||
|  | ||||
|             it('renders color palette options', () => { | ||||
|   | ||||
| @@ -1,5 +1,3 @@ | ||||
| import { antisymlog, symlog } from "./mathUtils"; | ||||
|  | ||||
| const e10 = Math.sqrt(50); | ||||
| const e5 = Math.sqrt(10); | ||||
| const e2 = Math.sqrt(2); | ||||
| @@ -42,47 +40,6 @@ function getPrecision(step) { | ||||
|     return precision; | ||||
| } | ||||
|  | ||||
| export function getLogTicks(start, stop, mainTickCount = 8, secondaryTickCount = 6) { | ||||
|     // log()'ed values | ||||
|     const mainLogTicks = ticks(start, stop, mainTickCount); | ||||
|  | ||||
|     // original values | ||||
|     const mainTicks = mainLogTicks.map(n => antisymlog(n, 10)); | ||||
|  | ||||
|     const result = []; | ||||
|  | ||||
|     let i = 0; | ||||
|     for (const logTick of mainLogTicks) { | ||||
|         result.push(logTick); | ||||
|  | ||||
|         if (i === mainLogTicks.length - 1) { | ||||
|             break; | ||||
|         } | ||||
|  | ||||
|         const tick = mainTicks[i]; | ||||
|         const nextTick = mainTicks[i + 1]; | ||||
|         const rangeBetweenMainTicks = nextTick - tick; | ||||
|  | ||||
|         const secondaryLogTicks = ticks( | ||||
|             tick + rangeBetweenMainTicks / (secondaryTickCount + 1), | ||||
|             nextTick - rangeBetweenMainTicks / (secondaryTickCount + 1), | ||||
|             secondaryTickCount - 2 | ||||
|         ) | ||||
|             .map(n => symlog(n, 10)); | ||||
|  | ||||
|         result.push(...secondaryLogTicks); | ||||
|  | ||||
|         i++; | ||||
|     } | ||||
|  | ||||
|     return result; | ||||
| } | ||||
|  | ||||
| export function getLogTicks2(start, stop, count = 8) { | ||||
|     return ticks(antisymlog(start, 10), antisymlog(stop, 10), count) | ||||
|         .map(n => symlog(n, 10)); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Linear tick generation from d3-array. | ||||
|  */ | ||||
|   | ||||
| @@ -77,7 +77,6 @@ define([ | ||||
|     './userIndicator/plugin', | ||||
|     '../../example/exampleUser/plugin', | ||||
|     './localStorage/plugin', | ||||
|     './gauge/GaugePlugin', | ||||
|     './timelist/plugin' | ||||
| ], function ( | ||||
|     _, | ||||
| @@ -136,7 +135,6 @@ define([ | ||||
|     UserIndicator, | ||||
|     ExampleUser, | ||||
|     LocalStorage, | ||||
|     GaugePlugin, | ||||
|     TimeList | ||||
| ) { | ||||
|     const plugins = {}; | ||||
| @@ -214,7 +212,6 @@ define([ | ||||
|     plugins.DeviceClassifier = DeviceClassifier.default; | ||||
|     plugins.UserIndicator = UserIndicator.default; | ||||
|     plugins.LocalStorage = LocalStorage.default; | ||||
|     plugins.Gauge = GaugePlugin.default; | ||||
|     plugins.Timelist = TimeList.default; | ||||
|  | ||||
|     return plugins; | ||||
|   | ||||
| @@ -29,6 +29,10 @@ define( | ||||
|         _, | ||||
|         EventEmitter | ||||
|     ) { | ||||
|         const LESS_THAN = -1; | ||||
|         const EQUAL = 0; | ||||
|         const GREATER_THAN = 1; | ||||
|  | ||||
|         /** | ||||
|          * @constructor | ||||
|          */ | ||||
| @@ -76,7 +80,10 @@ define( | ||||
|                     this.rows = []; | ||||
|                 } | ||||
|  | ||||
|                 this.sortAndMergeRows(rowsToAdd); | ||||
|                 for (let row of rowsToAdd) { | ||||
|                     let index = this.sortedIndex(this.rows, row); | ||||
|                     this.rows.splice(index, 0, row); | ||||
|                 } | ||||
|  | ||||
|                 // we emit filter no matter what to trigger | ||||
|                 // an update of visible rows | ||||
| @@ -85,85 +92,58 @@ define( | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             sortAndMergeRows(rows) { | ||||
|                 const sortedRowsToAdd = this.sortCollection(rows); | ||||
|             sortedLastIndex(rows, testRow) { | ||||
|                 return this.sortedIndex(rows, testRow, _.sortedLastIndex); | ||||
|             } | ||||
|  | ||||
|             /** | ||||
|              * Finds the correct insertion point for the given row. | ||||
|              * Leverages lodash's `sortedIndex` function which implements a binary search. | ||||
|              * @private | ||||
|              */ | ||||
|             sortedIndex(rows, testRow, lodashFunction = _.sortedIndexBy) { | ||||
|                 if (this.rows.length === 0) { | ||||
|                     this.rows = sortedRowsToAdd; | ||||
|  | ||||
|                     return; | ||||
|                     return 0; | ||||
|                 } | ||||
|  | ||||
|                 const firstIncomingRow = sortedRowsToAdd[0]; | ||||
|                 const lastIncomingRow = sortedRowsToAdd[sortedRowsToAdd.length - 1]; | ||||
|                 const firstExistingRow = this.rows[0]; | ||||
|                 const lastExistingRow = this.rows[this.rows.length - 1]; | ||||
|  | ||||
|                 if (this.firstRowInSortOrder(lastIncomingRow, firstExistingRow) | ||||
|                     === lastIncomingRow | ||||
|                 ) { | ||||
|                     this.rows = [...sortedRowsToAdd, ...this.rows]; | ||||
|                 } else if (this.firstRowInSortOrder(lastExistingRow, firstIncomingRow) | ||||
|                     === lastExistingRow | ||||
|                 ) { | ||||
|                     this.rows = [...this.rows, ...sortedRowsToAdd]; | ||||
|                 } else { | ||||
|                     this.mergeSortedRows(sortedRowsToAdd); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             sortCollection(rows) { | ||||
|                 const sortedRows = _.orderBy( | ||||
|                     rows, | ||||
|                     row => row.getParsedValue(this.sortOptions.key), this.sortOptions.direction | ||||
|                 ); | ||||
|  | ||||
|                 return sortedRows; | ||||
|             } | ||||
|  | ||||
|             mergeSortedRows(rows) { | ||||
|                 const mergedRows = []; | ||||
|                 let i = 0; | ||||
|                 let j = 0; | ||||
|  | ||||
|                 while (i < this.rows.length && j < rows.length) { | ||||
|                     const existingRow = this.rows[i]; | ||||
|                     const incomingRow = rows[j]; | ||||
|  | ||||
|                     if (this.firstRowInSortOrder(existingRow, incomingRow) === existingRow) { | ||||
|                         mergedRows.push(existingRow); | ||||
|                         i++; | ||||
|                     } else { | ||||
|                         mergedRows.push(incomingRow); | ||||
|                         j++; | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 // tail of existing rows is all that is left to merge | ||||
|                 if (i < this.rows.length) { | ||||
|                     for (i; i < this.rows.length; i++) { | ||||
|                         mergedRows.push(this.rows[i]); | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 // tail of incoming rows is all that is left to merge | ||||
|                 if (j < rows.length) { | ||||
|                     for (j; j < rows.length; j++) { | ||||
|                         mergedRows.push(rows[j]); | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 this.rows = mergedRows; | ||||
|             } | ||||
|  | ||||
|             firstRowInSortOrder(row1, row2) { | ||||
|                 const val1 = this.getValueForSortColumn(row1); | ||||
|                 const val2 = this.getValueForSortColumn(row2); | ||||
|                 const testRowValue = this.getValueForSortColumn(testRow); | ||||
|                 const firstValue = this.getValueForSortColumn(this.rows[0]); | ||||
|                 const lastValue = this.getValueForSortColumn(this.rows[this.rows.length - 1]); | ||||
|  | ||||
|                 if (this.sortOptions.direction === 'asc') { | ||||
|                     return val1 <= val2 ? row1 : row2; | ||||
|                     if (testRowValue > lastValue) { | ||||
|                         return this.rows.length; | ||||
|                     } else if (testRowValue === lastValue) { | ||||
|                         // Maintain stable sort | ||||
|                         return this.rows.length; | ||||
|                     } else if (testRowValue <= firstValue) { | ||||
|                         return 0; | ||||
|                     } else { | ||||
|                         return lodashFunction(rows, testRow, (thisRow) => { | ||||
|                             return this.getValueForSortColumn(thisRow); | ||||
|                         }); | ||||
|                     } | ||||
|                 } else { | ||||
|                     return val1 >= val2 ? row1 : row2; | ||||
|                     if (testRowValue >= firstValue) { | ||||
|                         return 0; | ||||
|                     } else if (testRowValue < lastValue) { | ||||
|                         return this.rows.length; | ||||
|                     } else if (testRowValue === lastValue) { | ||||
|                         // Maintain stable sort | ||||
|                         return this.rows.length; | ||||
|                     } else { | ||||
|                         // Use a custom comparison function to support descending sort. | ||||
|                         return lodashFunction(rows, testRow, (thisRow) => { | ||||
|                             const thisRowValue = this.getValueForSortColumn(thisRow); | ||||
|                             if (testRowValue === thisRowValue) { | ||||
|                                 return EQUAL; | ||||
|                             } else if (testRowValue < thisRowValue) { | ||||
|                                 return LESS_THAN; | ||||
|                             } else { | ||||
|                                 return GREATER_THAN; | ||||
|                             } | ||||
|                         }); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
| @@ -225,9 +205,8 @@ define( | ||||
|             sortBy(sortOptions) { | ||||
|                 if (arguments.length > 0) { | ||||
|                     this.sortOptions = sortOptions; | ||||
|                     performance.mark('table:row:sort:start'); | ||||
|                     this.rows = _.orderBy(this.rows, (row) => row.getParsedValue(sortOptions.key), sortOptions.direction); | ||||
|                     performance.mark('table:row:sort:stop'); | ||||
|  | ||||
|                     this.emit('sort'); | ||||
|                 } | ||||
|  | ||||
|   | ||||
| @@ -613,7 +613,6 @@ export default { | ||||
|             this.calculateScrollbarWidth(); | ||||
|         }, | ||||
|         sortBy(columnKey) { | ||||
|             performance.mark('table:sort'); | ||||
|             // If sorting by the same column, flip the sort direction. | ||||
|             if (this.sortOptions.key === columnKey) { | ||||
|                 if (this.sortOptions.direction === 'asc') { | ||||
| @@ -670,7 +669,6 @@ export default { | ||||
|             this.setHeight(); | ||||
|         }, | ||||
|         rowsAdded(rows) { | ||||
|             performance.mark('row:added'); | ||||
|             this.setHeight(); | ||||
|  | ||||
|             let sizingRow; | ||||
| @@ -692,7 +690,6 @@ export default { | ||||
|             this.updateVisibleRows(); | ||||
|         }, | ||||
|         rowsRemoved(rows) { | ||||
|             performance.mark('row:removed'); | ||||
|             this.setHeight(); | ||||
|             this.updateVisibleRows(); | ||||
|         }, | ||||
|   | ||||
| @@ -366,16 +366,6 @@ $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%); | ||||
|   | ||||
| @@ -370,16 +370,6 @@ $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%); | ||||
|   | ||||
| @@ -366,16 +366,6 @@ $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%); | ||||
|   | ||||
| @@ -70,24 +70,8 @@ | ||||
|         padding: $formTBPad $formLRPad; | ||||
|         text-transform: uppercase; | ||||
|     } | ||||
|  | ||||
|     &--sub-grid { | ||||
|         // 3 columns: <req> <label> <input/control> | ||||
|         display: grid; | ||||
|         grid-column-gap: $interiorMargin; | ||||
|         grid-template-columns: 20px max-content 1fr; | ||||
|         grid-row-gap: $interiorMargin; | ||||
|         margin-top: $interiorMarginLg; | ||||
|         width: max-content; | ||||
|  | ||||
|         .c-form__row { | ||||
|             display: contents; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
|  | ||||
| .c-form-row { | ||||
|     align-items: start; | ||||
|  | ||||
|   | ||||
| @@ -53,7 +53,6 @@ | ||||
| @import "../ui/toolbar/components/toolbar-checkbox.scss"; | ||||
| @import "./notebook.scss"; | ||||
| @import "../plugins/notebook/components/sidebar.scss"; | ||||
| @import "../plugins/gauge/gauge.scss"; | ||||
|  | ||||
| #splash-screen { | ||||
|      display: none; | ||||
|   | ||||
| @@ -51,7 +51,6 @@ export default { | ||||
|     }, | ||||
|     watch: { | ||||
|         value(inputValue) { | ||||
|             eval(inputValue); | ||||
|             if (!inputValue.length) { | ||||
|                 this.clearInput(); | ||||
|             } | ||||
|   | ||||
| @@ -166,8 +166,7 @@ export default { | ||||
|             } | ||||
|  | ||||
|             return definition.form | ||||
|                 .filter(field => !field.hideFromInspector) | ||||
|                 .map(field => { | ||||
|                 .map((field) => { | ||||
|                     let path = field.property; | ||||
|                     if (typeof path === 'string') { | ||||
|                         path = [path]; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user