Compare commits
72 Commits
bleeding-n
...
add-gauge-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
960db3c4fe | ||
|
|
a305017bee | ||
|
|
c33feaa8ba | ||
|
|
3aac86541a | ||
|
|
21c3fcdc8a | ||
|
|
7dffefc17d | ||
|
|
56d8838d58 | ||
|
|
6800d8c4ee | ||
|
|
a630880fe6 | ||
|
|
187be2e970 | ||
|
|
0db937febb | ||
|
|
f428d5915a | ||
|
|
94d5c23592 | ||
|
|
510952551b | ||
|
|
d84a2b5c92 | ||
|
|
fc8a270e03 | ||
|
|
ed0ed5ae65 | ||
|
|
f4e78cb4d4 | ||
|
|
a02cf9864a | ||
|
|
0dcb87cd5d | ||
|
|
e30b58d939 | ||
|
|
17bd9eb923 | ||
|
|
22a501b507 | ||
|
|
600f8559ce | ||
|
|
f198cbaae4 | ||
|
|
205a80a3b0 | ||
|
|
3304be0a70 | ||
|
|
70b9d797c0 | ||
|
|
5d6ae0ddbc | ||
|
|
c467a3ef9e | ||
|
|
2717af20e4 | ||
|
|
e9b8dbcfe8 | ||
|
|
6919067c5e | ||
|
|
5f68ef5632 | ||
|
|
0c529d0697 | ||
|
|
c05812d7a7 | ||
|
|
e671df8064 | ||
|
|
9e071d14c4 | ||
|
|
0424f010a4 | ||
|
|
a5f906d569 | ||
|
|
e7f776f473 | ||
|
|
b99c606b3d | ||
|
|
85478721b5 | ||
|
|
44b517fa66 | ||
|
|
8f762d08c3 | ||
|
|
5989cd622a | ||
|
|
7ab4a80d21 | ||
|
|
2b7d626053 | ||
|
|
c1a4bda230 | ||
|
|
445350e026 | ||
|
|
8260d8c284 | ||
|
|
ad47d0f62c | ||
|
|
fb9c043fbf | ||
|
|
063358226a | ||
|
|
c5bc993870 | ||
|
|
845f9e86df | ||
|
|
beb0ac8ddc | ||
|
|
80620d7705 | ||
|
|
6804ed5c8f | ||
|
|
36eaee4a24 | ||
|
|
84f527c1d3 | ||
|
|
ec531cb08e | ||
|
|
b3fabfefce | ||
|
|
fd400650c7 | ||
|
|
92550af8c1 | ||
|
|
e87eb3d6d7 | ||
|
|
ce7df4c51a | ||
|
|
db288d8c66 | ||
|
|
791ff2e21b | ||
|
|
2e2bf9fd2b | ||
|
|
e0326cf3ea | ||
|
|
b6998b3efa |
@@ -149,15 +149,11 @@ workflows:
|
||||
node-version: lts/fermium
|
||||
browser: ChromeHeadless
|
||||
post-steps:
|
||||
- upload_code_covio
|
||||
- upload_code_covio
|
||||
- unit-test:
|
||||
name: node16-chrome
|
||||
node-version: lts/gallium
|
||||
browser: ChromeHeadless
|
||||
- unit-test:
|
||||
name: node18-chrome
|
||||
node-version: "18"
|
||||
browser: ChromeHeadless
|
||||
browser: ChromeHeadless
|
||||
- e2e-test:
|
||||
name: e2e-ci
|
||||
node-version: lts/gallium
|
||||
@@ -180,10 +176,6 @@ workflows:
|
||||
name: node16-chrome-nightly
|
||||
node-version: lts/gallium
|
||||
browser: ChromeHeadless
|
||||
- unit-test:
|
||||
name: node18-chrome
|
||||
node-version: "18"
|
||||
browser: ChromeHeadless
|
||||
- npm-audit:
|
||||
node-version: lts/gallium
|
||||
- e2e-test:
|
||||
|
||||
2
.github/workflows/e2e-pr.yml
vendored
2
.github/workflows/e2e-pr.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
- run: npm install
|
||||
- run: npm run test:e2e:full
|
||||
- name: Archive test results
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
path: test-results
|
||||
- name: Test success
|
||||
|
||||
1
.github/workflows/pr-platform.yml
vendored
1
.github/workflows/pr-platform.yml
vendored
@@ -18,7 +18,6 @@ jobs:
|
||||
node_version:
|
||||
- 14
|
||||
- 16
|
||||
- 18
|
||||
architecture:
|
||||
- x64
|
||||
name: Node ${{ matrix.node_version }} - ${{ matrix.architecture }} on ${{ matrix.os }}
|
||||
|
||||
31
e2e/tests/plugins/gauge.e2eSpec.js
Normal file
31
e2e/tests/plugins/gauge.e2eSpec.js
Normal file
@@ -0,0 +1,31 @@
|
||||
// 1. create an gauge plugin
|
||||
// 2. create with custom settings
|
||||
// 3. verify required fields are required
|
||||
// 4. should not be able to create gaugue inside a gauge.
|
||||
// 5. should not be able to move/duplicate gaugue inside a gauge.
|
||||
// 6. delete gauge
|
||||
// 7. snapshot gauge
|
||||
// 8. snapshot gauge inside Notebook entry
|
||||
// 9. gauge inside Notebook entry
|
||||
// 10. can drop inside other objects:
|
||||
// can drop to display layout : yes
|
||||
// can drop to Flexible layout : yes
|
||||
// can drop to Folder : yes
|
||||
// can drop to Bar graph: No
|
||||
// can drop to clock: No
|
||||
// . can drop to condition set: No
|
||||
// . can drop to condition widegt: No
|
||||
// // .............all other objects
|
||||
// . can drop to webpage: No
|
||||
// 11. drop telemetry inside a gauge and reflect on data in both fixed and realtime
|
||||
// 12. can have only one telemetry per gague. (show confirmation dialog if want to replace)
|
||||
// 12. all form props (except hidefromInspector) shows inside inspector
|
||||
// 13. should be able to export and import
|
||||
// 14. able to search in tree
|
||||
// 15. supports fix and realtime mode
|
||||
// 17. refresh after creating
|
||||
// 18. open in new tab
|
||||
// 19. two separate gauges inside display layout should not show same numbers unless have same telemetry (should show respctive telemetry data).
|
||||
// 20. inside display layout compare gague values vs its telemetry value (should be same in fixed time and should tick with same rate/value in realtime)
|
||||
// 21. same for two diff gauges- > inside display layout compare gague values vs its telemetry value (should be same in fixed time and should tick with same rate/value in realtime)
|
||||
// 22. export jpeg/png of plugin
|
||||
@@ -1,190 +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.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
Test for plot autoscale.
|
||||
*/
|
||||
|
||||
const { test: _test, expect } = require('@playwright/test');
|
||||
|
||||
// create a new `test` API that will not append platform details to snapshot
|
||||
// file names, only for the tests in this file, so that the same snapshots will
|
||||
// be used for all platforms.
|
||||
const test = _test.extend({
|
||||
_autoSnapshotSuffix: [
|
||||
async ({}, use, testInfo) => {
|
||||
testInfo.snapshotSuffix = '';
|
||||
await use();
|
||||
},
|
||||
{ auto: true }
|
||||
]
|
||||
});
|
||||
|
||||
test.use({
|
||||
viewport: {
|
||||
width: 1280,
|
||||
height: 720
|
||||
}
|
||||
});
|
||||
|
||||
test.describe('ExportAsJSON', () => {
|
||||
test.only('autoscale off causes no error from undefined user range', async ({ page }) => {
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
await setTimeRange(page);
|
||||
|
||||
await createSinewaveOverlayPlot(page);
|
||||
|
||||
await testYTicks(page, ['-1.00', '-0.50', '0.00', '0.50', '1.00']);
|
||||
|
||||
await turnOffAutoscale(page);
|
||||
|
||||
const canvas = page.locator('canvas').nth(1);
|
||||
|
||||
// Make sure that after turning off autoscale, the user selected range values start at the same values the plot had prior.
|
||||
await Promise.all([
|
||||
testYTicks(page, ['-1.00', '-0.50', '0.00', '0.50', '1.00']),
|
||||
new Promise(r => setTimeout(r, 100))
|
||||
.then(() => canvas.screenshot())
|
||||
.then(shot => expect(shot).toMatchSnapshot('autoscale-canvas-prepan.png', { maxDiffPixels: 40 }))
|
||||
]);
|
||||
|
||||
let errorCount = 0;
|
||||
|
||||
function onError() {
|
||||
errorCount++;
|
||||
}
|
||||
|
||||
page.on('pageerror', onError);
|
||||
|
||||
await page.keyboard.down('Alt');
|
||||
|
||||
await canvas.dragTo(canvas, {
|
||||
sourcePosition: {
|
||||
x: 200,
|
||||
y: 200
|
||||
},
|
||||
targetPosition: {
|
||||
x: 400,
|
||||
y: 400
|
||||
}
|
||||
});
|
||||
|
||||
await page.keyboard.up('Alt');
|
||||
|
||||
page.off('pageerror', onError);
|
||||
|
||||
// There would have been an error at this point. So if there isn't, then
|
||||
// we fixed it.
|
||||
expect(errorCount).toBe(0);
|
||||
|
||||
// Ensure the drag worked.
|
||||
await Promise.all([
|
||||
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: 20 }))
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} start
|
||||
* @param {string} end
|
||||
*/
|
||||
async function setTimeRange(page, start = '2022-03-29 22:00:00.000Z', end = '2022-03-29 22:00:30.000Z') {
|
||||
// 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(start);
|
||||
|
||||
await timeInputs.nth(1).click();
|
||||
await timeInputs.nth(1).fill(end);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function createSinewaveOverlayPlot(page) {
|
||||
// click create button
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
|
||||
// add overlay plot with defaults
|
||||
await page.locator('li:has-text("Overlay Plot")').click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/a9268c6f-45cc-4bcd-a6a0-50ac4036e396?tc.mode=fixed&tc.startBound=1649305424163&tc.endBound=1649307224163&tc.timeSystem=utc&view=plot-overlay' }*/),
|
||||
page.locator('text=OK').click()
|
||||
]);
|
||||
|
||||
// save (exit edit mode)
|
||||
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();
|
||||
|
||||
// click create button
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
|
||||
// add sine wave generator with defaults
|
||||
await page.locator('li:has-text("Sine Wave Generator")').click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/a9268c6f-45cc-4bcd-a6a0-50ac4036e396/5cfa5c69-17bc-4a99-9545-4da8125380c5?tc.mode=fixed&tc.startBound=1649305424163&tc.endBound=1649307224163&tc.timeSystem=utc&view=plot-single' }*/),
|
||||
page.locator('text=OK').click()
|
||||
]);
|
||||
|
||||
// focus the 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/a9268c6f-45cc-4bcd-a6a0-50ac4036e396?tc.mode=fixed&tc.startBound=1649305424163&tc.endBound=1649307224163&tc.timeSystem=utc&view=plot-overlay' }*/),
|
||||
page.locator('text=Unnamed Overlay Plot').first().click()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function turnOffAutoscale(page) {
|
||||
// enter edit mode
|
||||
await page.locator('text=Unnamed Overlay Plot Snapshot >> button').nth(3).click();
|
||||
|
||||
// uncheck autoscale
|
||||
await page.locator('text=Y Axis Scaling Auto scale Padding >> input[type="checkbox"]').uncheck();
|
||||
|
||||
// save
|
||||
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 testYTicks(page, values) {
|
||||
const yTicks = page.locator('.gl-plot-y-tick-label');
|
||||
let promises = [yTicks.count().then(c => expect(c).toBe(values.length))];
|
||||
|
||||
for (let i = 0, l = values.length; i < l; i += 1) {
|
||||
promises.push(expect(yTicks.nth(i)).toHaveText(values[i])); // eslint-disable-line
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 26 KiB |
@@ -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());
|
||||
|
||||
14
package.json
14
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openmct",
|
||||
"version": "2.0.3-SNAPSHOT",
|
||||
"version": "2.0.2",
|
||||
"description": "The Open MCT core platform",
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "7.16.3",
|
||||
@@ -23,7 +23,7 @@
|
||||
"d3-axis": "1.0.x",
|
||||
"d3-scale": "1.0.x",
|
||||
"d3-selection": "1.3.x",
|
||||
"eslint": "8.13.0",
|
||||
"eslint": "8.11.0",
|
||||
"eslint-plugin-compat": "4.0.2",
|
||||
"eslint-plugin-playwright": "0.8.0",
|
||||
"eslint-plugin-vue": "8.5.0",
|
||||
@@ -37,7 +37,7 @@
|
||||
"imports-loader": "0.8.0",
|
||||
"jasmine-core": "4.0.1",
|
||||
"jsdoc": "3.5.5",
|
||||
"karma": "6.3.18",
|
||||
"karma": "6.3.15",
|
||||
"karma-chrome-launcher": "3.1.1",
|
||||
"karma-cli": "2.0.0",
|
||||
"karma-coverage": "2.1.1",
|
||||
@@ -46,14 +46,14 @@
|
||||
"karma-jasmine": "4.0.1",
|
||||
"karma-junit-reporter": "2.0.1",
|
||||
"karma-sourcemap-loader": "0.3.8",
|
||||
"karma-spec-reporter": "0.0.34",
|
||||
"karma-spec-reporter": "0.0.33",
|
||||
"karma-webpack": "5.0.0",
|
||||
"lighthouse": "9.5.0",
|
||||
"location-bar": "3.0.1",
|
||||
"lodash": "4.17.21",
|
||||
"mini-css-extract-plugin": "2.6.0",
|
||||
"moment": "2.29.1",
|
||||
"moment-duration-format": "2.3.2",
|
||||
"moment-duration-format": "2.2.2",
|
||||
"moment-timezone": "0.5.34",
|
||||
"node-bourbon": "4.2.3",
|
||||
"painterro": "1.2.56",
|
||||
@@ -61,14 +61,14 @@
|
||||
"plotly.js-gl2d-dist": "2.5.0",
|
||||
"printj": "1.3.1",
|
||||
"request": "2.88.2",
|
||||
"resolve-url-loader": "5.0.0",
|
||||
"resolve-url-loader": "4.0.0",
|
||||
"sass": "1.49.9",
|
||||
"sass-loader": "12.6.0",
|
||||
"sinon": "13.0.1",
|
||||
"style-loader": "^1.0.1",
|
||||
"uuid": "3.3.3",
|
||||
"vue": "2.6.14",
|
||||
"vue-eslint-parser": "8.3.0",
|
||||
"vue-eslint-parser": "8.2.0",
|
||||
"vue-loader": "15.9.8",
|
||||
"vue-template-compiler": "2.6.14",
|
||||
"webpack": "5.68.0",
|
||||
|
||||
@@ -242,6 +242,7 @@ define([
|
||||
|
||||
// Plugins that are installed by default
|
||||
|
||||
this.install(this.plugins.Gauge());
|
||||
this.install(this.plugins.Plot());
|
||||
this.install(this.plugins.Chart());
|
||||
this.install(this.plugins.TelemetryTable.default());
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import AutoCompleteField from './components/controls/AutoCompleteField.vue';
|
||||
import ClockDisplayFormatField from './components/controls/ClockDisplayFormatField.vue';
|
||||
import CheckBoxField from './components/controls/CheckBoxField.vue';
|
||||
import Datetime from './components/controls/Datetime.vue';
|
||||
import FileInput from './components/controls/FileInput.vue';
|
||||
import Locator from './components/controls/Locator.vue';
|
||||
@@ -7,11 +8,13 @@ import NumberField from './components/controls/NumberField.vue';
|
||||
import SelectField from './components/controls/SelectField.vue';
|
||||
import TextAreaField from './components/controls/TextAreaField.vue';
|
||||
import TextField from './components/controls/TextField.vue';
|
||||
import ToggleSwitchField from './components/controls/ToggleSwitchField.vue';
|
||||
|
||||
import Vue from 'vue';
|
||||
|
||||
export const DEFAULT_CONTROLS_MAP = {
|
||||
'autocomplete': AutoCompleteField,
|
||||
'checkbox': CheckBoxField,
|
||||
'composite': ClockDisplayFormatField,
|
||||
'datetime': Datetime,
|
||||
'file-input': FileInput,
|
||||
@@ -19,7 +22,8 @@ export const DEFAULT_CONTROLS_MAP = {
|
||||
'numberfield': NumberField,
|
||||
'select': SelectField,
|
||||
'textarea': TextAreaField,
|
||||
'textfield': TextField
|
||||
'textfield': TextField,
|
||||
'toggleSwitch': ToggleSwitchField
|
||||
};
|
||||
|
||||
export default class FormControl {
|
||||
@@ -94,4 +98,3 @@ export default class FormControl {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -79,10 +79,12 @@ export default {
|
||||
rowClass() {
|
||||
let cssClass = this.cssClass;
|
||||
|
||||
if (this.row.required) {
|
||||
cssClass = `${cssClass} req`;
|
||||
if (!this.row.required) {
|
||||
return;
|
||||
}
|
||||
|
||||
cssClass = `${cssClass} req`;
|
||||
|
||||
if (this.visited && this.valid !== undefined) {
|
||||
if (this.valid === true) {
|
||||
cssClass = `${cssClass} valid`;
|
||||
|
||||
55
src/api/forms/components/controls/CheckBoxField.vue
Normal file
55
src/api/forms/components/controls/CheckBoxField.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
<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,7 +58,6 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
updateText() {
|
||||
console.log('updateText', this.field);
|
||||
const data = {
|
||||
model: this.model,
|
||||
value: this.field
|
||||
|
||||
62
src/api/forms/components/controls/ToggleSwitchField.vue
Normal file
62
src/api/forms/components/controls/ToggleSwitchField.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
<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>
|
||||
19
src/api/forms/toggle-check-box-mixin.js
Normal file
19
src/api/forms/toggle-check-box-mixin.js
Normal file
@@ -0,0 +1,19 @@
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
isChecked: false
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
toggleCheckBox(event) {
|
||||
this.isChecked = !this.isChecked;
|
||||
|
||||
const data = {
|
||||
model: this.model,
|
||||
value: this.isChecked
|
||||
};
|
||||
|
||||
this.$emit('onChange', data);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -90,6 +90,9 @@ export default class CreateWizard {
|
||||
rows: this.properties.map(property => {
|
||||
const row = JSON.parse(JSON.stringify(property));
|
||||
row.value = this.getValue(row);
|
||||
if (property.validate) {
|
||||
row.validate = property.validate;
|
||||
}
|
||||
|
||||
return row;
|
||||
}).filter(row => row && row.control)
|
||||
|
||||
221
src/plugins/gauge/GaugePlugin.js
Normal file
221
src/plugins/gauge/GaugePlugin.js
Normal file
@@ -0,0 +1,221 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2018, 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 = [
|
||||
['Dial', 'dial'],
|
||||
['Vertical Meter', 'meter-vertical'],
|
||||
['Vertical Meter Inverted', 'meter-vertical-inverted'],
|
||||
['Horizontal Meter', 'meter-horizontal']
|
||||
];
|
||||
|
||||
export const GAUGE_DISPLAY_STYLES = [
|
||||
['Bar', 'bar'],
|
||||
['Needle', 'needle']
|
||||
];
|
||||
|
||||
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],
|
||||
gaugeDisplayStyle: GAUGE_DISPLAY_STYLES[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: "Display style",
|
||||
options: GAUGE_DISPLAY_STYLES.map(type => {
|
||||
return {
|
||||
name: type[0],
|
||||
value: type[1]
|
||||
};
|
||||
}),
|
||||
control: "select",
|
||||
cssClass: "l-input-sm",
|
||||
key: "gaugeController",
|
||||
property: [
|
||||
"configuration",
|
||||
"gaugeController",
|
||||
"gaugeDisplayStyle"
|
||||
]
|
||||
},
|
||||
{
|
||||
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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
65
src/plugins/gauge/GaugeViewProvider.js
Normal file
65
src/plugins/gauge/GaugeViewProvider.js
Normal file
@@ -0,0 +1,65 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2018, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import GaugeComponent from './components/Gauge.vue';
|
||||
import Vue from 'vue';
|
||||
|
||||
export default function GaugeViewProvider(openmct) {
|
||||
return {
|
||||
key: 'gauge',
|
||||
name: 'Gauge',
|
||||
cssClass: 'icon-gauge',
|
||||
canView: function (domainObject) {
|
||||
return domainObject.type === 'gauge';
|
||||
},
|
||||
canEdit: function (domainObject) {
|
||||
return false;
|
||||
},
|
||||
view: function (domainObject) {
|
||||
let component;
|
||||
|
||||
return {
|
||||
show: function (element) {
|
||||
component = new Vue({
|
||||
el: element,
|
||||
components: {
|
||||
GaugeComponent
|
||||
},
|
||||
provide: {
|
||||
openmct,
|
||||
domainObject,
|
||||
composition: openmct.composition.get(domainObject)
|
||||
},
|
||||
template: '<gauge-component></gauge-component>'
|
||||
});
|
||||
},
|
||||
destroy: function (element) {
|
||||
component.$destroy();
|
||||
component = undefined;
|
||||
}
|
||||
};
|
||||
},
|
||||
priority: function () {
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
}
|
||||
443
src/plugins/gauge/components/Gauge.vue
Normal file
443
src/plugins/gauge/components/Gauge.vue
Normal file
@@ -0,0 +1,443 @@
|
||||
<template>
|
||||
<div
|
||||
class="c-gauge"
|
||||
:class="`c-gauge--${gaugeType} c-gauge--style-${gaugeDisplayStyle}`"
|
||||
>
|
||||
<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 && styleBar,
|
||||
'c-dial-clip--180': degValue >= 90 && degValue < 180 && styleBar
|
||||
}"
|
||||
>
|
||||
<path
|
||||
v-if="styleBar && 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="styleNeedle && 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,
|
||||
gaugeDisplayStyle: gaugeController.gaugeDisplayStyle
|
||||
};
|
||||
},
|
||||
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');
|
||||
},
|
||||
typeMeter() {
|
||||
return this.matchGaugeType('meter');
|
||||
},
|
||||
typeMeterHorizontal() {
|
||||
return this.matchGaugeType('horizontal');
|
||||
},
|
||||
typeMeterVertical() {
|
||||
return this.matchGaugeType('vertical');
|
||||
},
|
||||
typeMeterInverted() {
|
||||
return this.matchGaugeType('inverted');
|
||||
},
|
||||
styleBar() {
|
||||
return this.matchGaugeDisplayStyle('bar');
|
||||
},
|
||||
styleNeedle() {
|
||||
return this.matchGaugeDisplayStyle('needle');
|
||||
},
|
||||
meterValueToPerc() {
|
||||
const meterDirection = (this.typeMeterInverted) ? -1 : 1;
|
||||
|
||||
// For bar meter, don't move the bar if it's not visible
|
||||
if (this.styleBar && this.curVal <= this.rangeLow) {
|
||||
return meterDirection * 100;
|
||||
}
|
||||
// For bar meter, don't move the bar if it's beyond 100% of the range
|
||||
if (this.styleBar && 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);
|
||||
}
|
||||
},
|
||||
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);
|
||||
},
|
||||
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);
|
||||
},
|
||||
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();
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
},
|
||||
getDateValueFormatter() {
|
||||
const timeSystem = this.openmct.time.timeSystem();
|
||||
const metadataValue = this.metadata.value(timeSystem.key) || { format: timeSystem.key };
|
||||
|
||||
return this.openmct.telemetry.getValueFormatter(metadataValue);
|
||||
},
|
||||
matchGaugeType(str) {
|
||||
return this.gaugeType.indexOf(str) !== -1;
|
||||
},
|
||||
matchGaugeDisplayStyle(str) {
|
||||
return this.gaugeDisplayStyle.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;
|
||||
},
|
||||
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) {
|
||||
const dateValueFormatter = this.getDateValueFormatter();
|
||||
const parsedValue = dateValueFormatter.parse(datum);
|
||||
const { start, end } = this.openmct.time.bounds();
|
||||
|
||||
const beforeStartOfBounds = parsedValue < start;
|
||||
const afterEndOfBounds = parsedValue > end;
|
||||
if (afterEndOfBounds || beforeStartOfBounds) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isRendering) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isRendering = true;
|
||||
requestAnimationFrame(() => {
|
||||
this.isRendering = false;
|
||||
|
||||
this.curVal = this.round(this.formats[this.valueKey].format(datum), this.precision);
|
||||
});
|
||||
},
|
||||
valToPercent(vValue) {
|
||||
// Used by dial
|
||||
if (vValue >= this.rangeHigh && this.styleBar) {
|
||||
// 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>
|
||||
172
src/plugins/gauge/components/GaugeFormController.vue
Normal file
172
src/plugins/gauge/components/GaugeFormController.vue
Normal file
@@ -0,0 +1,172 @@
|
||||
/*****************************************************************************
|
||||
* 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
|
||||
id="isUseTelemetryLimits"
|
||||
: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,
|
||||
gaugeDisplayStyle: this.model.value.gaugeDisplayStyle,
|
||||
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>
|
||||
299
src/plugins/gauge/gauge.scss
Normal file
299
src/plugins/gauge/gauge.scss
Normal file
@@ -0,0 +1,299 @@
|
||||
$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;
|
||||
}
|
||||
}
|
||||
|
||||
/********************************************** HORIZONTAL AND VERTICAL METERS */
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
/*********************************************** VERTICAL METER */
|
||||
[class*='c-gauge--meter-vertical'] {
|
||||
.c-meter {
|
||||
&__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;
|
||||
}
|
||||
|
||||
&__limit-low {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
&__limit-high {
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&[class*='--style-needle'] {
|
||||
.c-meter__value {
|
||||
border-top: 3px solid $colorGaugeValue;
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
&[class*='inverted'] {
|
||||
.c-meter {
|
||||
&__limit-low {
|
||||
bottom: auto;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
&__limit-high {
|
||||
bottom: 0;
|
||||
top: auto;
|
||||
}
|
||||
|
||||
&__range__low {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
&__range__high {
|
||||
order: 2;
|
||||
}
|
||||
}
|
||||
|
||||
&[class*='--style-needle'] {
|
||||
.c-meter__value {
|
||||
border-top: none;
|
||||
border-bottom: 3px solid $colorGaugeValue;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*********************************************** HORIZONTAL METER */
|
||||
[class*='c-gauge--meter-horizontal'] {
|
||||
.c-meter {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
&[class*='--style-needle'] {
|
||||
.c-meter {
|
||||
&__value {
|
||||
border-right: 3px solid $colorGaugeValue;
|
||||
bottom: 0;
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ $elemBg: rgba(black, 0.7);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
z-index: 2;
|
||||
@include userSelectNone;
|
||||
}
|
||||
|
||||
|
||||
@@ -536,7 +536,7 @@ export default {
|
||||
this.updateRelatedTelemetryForFocusedImage = _.debounce(this.updateRelatedTelemetryForFocusedImage, 400);
|
||||
|
||||
// for resizing the object view
|
||||
this.resizeImageContainer = _.debounce(this.resizeImageContainer, 400, { leading: true });
|
||||
this.resizeImageContainer = _.debounce(this.resizeImageContainer, 400);
|
||||
|
||||
if (this.$refs.imageBG) {
|
||||
this.imageContainerResizeObserver = new ResizeObserver(this.resizeImageContainer);
|
||||
|
||||
@@ -222,7 +222,7 @@
|
||||
.h-local-controls--overlay-content {
|
||||
position: absolute;
|
||||
left: $interiorMargin; top: $interiorMargin;
|
||||
z-index: 2;
|
||||
z-index: 70;
|
||||
background: $colorLocalControlOvrBg;
|
||||
border-radius: $basicCr;
|
||||
max-width: 250px;
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
v-if="seriesModels.length > 0"
|
||||
:tick-width="tickWidth"
|
||||
:single-series="seriesModels.length === 1"
|
||||
:has-same-range-value="hasSameRangeValue"
|
||||
:series-model="seriesModels[0]"
|
||||
:style="{
|
||||
left: (plotWidth - tickWidth) + 'px'
|
||||
@@ -251,8 +250,7 @@ export default {
|
||||
loaded: false,
|
||||
isTimeOutOfSync: false,
|
||||
showLimitLineLabels: undefined,
|
||||
isFrozenOnMouseDown: false,
|
||||
hasSameRangeValue: true
|
||||
isFrozenOnMouseDown: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -364,7 +362,6 @@ export default {
|
||||
this.setDisplayRange(series, xKey);
|
||||
}, this);
|
||||
this.listenTo(series, 'change:yKey', () => {
|
||||
this.checkSameRangeValue();
|
||||
this.loadSeriesData(series);
|
||||
}, this);
|
||||
|
||||
@@ -372,18 +369,10 @@ export default {
|
||||
this.loadSeriesData(series);
|
||||
}, this);
|
||||
|
||||
this.checkSameRangeValue();
|
||||
this.loadSeriesData(series);
|
||||
},
|
||||
|
||||
checkSameRangeValue() {
|
||||
this.hasSameRangeValue = this.seriesModels.every((model) => {
|
||||
return model.get('yKey') === this.seriesModels[0].get('yKey');
|
||||
});
|
||||
},
|
||||
|
||||
removeSeries(plotSeries) {
|
||||
this.checkSameRangeValue();
|
||||
this.stopListening(plotSeries);
|
||||
},
|
||||
|
||||
@@ -499,7 +488,7 @@ export default {
|
||||
},
|
||||
|
||||
setDisplayRange(series, xKey) {
|
||||
if (this.config.series.models.length !== 1) {
|
||||
if (this.config.series.length !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,9 +29,9 @@
|
||||
>
|
||||
|
||||
<div
|
||||
v-if="canShowYAxisLabel"
|
||||
v-if="singleSeries"
|
||||
class="gl-plot-label gl-plot-y-label"
|
||||
:class="{'icon-gear': (yKeyOptions.length > 1 && singleSeries)}"
|
||||
:class="{'icon-gear': (yKeyOptions.length > 1)}"
|
||||
>{{ yAxisLabel }}
|
||||
</div>
|
||||
|
||||
@@ -76,12 +76,6 @@ export default {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
hasSameRangeValue: {
|
||||
type: Boolean,
|
||||
default() {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
seriesModel: {
|
||||
type: Object,
|
||||
default() {
|
||||
@@ -101,11 +95,6 @@ export default {
|
||||
loaded: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
canShowYAxisLabel() {
|
||||
return this.singleSeries === true || this.hasSameRangeValue === true;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.yAxis = this.getYAxisFromConfig();
|
||||
this.loaded = true;
|
||||
|
||||
@@ -157,8 +157,7 @@ export default class PlotConfigurationModel extends Model {
|
||||
@typedef {{
|
||||
configuration: {
|
||||
series: import('./PlotSeries').PlotSeriesModelType[]
|
||||
yAxis: import('./YAxisModel').YAxisModelType
|
||||
},
|
||||
}
|
||||
}} SomeDomainObject_NeedsName
|
||||
*/
|
||||
|
||||
|
||||
@@ -250,7 +250,6 @@ export default class PlotSeries extends Model {
|
||||
this.evaluate = function (datum) {
|
||||
return this.limitEvaluator.evaluate(datum, valueMetadata);
|
||||
}.bind(this);
|
||||
this.set('unit', valueMetadata.unit);
|
||||
const format = this.formats[newKey];
|
||||
this.getYVal = format.parse.bind(format);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import Model from './Model';
|
||||
import Model from "./Model";
|
||||
|
||||
/**
|
||||
* @extends {Model<XAxisModelType, XAxisModelOptions>}
|
||||
@@ -49,11 +49,11 @@ export default class XAxisModel extends Model {
|
||||
}
|
||||
});
|
||||
|
||||
this.on('change:frozen', (frozen) => {
|
||||
this.on('change:frozen', ((frozen) => {
|
||||
if (!frozen) {
|
||||
this.set('range', this.get('range'));
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
if (this.get('range')) {
|
||||
this.set('range', this.get('range'));
|
||||
@@ -126,7 +126,7 @@ export default class XAxisModel extends Model {
|
||||
|
||||
/**
|
||||
@typedef {import("./Model").ModelType<{
|
||||
range?: NumberRange
|
||||
range: NumberRange
|
||||
displayRange: NumberRange
|
||||
frozen: boolean
|
||||
label: string
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import _ from 'lodash';
|
||||
import Model from './Model';
|
||||
|
||||
/**
|
||||
@@ -62,14 +63,14 @@ export default class YAxisModel extends Model {
|
||||
*/
|
||||
listenToSeriesCollection(seriesCollection) {
|
||||
this.seriesCollection = seriesCollection;
|
||||
this.listenTo(this.seriesCollection, 'add', series => {
|
||||
this.listenTo(this.seriesCollection, 'add', (series => {
|
||||
this.trackSeries(series);
|
||||
this.updateFromSeries(this.seriesCollection);
|
||||
}, this);
|
||||
this.listenTo(this.seriesCollection, 'remove', series => {
|
||||
}), this);
|
||||
this.listenTo(this.seriesCollection, 'remove', (series => {
|
||||
this.untrackSeries(series);
|
||||
this.updateFromSeries(this.seriesCollection);
|
||||
}, this);
|
||||
}), this);
|
||||
this.seriesCollection.forEach(this.trackSeries, this);
|
||||
this.updateFromSeries(this.seriesCollection);
|
||||
}
|
||||
@@ -139,11 +140,11 @@ export default class YAxisModel extends Model {
|
||||
}
|
||||
resetStats() {
|
||||
this.unset('stats');
|
||||
this.seriesCollection.forEach(series => {
|
||||
this.seriesCollection.forEach(function (series) {
|
||||
if (series.has('stats')) {
|
||||
this.updateStats(series.get('stats'));
|
||||
}
|
||||
});
|
||||
}, this);
|
||||
}
|
||||
/**
|
||||
* @param {import('./PlotSeries').default} series
|
||||
@@ -169,18 +170,7 @@ export default class YAxisModel extends Model {
|
||||
if (autoscale && this.has('stats')) {
|
||||
this.set('displayRange', this.applyPadding(this.get('stats')));
|
||||
} else {
|
||||
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.
|
||||
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('displayRange', this.get('range'));
|
||||
}
|
||||
}
|
||||
/**
|
||||
@@ -189,7 +179,7 @@ export default class YAxisModel extends Model {
|
||||
*/
|
||||
updateFromSeries(seriesCollection) {
|
||||
const plotModel = this.plot.get('domainObject');
|
||||
const label = plotModel?.configuration?.yAxis?.label;
|
||||
const label = _.get(plotModel, 'configuration.yAxis.label');
|
||||
const sampleSeries = seriesCollection.first();
|
||||
if (!sampleSeries) {
|
||||
if (!label) {
|
||||
@@ -205,19 +195,19 @@ export default class YAxisModel extends Model {
|
||||
this.set('format', yFormat.format.bind(yFormat));
|
||||
this.set('values', yMetadata.values);
|
||||
if (!label) {
|
||||
const labelName = seriesCollection
|
||||
.map(s => (s.metadata ? s.metadata.value(s.get('yKey')).name : ''))
|
||||
.reduce((a, b) => {
|
||||
if (a === undefined) {
|
||||
return b;
|
||||
}
|
||||
const labelName = seriesCollection.map(function (s) {
|
||||
return s.metadata ? s.metadata.value(s.get('yKey')).name : '';
|
||||
}).reduce(function (a, b) {
|
||||
if (a === undefined) {
|
||||
return b;
|
||||
}
|
||||
|
||||
if (a === b) {
|
||||
return a;
|
||||
}
|
||||
if (a === b) {
|
||||
return a;
|
||||
}
|
||||
|
||||
return '';
|
||||
}, undefined);
|
||||
return '';
|
||||
}, undefined);
|
||||
|
||||
if (labelName) {
|
||||
this.set('label', labelName);
|
||||
@@ -225,19 +215,19 @@ export default class YAxisModel extends Model {
|
||||
return;
|
||||
}
|
||||
|
||||
const labelUnits = seriesCollection
|
||||
.map(s => (s.metadata ? s.metadata.value(s.get('yKey')).units : ''))
|
||||
.reduce((a, b) => {
|
||||
if (a === undefined) {
|
||||
return b;
|
||||
}
|
||||
const labelUnits = seriesCollection.map(function (s) {
|
||||
return s.metadata ? s.metadata.value(s.get('yKey')).units : '';
|
||||
}).reduce(function (a, b) {
|
||||
if (a === undefined) {
|
||||
return b;
|
||||
}
|
||||
|
||||
if (a === b) {
|
||||
return a;
|
||||
}
|
||||
if (a === b) {
|
||||
return a;
|
||||
}
|
||||
|
||||
return '';
|
||||
}, undefined);
|
||||
return '';
|
||||
}, undefined);
|
||||
|
||||
if (labelUnits) {
|
||||
this.set('label', labelUnits);
|
||||
@@ -249,17 +239,14 @@ export default class YAxisModel extends Model {
|
||||
/**
|
||||
* @override
|
||||
* @param {import('./Model').ModelOptions<YAxisModelType, YAxisModelOptions>} options
|
||||
* @returns {Partial<YAxisModelType>}
|
||||
* @returns {YAxisModelType}
|
||||
*/
|
||||
defaultModel(options) {
|
||||
// @ts-ignore incomplete YAxisModelType object used for default value.
|
||||
return {
|
||||
frozen: false,
|
||||
autoscale: true,
|
||||
autoscalePadding: 0.1
|
||||
|
||||
// 'range' is not specified here, it is undefined at first. When the
|
||||
// user turns off autoscale, the current 'displayRange' is used for
|
||||
// the initial value of 'range'.
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -270,7 +257,7 @@ export default class YAxisModel extends Model {
|
||||
@typedef {import('./XAxisModel').AxisModelType & {
|
||||
autoscale: boolean
|
||||
autoscalePadding: number
|
||||
stats?: import('./XAxisModel').NumberRange
|
||||
stats: import('./XAxisModel').NumberRange
|
||||
values: Array<TODO>
|
||||
}} YAxisModelType
|
||||
*/
|
||||
|
||||
@@ -52,10 +52,10 @@
|
||||
class="l-inspector-part"
|
||||
>
|
||||
<div
|
||||
v-show="!autoscale && validationErrors.range"
|
||||
v-show="!autoscale && validation.range"
|
||||
class="grid-span-all form-error"
|
||||
>
|
||||
{{ validationErrors.range }}
|
||||
{{ validation.range }}
|
||||
</div>
|
||||
<li class="grid-row force-border">
|
||||
<div
|
||||
@@ -88,7 +88,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { objectPath } from "./formUtil";
|
||||
import { objectPath, validate, coerce } from "./formUtil";
|
||||
import _ from "lodash";
|
||||
|
||||
export default {
|
||||
@@ -108,7 +108,7 @@ export default {
|
||||
autoscalePadding: '',
|
||||
rangeMin: '',
|
||||
rangeMax: '',
|
||||
validationErrors: {}
|
||||
validation: {}
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
@@ -178,6 +178,12 @@ export default {
|
||||
if (Number(range.min) > Number(range.max)) {
|
||||
return 'Minimum must be less than Maximum.';
|
||||
}
|
||||
|
||||
if (model.get('autoscale')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
];
|
||||
@@ -186,9 +192,14 @@ export default {
|
||||
this.label = this.yAxis.get('label');
|
||||
this.autoscale = this.yAxis.get('autoscale');
|
||||
this.autoscalePadding = this.yAxis.get('autoscalePadding');
|
||||
const range = this.yAxis.get('range') ?? this.yAxis.get('displayRange');
|
||||
this.rangeMin = range?.min;
|
||||
this.rangeMax = range?.max;
|
||||
const range = this.yAxis.get('range');
|
||||
if (!range) {
|
||||
this.rangeMin = undefined;
|
||||
this.rangeMax = undefined;
|
||||
} else {
|
||||
this.rangeMin = range.min;
|
||||
this.rangeMax = range.max;
|
||||
}
|
||||
},
|
||||
updateForm(formKey) {
|
||||
let newVal;
|
||||
@@ -201,27 +212,26 @@ export default {
|
||||
newVal = this[formKey];
|
||||
}
|
||||
|
||||
let oldVal = this.yAxis.get(formKey);
|
||||
const oldVal = this.yAxis.get(formKey);
|
||||
const formField = this.fields.find((field) => field.modelProp === formKey);
|
||||
|
||||
const validationError = formField.validate?.(newVal, this.yAxis);
|
||||
this.validationErrors[formKey] = validationError;
|
||||
if (validationError) {
|
||||
const path = objectPath(formField.objectPath);
|
||||
const validationResult = validate(newVal, this.yAxis, formField.validate);
|
||||
if (validationResult === true) {
|
||||
delete this.validation[formKey];
|
||||
} else {
|
||||
this.validation[formKey] = validationResult;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
newVal = formField.coerce?.(newVal) ?? newVal;
|
||||
oldVal = formField.coerce?.(oldVal) ?? oldVal;
|
||||
|
||||
const path = objectPath(formField.objectPath);
|
||||
if (!_.isEqual(newVal, oldVal)) {
|
||||
// TODO: Why do we mutate yAxis twice, once directly, once via objects.mutate? Or are they different objects?
|
||||
this.yAxis.set(formKey, newVal);
|
||||
if (!_.isEqual(coerce(newVal, formField.coerce), coerce(oldVal, formField.coerce))) {
|
||||
this.yAxis.set(formKey, coerce(newVal, formField.coerce));
|
||||
if (path) {
|
||||
this.openmct.objects.mutate(
|
||||
this.domainObject,
|
||||
path(this.domainObject, this.yAxis),
|
||||
newVal
|
||||
coerce(newVal, formField.coerce)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,5 +15,15 @@ export function validate(value, model, validateFunc) {
|
||||
}
|
||||
|
||||
export function objectPath(path) {
|
||||
return path && typeof path !== 'function' ? () => path : path;
|
||||
if (path) {
|
||||
if (typeof path !== "function") {
|
||||
const staticObjectPath = path;
|
||||
|
||||
return function (object, model) {
|
||||
return staticObjectPath;
|
||||
};
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ define([
|
||||
'./userIndicator/plugin',
|
||||
'../../example/exampleUser/plugin',
|
||||
'./localStorage/plugin',
|
||||
'./gauge/GaugePlugin',
|
||||
'./timelist/plugin'
|
||||
], function (
|
||||
_,
|
||||
@@ -135,6 +136,7 @@ define([
|
||||
UserIndicator,
|
||||
ExampleUser,
|
||||
LocalStorage,
|
||||
GaugePlugin,
|
||||
TimeList
|
||||
) {
|
||||
const plugins = {};
|
||||
@@ -212,6 +214,7 @@ define([
|
||||
plugins.DeviceClassifier = DeviceClassifier.default;
|
||||
plugins.UserIndicator = UserIndicator.default;
|
||||
plugins.LocalStorage = LocalStorage.default;
|
||||
plugins.Gauge = GaugePlugin.default;
|
||||
plugins.Timelist = TimeList.default;
|
||||
|
||||
return plugins;
|
||||
|
||||
@@ -9,11 +9,7 @@ export default class TelemetryTableView {
|
||||
this.objectPath = objectPath;
|
||||
this.component = undefined;
|
||||
|
||||
Object.defineProperty(this, 'table', {
|
||||
value: new TelemetryTable(domainObject, openmct),
|
||||
enumerable: false,
|
||||
configurable: false
|
||||
});
|
||||
this.table = new TelemetryTable(domainObject, openmct);
|
||||
}
|
||||
|
||||
getViewContext() {
|
||||
|
||||
@@ -229,7 +229,7 @@ export default {
|
||||
} else if (durationIndex === 1) {
|
||||
return duration * 60 * 1000;
|
||||
} else if (durationIndex === 2) {
|
||||
return duration * 60 * 60 * 1000;
|
||||
return duration * 60 * 24 * 1000;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -366,6 +366,16 @@ $legendHoverValueBg: rgba($colorBodyFg, 0.2);
|
||||
$legendTableHeadBg: $colorTabHeaderBg;
|
||||
$colorPlotLimitLineBg: rgba($colorBodyBg, 0.2);
|
||||
|
||||
// Gauges
|
||||
$colorGaugeBg: pullForward($colorBodyBg, 5%); // Gauge radial area background, meter background
|
||||
$colorGaugeValue: rgba(#fff, 0.3); // Gauge value graphic (radial sweep, bar) color
|
||||
$colorGaugeTextValue: #fff; // Radial gauge text value
|
||||
$colorGaugeMeterTextValue: $colorGaugeTextValue; // Meter text value, overlaid on value bar
|
||||
$colorGaugeRange: $colorBodyFg; // Range text color
|
||||
$colorGaugeLimitHigh: rgba($colorLimitRedBg, 0.4);
|
||||
$colorGaugeLimitLow: $colorGaugeLimitHigh;
|
||||
$transitionTimeGauge: 150ms; // CSS transition time used to smooth needle gauge and meter value transitions
|
||||
$marginGaugeMeterValue: 10%; // Margin between meter value bar and bounds edges
|
||||
// Time Strip and Lists
|
||||
$colorCurrentBg: rgba($colorStatusAlert, 0.3);
|
||||
$colorCurrentFg: pullForward($colorBodyFg, 20%);
|
||||
|
||||
@@ -370,6 +370,16 @@ $legendHoverValueBg: rgba($colorBodyFg, 0.2);
|
||||
$legendTableHeadBg: rgba($colorBodyFg, 0.15);
|
||||
$colorPlotLimitLineBg: rgba($colorBodyBg, 0.2);
|
||||
|
||||
// Gauges
|
||||
$colorGaugeBg: pullForward($colorBodyBg, 5%); // Gauge radial area background, meter background
|
||||
$colorGaugeValue: rgba(#fff, 0.3); // Gauge value graphic (radial sweep, bar) color
|
||||
$colorGaugeTextValue: #fff; // Radial gauge text value
|
||||
$colorGaugeMeterTextValue: $colorGaugeTextValue; // Meter text value, overlaid on value bar
|
||||
$colorGaugeRange: $colorBodyFg; // Range text color
|
||||
$colorGaugeLimitHigh: rgba($colorLimitRedBg, 0.4);
|
||||
$colorGaugeLimitLow: $colorGaugeLimitHigh;
|
||||
$transitionTimeGauge: 150ms; // CSS transition time used to smooth needle gauge and meter value transitions
|
||||
$marginGaugeMeterValue: 10%; // Margin between meter value bar and bounds edges
|
||||
// Time Strip and Lists
|
||||
$colorCurrentBg: rgba($colorStatusAlert, 0.3);
|
||||
$colorCurrentFg: pullForward($colorBodyFg, 20%);
|
||||
|
||||
@@ -366,6 +366,16 @@ $legendHoverValueBg: rgba($colorBodyFg, 0.2);
|
||||
$legendTableHeadBg: rgba($colorBodyFg, 0.15);
|
||||
$colorPlotLimitLineBg: rgba($colorBodyBg, 0.4);
|
||||
|
||||
// Gauges
|
||||
$colorGaugeBg: pullForward($colorBodyBg, 20%); // Gauge radial area background, meter background
|
||||
$colorGaugeValue: rgba(#000, 0.3); // Gauge value graphic (radial sweep, bar) color
|
||||
$colorGaugeTextValue: pullForward($colorBodyFg, 20%); // Radial gauge text value
|
||||
$colorGaugeMeterTextValue: $colorGaugeTextValue; // Meter text value, overlaid on value bar
|
||||
$colorGaugeRange: $colorBodyFg; // Range text color
|
||||
$colorGaugeLimitHigh: rgba($colorLimitRedBg, 0.2);
|
||||
$colorGaugeLimitLow: $colorGaugeLimitHigh;
|
||||
$transitionTimeGauge: 150ms; // CSS transition time used to smooth needle gauge and meter value transitions
|
||||
$marginGaugeMeterValue: 10%; // Margin between meter value bar and bounds edges
|
||||
// Time Strip and Lists
|
||||
$colorCurrentBg: rgba($colorStatusAlert, 0.3);
|
||||
$colorCurrentFg: pullForward($colorBodyFg, 20%);
|
||||
|
||||
@@ -70,8 +70,24 @@
|
||||
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,6 +53,7 @@
|
||||
@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;
|
||||
|
||||
@@ -166,7 +166,8 @@ export default {
|
||||
}
|
||||
|
||||
return definition.form
|
||||
.map((field) => {
|
||||
.filter(field => !field.hideFromInspector)
|
||||
.map(field => {
|
||||
let path = field.property;
|
||||
if (typeof path === 'string') {
|
||||
path = [path];
|
||||
|
||||
@@ -128,7 +128,7 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
const isExistingView = viewProvider.key === this.existingView?.key;
|
||||
const isExistingView = viewProvider.key === this.existingView.key;
|
||||
|
||||
this.clear();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user