Compare commits
94 Commits
imagery-la
...
add-gauge-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
960db3c4fe | ||
|
|
a305017bee | ||
|
|
c33feaa8ba | ||
|
|
47099786cb | ||
|
|
3aac86541a | ||
|
|
21c3fcdc8a | ||
|
|
3a11291a3b | ||
|
|
476f1b2579 | ||
|
|
7dffefc17d | ||
|
|
56d8838d58 | ||
|
|
6800d8c4ee | ||
|
|
a630880fe6 | ||
|
|
6153ad8e1e | ||
|
|
77c0b16050 | ||
|
|
187be2e970 | ||
|
|
0db937febb | ||
|
|
d19088cec6 | ||
|
|
f428d5915a | ||
|
|
43afb39e56 | ||
|
|
cd8c332fb5 | ||
|
|
b899475939 | ||
|
|
cc1f7659f9 | ||
|
|
0d5539be96 | ||
|
|
0a511e6155 | ||
|
|
47b6d19de8 | ||
|
|
3fd93f47bc | ||
|
|
651e61954c | ||
|
|
d30ec4c757 | ||
|
|
3c24733476 | ||
|
|
94d5c23592 | ||
|
|
04d00fac3d | ||
|
|
150909d4b9 | ||
|
|
2b599a7ff4 | ||
|
|
824a597825 | ||
|
|
510952551b | ||
|
|
cf5edf2db0 | ||
|
|
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 |
@@ -2,7 +2,7 @@ version: 2.1
|
||||
executors:
|
||||
pw-focal-development:
|
||||
docker:
|
||||
- image: mcr.microsoft.com/playwright:v1.19.1-focal
|
||||
- image: mcr.microsoft.com/playwright:v1.19.2-focal
|
||||
environment:
|
||||
NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed
|
||||
parameters:
|
||||
@@ -101,7 +101,7 @@ jobs:
|
||||
equal: [ "FirefoxESR", <<parameters.browser>> ]
|
||||
steps:
|
||||
- browser-tools/install-firefox:
|
||||
version: "91.4.0esr" #https://archive.mozilla.org/pub/firefox/releases/
|
||||
version: "91.7.1esr" #https://archive.mozilla.org/pub/firefox/releases/
|
||||
- when:
|
||||
condition:
|
||||
equal: [ "FirefoxHeadless", <<parameters.browser>> ]
|
||||
@@ -142,11 +142,8 @@ workflows:
|
||||
overall-circleci-commit-status: #These jobs run on every commit
|
||||
jobs:
|
||||
- lint:
|
||||
name: node16-lint
|
||||
node-version: lts/gallium
|
||||
- unit-test:
|
||||
name: node12-chrome
|
||||
node-version: lts/erbium
|
||||
browser: ChromeHeadless
|
||||
- unit-test:
|
||||
name: node14-chrome
|
||||
node-version: lts/fermium
|
||||
@@ -164,13 +161,9 @@ workflows:
|
||||
the-nightly: #These jobs do not run on PRs, but against master at night
|
||||
jobs:
|
||||
- unit-test:
|
||||
name: node12-firefoxESR-nightly
|
||||
node-version: lts/erbium
|
||||
name: node16-firefoxESR-nightly
|
||||
node-version: lts/gallium
|
||||
browser: FirefoxESR
|
||||
- unit-test:
|
||||
name: node12-chrome-nightly
|
||||
node-version: lts/erbium
|
||||
browser: ChromeHeadless
|
||||
- unit-test:
|
||||
name: node14-firefox-nightly
|
||||
node-version: lts/fermium
|
||||
|
||||
14
.github/workflows/lighthouse.yml
vendored
14
.github/workflows/lighthouse.yml
vendored
@@ -9,8 +9,6 @@ on:
|
||||
pull_request:
|
||||
types:
|
||||
- labeled
|
||||
schedule:
|
||||
- cron: '28 21 * * 1-5'
|
||||
jobs:
|
||||
lighthouse-pr:
|
||||
if: ${{ github.event.label.name == 'pr:lighthouse' }}
|
||||
@@ -20,10 +18,10 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: master #explicitly checkout master for baseline
|
||||
- name: Install Node 14
|
||||
- name: Install Node 16
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '14'
|
||||
node-version: '16'
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v2
|
||||
env:
|
||||
@@ -54,10 +52,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Node 14
|
||||
- name: Install Node 16
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '14'
|
||||
node-version: '16'
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v2
|
||||
env:
|
||||
@@ -83,9 +81,9 @@ jobs:
|
||||
- name: Install Node 14
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '14'
|
||||
node-version: '16'
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v3
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
|
||||
1
.github/workflows/pr-platform.yml
vendored
1
.github/workflows/pr-platform.yml
vendored
@@ -16,7 +16,6 @@ jobs:
|
||||
- macos-latest
|
||||
- windows-latest
|
||||
node_version:
|
||||
- 12
|
||||
- 14
|
||||
- 16
|
||||
architecture:
|
||||
|
||||
2
app.js
2
app.js
@@ -64,7 +64,7 @@ app.use(require('webpack-dev-middleware')(
|
||||
compiler,
|
||||
{
|
||||
publicPath: '/dist',
|
||||
logLevel: 'warn'
|
||||
stats: 'errors-warnings'
|
||||
}
|
||||
));
|
||||
|
||||
|
||||
@@ -157,5 +157,10 @@ test.describe('Sine Wave Generator', () => {
|
||||
y: 28
|
||||
}
|
||||
});
|
||||
|
||||
// Verify that where we click on canvas shows the number we clicked on
|
||||
// Note that any number will do, we just care that a number exists
|
||||
await expect(page.locator('.value-to-display-nearestValue')).toContainText(/[+-]?([0-9]*[.])?[0-9]+/);
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
66
e2e/tests/plugins/clock/Clock.e2e.spec.js
Normal file
66
e2e/tests/plugins/clock/Clock.e2e.spec.js
Normal file
@@ -0,0 +1,66 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
This test suite is dedicated to tests which verify the basic operations surrounding Clock.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('@playwright/test');
|
||||
|
||||
test.describe('Clock Generator', () => {
|
||||
|
||||
test('Timezone dropdown will collapse when clicked outside or on dropdown icon again', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/4878'
|
||||
});
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
//Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
// Click Clock
|
||||
await page.click('text=Clock');
|
||||
|
||||
// Click .icon-arrow-down
|
||||
await page.locator('.icon-arrow-down').click();
|
||||
//verify if the autocomplete dropdown is visible
|
||||
await expect(page.locator(".optionPreSelected")).toBeVisible();
|
||||
// Click .icon-arrow-down
|
||||
await page.locator('.icon-arrow-down').click();
|
||||
|
||||
// Verify clicking on the autocomplete arrow collapses the dropdown
|
||||
await expect(page.locator(".optionPreSelected")).not.toBeVisible();
|
||||
|
||||
// Click timezone input to open dropdown
|
||||
await page.locator('.autocompleteInput').click();
|
||||
//verify if the autocomplete dropdown is visible
|
||||
await expect(page.locator(".optionPreSelected")).toBeVisible();
|
||||
|
||||
// Verify clicking outside the autocomplete dropdown collapses it
|
||||
await page.locator('text=Timezone').click();
|
||||
// Verify clicking on the autocomplete arrow collapses the dropdown
|
||||
await expect(page.locator(".optionPreSelected")).not.toBeVisible();
|
||||
|
||||
});
|
||||
});
|
||||
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
|
||||
@@ -166,17 +166,17 @@ test.describe('Example Imagery', () => {
|
||||
// wait for zoom animation to finish
|
||||
await bgImageLocator.hover();
|
||||
const zoomedInBoundingBox = await bgImageLocator.boundingBox();
|
||||
expect(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
|
||||
expect(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
|
||||
expect.soft(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
|
||||
expect.soft(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
|
||||
|
||||
await zoomResetBtn.click();
|
||||
await bgImageLocator.hover();
|
||||
|
||||
const resetBoundingBox = await bgImageLocator.boundingBox();
|
||||
expect(resetBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height);
|
||||
expect(resetBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width);
|
||||
expect.soft(resetBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height);
|
||||
expect.soft(resetBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width);
|
||||
|
||||
expect(resetBoundingBox.height).toEqual(initialBoundingBox.height);
|
||||
expect.soft(resetBoundingBox.height).toEqual(initialBoundingBox.height);
|
||||
expect(resetBoundingBox.width).toEqual(initialBoundingBox.width);
|
||||
});
|
||||
|
||||
|
||||
@@ -35,8 +35,8 @@ define([
|
||||
phase: 0
|
||||
};
|
||||
|
||||
function GeneratorProvider() {
|
||||
this.workerInterface = new WorkerInterface();
|
||||
function GeneratorProvider(openmct) {
|
||||
this.workerInterface = new WorkerInterface(openmct);
|
||||
}
|
||||
|
||||
GeneratorProvider.prototype.canProvideTelemetry = function (domainObject) {
|
||||
|
||||
@@ -21,20 +21,13 @@
|
||||
*****************************************************************************/
|
||||
|
||||
define([
|
||||
'raw-loader!./generatorWorker.js',
|
||||
'uuid'
|
||||
], function (
|
||||
workerText,
|
||||
uuid
|
||||
) {
|
||||
|
||||
var workerBlob = new Blob(
|
||||
[workerText],
|
||||
{type: 'application/javascript'}
|
||||
);
|
||||
var workerUrl = URL.createObjectURL(workerBlob);
|
||||
|
||||
function WorkerInterface() {
|
||||
function WorkerInterface(openmct) {
|
||||
// eslint-disable-next-line no-undef
|
||||
const workerUrl = `${openmct.getAssetPath()}${__OPENMCT_ROOT_RELATIVE__}generatorWorker.js`;
|
||||
this.worker = new Worker(workerUrl);
|
||||
this.worker.onmessage = this.onMessage.bind(this);
|
||||
this.callbacks = {};
|
||||
|
||||
@@ -146,7 +146,7 @@ define([
|
||||
}
|
||||
});
|
||||
|
||||
openmct.telemetry.addProvider(new GeneratorProvider());
|
||||
openmct.telemetry.addProvider(new GeneratorProvider(openmct));
|
||||
openmct.telemetry.addProvider(new GeneratorMetadataProvider());
|
||||
openmct.telemetry.addProvider(new SinewaveLimitProvider());
|
||||
};
|
||||
|
||||
@@ -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());
|
||||
@@ -195,6 +195,7 @@
|
||||
));
|
||||
openmct.install(openmct.plugins.Clock({ enableClockIndicator: true }));
|
||||
openmct.install(openmct.plugins.Timer());
|
||||
openmct.install(openmct.plugins.Timelist());
|
||||
openmct.start();
|
||||
</script>
|
||||
</html>
|
||||
|
||||
@@ -38,6 +38,10 @@ module.exports = (config) => {
|
||||
{
|
||||
pattern: 'dist/inMemorySearchWorker.js*',
|
||||
included: false
|
||||
},
|
||||
{
|
||||
pattern: 'dist/generatorWorker.js*',
|
||||
included: false
|
||||
}
|
||||
],
|
||||
port: 9876,
|
||||
@@ -92,8 +96,7 @@ module.exports = (config) => {
|
||||
},
|
||||
webpack: webpackConfig,
|
||||
webpackMiddleware: {
|
||||
stats: 'errors-only',
|
||||
logLevel: 'warn'
|
||||
stats: 'errors-warnings'
|
||||
},
|
||||
concurrency: 1,
|
||||
singleRun: true,
|
||||
|
||||
35
package.json
35
package.json
@@ -1,19 +1,23 @@
|
||||
{
|
||||
"name": "openmct",
|
||||
"version": "2.0.2-SNAPSHOT",
|
||||
"version": "2.0.2",
|
||||
"description": "The Open MCT core platform",
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "7.16.3",
|
||||
"@braintree/sanitize-url": "6.0.0",
|
||||
"@percy/cli": "1.0.0-beta.76",
|
||||
"@percy/playwright": "1.0.1",
|
||||
"@percy/cli": "1.0.4",
|
||||
"@percy/playwright": "1.0.2",
|
||||
"@playwright/test": "1.19.2",
|
||||
"@types/eventemitter3": "^1.0.0",
|
||||
"@types/jasmine": "^4.0.1",
|
||||
"@types/karma": "^6.3.2",
|
||||
"@types/lodash": "^4.14.178",
|
||||
"@types/mocha": "^9.1.0",
|
||||
"allure-playwright": "2.0.0-beta.15",
|
||||
"babel-loader": "8.2.3",
|
||||
"babel-plugin-istanbul": "6.1.1",
|
||||
"comma-separated-values": "3.6.4",
|
||||
"copy-webpack-plugin": "10.2.0",
|
||||
"core-js": "3.21.1",
|
||||
"cross-env": "7.0.3",
|
||||
"css-loader": "4.0.0",
|
||||
"d3-axis": "1.0.x",
|
||||
@@ -27,10 +31,8 @@
|
||||
"eventemitter3": "1.2.0",
|
||||
"exports-loader": "0.7.0",
|
||||
"express": "4.13.1",
|
||||
"file-loader": "6.2.0",
|
||||
"file-saver": "2.0.5",
|
||||
"git-rev-sync": "3.0.2",
|
||||
"html-loader": "0.5.5",
|
||||
"html2canvas": "1.4.1",
|
||||
"imports-loader": "0.8.0",
|
||||
"jasmine-core": "4.0.1",
|
||||
@@ -46,22 +48,22 @@
|
||||
"karma-sourcemap-loader": "0.3.8",
|
||||
"karma-spec-reporter": "0.0.33",
|
||||
"karma-webpack": "5.0.0",
|
||||
"lighthouse": "9.5.0",
|
||||
"location-bar": "3.0.1",
|
||||
"lodash": "4.17.12",
|
||||
"mini-css-extract-plugin": "2.4.5",
|
||||
"lodash": "4.17.21",
|
||||
"mini-css-extract-plugin": "2.6.0",
|
||||
"moment": "2.29.1",
|
||||
"moment-duration-format": "2.2.2",
|
||||
"moment-timezone": "0.5.28",
|
||||
"moment-timezone": "0.5.34",
|
||||
"node-bourbon": "4.2.3",
|
||||
"painterro": "1.2.56",
|
||||
"plotly.js-basic-dist": "2.5.0",
|
||||
"plotly.js-gl2d-dist": "2.5.0",
|
||||
"printj": "1.3.1",
|
||||
"raw-loader": "4.0.2",
|
||||
"request": "2.88.2",
|
||||
"resolve-url-loader": "4.0.0",
|
||||
"sass": "1.49.0",
|
||||
"sass-loader": "12.4.0",
|
||||
"sass": "1.49.9",
|
||||
"sass-loader": "12.6.0",
|
||||
"sinon": "13.0.1",
|
||||
"style-loader": "^1.0.1",
|
||||
"uuid": "3.3.3",
|
||||
@@ -71,13 +73,13 @@
|
||||
"vue-template-compiler": "2.6.14",
|
||||
"webpack": "5.68.0",
|
||||
"webpack-cli": "4.9.2",
|
||||
"webpack-dev-middleware": "3.7.3",
|
||||
"webpack-dev-middleware": "5.3.1",
|
||||
"webpack-hot-middleware": "2.25.1",
|
||||
"webpack-merge": "5.8.0",
|
||||
"zepto": "1.2.0"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rm -rf ./dist ./node_modules; rm package-lock.json",
|
||||
"clean": "rm -rf ./dist ./node_modules ./package-lock.json",
|
||||
"clean-test-lint": "npm run clean; npm install; npm run test; npm run lint",
|
||||
"start": "node app.js",
|
||||
"lint": "eslint example src --ext .js,.vue openmct.js",
|
||||
@@ -107,7 +109,10 @@
|
||||
"url": "https://github.com/nasa/openmct.git"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.22.0"
|
||||
"node": ">=14.19.1"
|
||||
},
|
||||
"overrides": {
|
||||
"core-js": "3.21.1"
|
||||
},
|
||||
"browserslist": [
|
||||
"Firefox ESR",
|
||||
|
||||
@@ -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 {
|
||||
@@ -65,10 +69,11 @@ export default class FormControl {
|
||||
*/
|
||||
_getControlViewProvider(control) {
|
||||
const self = this;
|
||||
let rowComponent;
|
||||
|
||||
return {
|
||||
show(element, model, onChange) {
|
||||
const rowComponent = new Vue({
|
||||
rowComponent = new Vue({
|
||||
el: element,
|
||||
components: {
|
||||
FormControlComponent: DEFAULT_CONTROLS_MAP[control]
|
||||
@@ -86,8 +91,10 @@ export default class FormControl {
|
||||
});
|
||||
|
||||
return rowComponent;
|
||||
},
|
||||
destroy() {
|
||||
rowComponent.$destroy();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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`;
|
||||
|
||||
@@ -22,17 +22,19 @@
|
||||
|
||||
<template>
|
||||
<div class="form-control autocomplete">
|
||||
<input
|
||||
v-model="field"
|
||||
class="autocompleteInput"
|
||||
type="text"
|
||||
@click="inputClicked()"
|
||||
@keydown="keyDown($event)"
|
||||
>
|
||||
<span
|
||||
class="icon-arrow-down"
|
||||
@click="arrowClicked()"
|
||||
></span>
|
||||
<span class="autocompleteInputAndArrow">
|
||||
<input
|
||||
v-model="field"
|
||||
class="autocompleteInput"
|
||||
type="text"
|
||||
@click="inputClicked()"
|
||||
@keydown="keyDown($event)"
|
||||
>
|
||||
<span
|
||||
class="icon-arrow-down"
|
||||
@click="arrowClicked()"
|
||||
></span>
|
||||
</span>
|
||||
<div
|
||||
class="autocompleteOptions"
|
||||
@blur="hideOptions = true"
|
||||
@@ -108,10 +110,21 @@ export default {
|
||||
|
||||
this.$emit('onChange', data);
|
||||
}
|
||||
},
|
||||
hideOptions(newValue) {
|
||||
if (!newValue) {
|
||||
// adding a event listener when the hideOpntions is false (dropdown is visible)
|
||||
// handleoutsideclick can collapse the dropdown when clicked outside autocomplete
|
||||
document.body.addEventListener('click', this.handleOutsideClick);
|
||||
} else {
|
||||
//removing event listener when hideOptions become true (dropdown is collapsed)
|
||||
document.body.removeEventListener('click', this.handleOutsideClick);
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.options = this.model.options;
|
||||
this.autocompleteInputAndArrow = this.$el.getElementsByClassName('autocompleteInputAndArrow')[0];
|
||||
this.autocompleteInputElement = this.$el.getElementsByClassName('autocompleteInput')[0];
|
||||
if (this.options[0].name) {
|
||||
// If "options" include name, value pair
|
||||
@@ -123,6 +136,9 @@ export default {
|
||||
this.optionNames = this.options;
|
||||
}
|
||||
},
|
||||
destroyed() {
|
||||
document.body.removeEventListener('click', this.handleOutsideClick);
|
||||
},
|
||||
methods: {
|
||||
decrementOptionIndex() {
|
||||
if (this.optionIndex === 0) {
|
||||
@@ -176,7 +192,21 @@ export default {
|
||||
// to show them all the options
|
||||
this.showFilteredOptions = false;
|
||||
this.autocompleteInputElement.select();
|
||||
this.showOptions();
|
||||
|
||||
if (this.hideOptions) {
|
||||
this.showOptions();
|
||||
} else {
|
||||
this.hideOptions = true;
|
||||
}
|
||||
|
||||
},
|
||||
handleOutsideClick(event) {
|
||||
// if click event is detected outside autocomplete (both input & arrow) while the
|
||||
// dropdown is visible, this will collapse the dropdown.
|
||||
const clickedInsideAutocomplete = this.autocompleteInputAndArrow.contains(event.target);
|
||||
if (!clickedInsideAutocomplete && !this.hideOptions) {
|
||||
this.hideOptions = true;
|
||||
}
|
||||
},
|
||||
optionMouseover(optionId) {
|
||||
this.optionIndex = optionId;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -365,3 +365,7 @@ class TimeContext extends EventEmitter {
|
||||
}
|
||||
|
||||
export default TimeContext;
|
||||
|
||||
/**
|
||||
@typedef {{start: number, end: number}} Bounds
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="c-table c-table--sortable c-list-view">
|
||||
<div class="c-table c-table--sortable c-list-view c-list-view--sticky-header c-list-view--selectable">
|
||||
<table class="c-table__body">
|
||||
<thead class="c-table__header">
|
||||
<tr>
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
/******************************* LIST VIEW */
|
||||
.c-list-view {
|
||||
overflow-x: auto !important;
|
||||
overflow-y: auto;
|
||||
|
||||
tbody tr {
|
||||
background: $colorListItemBg;
|
||||
transition: $transOut;
|
||||
}
|
||||
|
||||
body.desktop & {
|
||||
tbody tr {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: $colorListItemBgHov;
|
||||
filter: $filterHov;
|
||||
transition: $transIn;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
$p: floor($interiorMargin * 1.5);
|
||||
@include ellipsize();
|
||||
line-height: 120%; // Needed for icon alignment
|
||||
max-width: 0;
|
||||
padding-top: $p;
|
||||
padding-bottom: $p;
|
||||
width: 25%;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,16 @@ import ImageryViewComponent from './components/ImageryView.vue';
|
||||
|
||||
import Vue from 'vue';
|
||||
|
||||
const DEFAULT_IMAGE_FRESHNESS_OPTIONS = {
|
||||
fadeOutDelayTime: '0s',
|
||||
fadeOutDurationTime: '30s'
|
||||
};
|
||||
export default class ImageryView {
|
||||
constructor(openmct, domainObject, objectPath) {
|
||||
constructor(openmct, domainObject, objectPath, options) {
|
||||
this.openmct = openmct;
|
||||
this.domainObject = domainObject;
|
||||
this.objectPath = objectPath;
|
||||
this.options = options;
|
||||
this.component = undefined;
|
||||
}
|
||||
|
||||
@@ -27,6 +32,7 @@ export default class ImageryView {
|
||||
openmct: this.openmct,
|
||||
domainObject: this.domainObject,
|
||||
objectPath: alternateObjectPath || this.objectPath,
|
||||
imageFreshnessOptions: this.options?.imageFreshness || DEFAULT_IMAGE_FRESHNESS_OPTIONS,
|
||||
currentView: this
|
||||
},
|
||||
data() {
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
*****************************************************************************/
|
||||
import ImageryView from './ImageryView';
|
||||
|
||||
export default function ImageryViewProvider(openmct) {
|
||||
export default function ImageryViewProvider(openmct, options) {
|
||||
const type = 'example.imagery';
|
||||
|
||||
function hasImageTelemetry(domainObject) {
|
||||
@@ -43,7 +43,7 @@ export default function ImageryViewProvider(openmct) {
|
||||
return hasImageTelemetry(domainObject) && (!isChildOfTimeStrip || openmct.router.isNavigatedObject(objectPath));
|
||||
},
|
||||
view: function (domainObject, objectPath) {
|
||||
return new ImageryView(openmct, domainObject, objectPath);
|
||||
return new ImageryView(openmct, domainObject, objectPath, options);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ $elemBg: rgba(black, 0.7);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
z-index: 2;
|
||||
@include userSelectNone;
|
||||
}
|
||||
|
||||
|
||||
@@ -132,6 +132,10 @@
|
||||
<!-- image fresh -->
|
||||
<div
|
||||
v-if="canTrackDuration"
|
||||
:style="{
|
||||
'animation-delay': imageFreshnessOptions.fadeOutDelayTime,
|
||||
'animation-duration': imageFreshnessOptions.fadeOutDurationTime
|
||||
}"
|
||||
:class="{'c-imagery--new': isImageNew && !refreshCSS}"
|
||||
class="c-imagery__age icon-timer"
|
||||
>{{ formattedDuration }}</div>
|
||||
@@ -235,7 +239,7 @@ export default {
|
||||
ImageControls
|
||||
},
|
||||
mixins: [imageryData],
|
||||
inject: ['openmct', 'domainObject', 'objectPath', 'currentView'],
|
||||
inject: ['openmct', 'domainObject', 'objectPath', 'currentView', 'imageFreshnessOptions'],
|
||||
props: {
|
||||
focusedImageTimestamp: {
|
||||
type: Number,
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
@keyframes fade-out {
|
||||
from {
|
||||
background-color: rgba($colorOk, 0.5);
|
||||
}
|
||||
to {
|
||||
background-color: rgba($colorOk, 0);
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.c-imagery {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -123,12 +133,17 @@
|
||||
// New imagery
|
||||
$bgColor: $colorOk;
|
||||
color: $colorOkFg;
|
||||
background: rgba($bgColor, 0.5);
|
||||
@include flash($animName: flashImageAge, $iter: 2, $dur: 250ms, $valStart: rgba($colorOk, 0.7), $valEnd: rgba($colorOk, 0));
|
||||
background-color: rgba($bgColor, 0.5);
|
||||
animation-name: fade-out;
|
||||
animation-timing-function: ease-in;
|
||||
animation-iteration-count: 1;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
|
||||
&__thumbs-wrapper {
|
||||
display: flex; // Uses row layout
|
||||
justify-content: flex-end;
|
||||
|
||||
&.is-autoscroll-off {
|
||||
background: $colorInteriorBorder;
|
||||
@@ -207,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;
|
||||
|
||||
@@ -23,9 +23,9 @@
|
||||
import ImageryViewProvider from './ImageryViewProvider';
|
||||
import ImageryTimestripViewProvider from './ImageryTimestripViewProvider';
|
||||
|
||||
export default function () {
|
||||
export default function (options) {
|
||||
return function install(openmct) {
|
||||
openmct.objectViews.addProvider(new ImageryViewProvider(openmct));
|
||||
openmct.objectViews.addProvider(new ImageryViewProvider(openmct, options));
|
||||
openmct.objectViews.addProvider(new ImageryTimestripViewProvider(openmct));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -35,6 +35,8 @@
|
||||
ref="searchResults"
|
||||
:domain-object="domainObject"
|
||||
:results="searchResults"
|
||||
@cancelEdit="cancelTransaction"
|
||||
@editingEntry="startTransaction"
|
||||
@changeSectionPage="changeSelectedSection"
|
||||
@updateEntries="updateEntries"
|
||||
/>
|
||||
@@ -140,6 +142,8 @@
|
||||
:selected-page="selectedPage"
|
||||
:selected-section="selectedSection"
|
||||
:read-only="false"
|
||||
@cancelEdit="cancelTransaction"
|
||||
@editingEntry="startTransaction"
|
||||
@deleteEntry="deleteEntry"
|
||||
@updateEntry="updateEntry"
|
||||
/>
|
||||
@@ -710,6 +714,8 @@ export default {
|
||||
notebookEntries[this.selectedSection.id][this.selectedPage.id] = entries;
|
||||
|
||||
mutateObject(this.openmct, this.domainObject, 'configuration.entries', notebookEntries);
|
||||
|
||||
this.saveTransaction();
|
||||
},
|
||||
getPageIdFromUrl() {
|
||||
return this.openmct.router.getParams().pageId;
|
||||
@@ -746,6 +752,39 @@ export default {
|
||||
this.selectPage(pageId);
|
||||
|
||||
this.syncUrlWithPageAndSection();
|
||||
},
|
||||
activeTransaction() {
|
||||
return this.openmct.objects.getActiveTransaction();
|
||||
},
|
||||
startTransaction() {
|
||||
if (!this.openmct.editor.isEditing()) {
|
||||
this.openmct.objects.startTransaction();
|
||||
}
|
||||
},
|
||||
saveTransaction() {
|
||||
const transaction = this.activeTransaction();
|
||||
|
||||
if (!transaction || this.openmct.editor.isEditing()) {
|
||||
return;
|
||||
}
|
||||
|
||||
return transaction.commit()
|
||||
.catch(error => {
|
||||
throw error;
|
||||
}).finally(() => {
|
||||
this.openmct.objects.endTransaction();
|
||||
});
|
||||
},
|
||||
cancelTransaction() {
|
||||
if (!this.openmct.editor.isEditing()) {
|
||||
const transaction = this.activeTransaction();
|
||||
transaction.cancel()
|
||||
.catch(error => {
|
||||
throw error;
|
||||
}).finally(() => {
|
||||
this.openmct.objects.endTransaction();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
class="c-ne__text c-ne__input"
|
||||
tabindex="0"
|
||||
contenteditable
|
||||
@focus="editingEntry()"
|
||||
@blur="updateEntryValue($event)"
|
||||
@keydown.enter.exact.prevent
|
||||
@keyup.enter.exact.prevent="forceBlur($event)"
|
||||
@@ -284,11 +285,16 @@ export default {
|
||||
|
||||
this.$emit('updateEntry', this.entry);
|
||||
},
|
||||
editingEntry() {
|
||||
this.$emit('editingEntry');
|
||||
},
|
||||
updateEntryValue($event) {
|
||||
const value = $event.target.innerText;
|
||||
if (value !== this.entry.text && value.match(/\S/)) {
|
||||
this.entry.text = value;
|
||||
this.$emit('updateEntry', this.entry);
|
||||
} else {
|
||||
this.$emit('cancelEdit');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@
|
||||
:read-only="true"
|
||||
:selected-page="result.page"
|
||||
:selected-section="result.section"
|
||||
@editingEntry="editingEntry"
|
||||
@cancelEdit="cancelEdit"
|
||||
@changeSectionPage="changeSectionPage"
|
||||
@updateEntries="updateEntries"
|
||||
/>
|
||||
@@ -63,6 +65,12 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
editingEntry() {
|
||||
this.$emit('editingEntry');
|
||||
},
|
||||
cancelEdit() {
|
||||
this.$emit('cancelEdit');
|
||||
},
|
||||
changeSectionPage(data) {
|
||||
this.$emit('changeSectionPage', data);
|
||||
},
|
||||
|
||||
@@ -76,6 +76,9 @@
|
||||
<div
|
||||
ref="chartContainer"
|
||||
class="gl-plot-chart-wrapper"
|
||||
:class="[
|
||||
{ 'alt-pressed': altPressed },
|
||||
]"
|
||||
>
|
||||
<mct-chart
|
||||
:rectangles="rectangles"
|
||||
@@ -230,6 +233,7 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
altPressed: false,
|
||||
highlights: [],
|
||||
lockHighlightPoint: false,
|
||||
tickWidth: 0,
|
||||
@@ -268,6 +272,8 @@ export default {
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
document.addEventListener('keydown', this.handleKeyDown);
|
||||
document.addEventListener('keyup', this.handleKeyUp);
|
||||
eventHelpers.extend(this);
|
||||
this.updateRealTime = this.updateRealTime.bind(this);
|
||||
this.updateDisplayBounds = this.updateDisplayBounds.bind(this);
|
||||
@@ -299,9 +305,21 @@ export default {
|
||||
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.removeEventListener('keydown', this.handleKeyDown);
|
||||
document.removeEventListener('keyup', this.handleKeyUp);
|
||||
this.destroy();
|
||||
},
|
||||
methods: {
|
||||
handleKeyDown(event) {
|
||||
if (event.key === 'Alt') {
|
||||
this.altPressed = true;
|
||||
}
|
||||
},
|
||||
handleKeyUp(event) {
|
||||
if (event.key === 'Alt') {
|
||||
this.altPressed = false;
|
||||
}
|
||||
},
|
||||
setTimeContext() {
|
||||
this.stopFollowingTimeContext();
|
||||
|
||||
@@ -643,7 +661,7 @@ export default {
|
||||
this.positionOverElement = {
|
||||
x: event.clientX - this.chartElementBounds.left,
|
||||
y: this.chartElementBounds.height
|
||||
- (event.clientY - this.chartElementBounds.top)
|
||||
- (event.clientY - this.chartElementBounds.top)
|
||||
};
|
||||
|
||||
this.positionOverPlot = {
|
||||
|
||||
@@ -112,6 +112,10 @@ export default {
|
||||
mounted() {
|
||||
eventHelpers.extend(this);
|
||||
|
||||
if (!this.axisType) {
|
||||
throw new Error("axis-type prop expected");
|
||||
}
|
||||
|
||||
this.axis = this.getAxisFromConfig();
|
||||
|
||||
this.tickCount = 4;
|
||||
@@ -126,15 +130,16 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
getAxisFromConfig() {
|
||||
if (!this.axisType) {
|
||||
return;
|
||||
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
|
||||
/** @type {import('./configuration/PlotConfigurationModel').default} */
|
||||
let config = configStore.get(configId);
|
||||
|
||||
if (!config) {
|
||||
throw new Error('config is missing');
|
||||
}
|
||||
|
||||
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
let config = configStore.get(configId);
|
||||
if (config) {
|
||||
return config[this.axisType];
|
||||
}
|
||||
return config[this.axisType];
|
||||
},
|
||||
/**
|
||||
* Determine whether ticks should be regenerated for a given range.
|
||||
@@ -210,8 +215,8 @@ export default {
|
||||
if (this.shouldRegenerateTicks(range, forceRegeneration)) {
|
||||
let newTicks = this.getTicks();
|
||||
this.tickRange = {
|
||||
min: Math.min.apply(Math, newTicks),
|
||||
max: Math.max.apply(Math, newTicks),
|
||||
min: Math.min(...newTicks),
|
||||
max: Math.max(...newTicks),
|
||||
step: newTicks[1] - newTicks[0]
|
||||
};
|
||||
|
||||
|
||||
@@ -23,6 +23,9 @@
|
||||
import eventHelpers from '../lib/eventHelpers';
|
||||
|
||||
export default class MCTChartAlarmLineSet {
|
||||
/**
|
||||
* @param {Bounds} bounds
|
||||
*/
|
||||
constructor(series, chart, offset, bounds) {
|
||||
this.series = series;
|
||||
this.chart = chart;
|
||||
@@ -40,6 +43,9 @@ export default class MCTChartAlarmLineSet {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Bounds} bounds
|
||||
*/
|
||||
updateBounds(bounds) {
|
||||
this.bounds = bounds;
|
||||
this.getLimitPoints(this.series);
|
||||
@@ -106,3 +112,7 @@ export default class MCTChartAlarmLineSet {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@typedef {import('@/api/time/TimeContext').Bounds} Bounds
|
||||
*/
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
import MCTChartSeriesElement from './MCTChartSeriesElement';
|
||||
|
||||
export default class MCTChartLineLinear extends MCTChartSeriesElement {
|
||||
addPoint(point, start, count) {
|
||||
addPoint(point, start) {
|
||||
this.buffer[start] = point.x;
|
||||
this.buffer[start + 1] = point.y;
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ export default class MCTChartLineStepAfter extends MCTChartSeriesElement {
|
||||
return 2 + ((index - 1) * 4);
|
||||
}
|
||||
|
||||
addPoint(point, start, count) {
|
||||
addPoint(point, start) {
|
||||
if (start === 0 && this.count === 0) {
|
||||
// First point is easy.
|
||||
this.buffer[start] = point.x;
|
||||
|
||||
@@ -24,7 +24,7 @@ import MCTChartSeriesElement from './MCTChartSeriesElement';
|
||||
|
||||
// TODO: Is this needed? This is identical to MCTChartLineLinear. Why is it a different class?
|
||||
export default class MCTChartPointSet extends MCTChartSeriesElement {
|
||||
addPoint(point, start, count) {
|
||||
addPoint(point, start) {
|
||||
this.buffer[start] = point.x;
|
||||
this.buffer[start + 1] = point.y;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
import eventHelpers from '../lib/eventHelpers';
|
||||
|
||||
/** @abstract */
|
||||
export default class MCTChartSeriesElement {
|
||||
constructor(series, chart, offset) {
|
||||
this.series = series;
|
||||
@@ -72,9 +73,11 @@ export default class MCTChartSeriesElement {
|
||||
}
|
||||
}
|
||||
|
||||
removePoint(point, index, count) {
|
||||
// by default, do nothing.
|
||||
}
|
||||
/** @abstract */
|
||||
removePoint(index) {}
|
||||
|
||||
/** @abstract */
|
||||
addPoint(point, index) {}
|
||||
|
||||
remove(point, index, series) {
|
||||
const vertexCount = this.vertexCountForPointAtIndex(index);
|
||||
@@ -155,3 +158,18 @@ export default class MCTChartSeriesElement {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/** @typedef {any} TODO */
|
||||
|
||||
/** @typedef {import('../configuration/PlotSeries').default} PlotSeries */
|
||||
|
||||
/**
|
||||
@typedef {{
|
||||
x: (x: number) => number
|
||||
y: (y: number) => number
|
||||
xVal: (point: Point, pSeries: PlotSeries) => number
|
||||
yVal: (point: Point, pSeries: PlotSeries) => number
|
||||
xKey: TODO
|
||||
yKey: TODO
|
||||
}} Offset
|
||||
*/
|
||||
|
||||
@@ -21,11 +21,17 @@
|
||||
*****************************************************************************/
|
||||
import Model from './Model';
|
||||
|
||||
/**
|
||||
* @template {object} T
|
||||
* @template {object} O
|
||||
* @extends {Model<T, O>}
|
||||
*/
|
||||
export default class Collection extends Model {
|
||||
/** @type {Constructor} */
|
||||
modelClass = Model;
|
||||
|
||||
initialize(options) {
|
||||
super.initialize(options);
|
||||
this.modelClass = Model;
|
||||
if (options.models) {
|
||||
this.models = options.models.map(this.modelFn, this);
|
||||
} else {
|
||||
@@ -107,3 +113,7 @@ export default class Collection extends Model {
|
||||
this.stopListening();
|
||||
}
|
||||
}
|
||||
|
||||
/** @typedef {any} TODO */
|
||||
|
||||
/** @typedef {new (...args: any[]) => object} Constructor */
|
||||
|
||||
@@ -19,32 +19,47 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
function ConfigStore() {
|
||||
this.store = {};
|
||||
}
|
||||
class ConfigStore {
|
||||
/** @type {Record<string, Destroyable>} */
|
||||
store = {};
|
||||
|
||||
ConfigStore.prototype.deleteStore = function (id) {
|
||||
if (this.store[id]) {
|
||||
if (this.store[id].destroy) {
|
||||
this.store[id].destroy();
|
||||
/**
|
||||
@param {string} id
|
||||
*/
|
||||
deleteStore(id) {
|
||||
const obj = this.store[id];
|
||||
|
||||
if (obj) {
|
||||
if (obj.destroy) {
|
||||
obj.destroy();
|
||||
}
|
||||
|
||||
delete this.store[id];
|
||||
}
|
||||
|
||||
delete this.store[id];
|
||||
}
|
||||
};
|
||||
|
||||
ConfigStore.prototype.deleteAll = function () {
|
||||
Object.keys(this.store).forEach(id => this.deleteStore(id));
|
||||
};
|
||||
deleteAll() {
|
||||
Object.keys(this.store).forEach(id => this.deleteStore(id));
|
||||
}
|
||||
|
||||
ConfigStore.prototype.add = function (id, config) {
|
||||
this.store[id] = config;
|
||||
};
|
||||
/**
|
||||
@param {string} id
|
||||
@param {any} config
|
||||
*/
|
||||
add(id, config) {
|
||||
this.store[id] = config;
|
||||
}
|
||||
|
||||
ConfigStore.prototype.get = function (id) {
|
||||
return this.store[id];
|
||||
};
|
||||
/**
|
||||
@param {string} id
|
||||
*/
|
||||
get(id) {
|
||||
return this.store[id];
|
||||
}
|
||||
}
|
||||
|
||||
const STORE = new ConfigStore();
|
||||
|
||||
export default STORE;
|
||||
|
||||
/** @typedef {{destroy?(): void}} Destroyable */
|
||||
|
||||
@@ -42,7 +42,10 @@ export default class LegendModel extends Model {
|
||||
}
|
||||
}
|
||||
|
||||
defaults(options) {
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
defaultModel(options) {
|
||||
return {
|
||||
position: 'top',
|
||||
expandByDefault: false,
|
||||
|
||||
@@ -20,11 +20,18 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import EventEmitter from 'EventEmitter';
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import eventHelpers from "../lib/eventHelpers";
|
||||
import _ from 'lodash';
|
||||
|
||||
/**
|
||||
* @template {object} T
|
||||
* @template {object} O
|
||||
*/
|
||||
export default class Model extends EventEmitter {
|
||||
/**
|
||||
* @param {ModelOptions<T, O>} options
|
||||
*/
|
||||
constructor(options) {
|
||||
super();
|
||||
|
||||
@@ -35,10 +42,14 @@ export default class Model extends EventEmitter {
|
||||
options = {};
|
||||
}
|
||||
|
||||
// FIXME: this.id is defined as a method further below, but here it is
|
||||
// assigned a possibly-undefined value. Is this code unused?
|
||||
this.id = options.id;
|
||||
|
||||
/** @type {ModelType<T>} */
|
||||
this.model = options.model;
|
||||
this.collection = options.collection;
|
||||
const defaults = this.defaults(options);
|
||||
const defaults = this.defaultModel(options);
|
||||
if (!this.model) {
|
||||
this.model = options.model = defaults;
|
||||
} else {
|
||||
@@ -46,14 +57,23 @@ export default class Model extends EventEmitter {
|
||||
}
|
||||
|
||||
this.initialize(options);
|
||||
|
||||
/** @type {keyof ModelType<T> } */
|
||||
this.idAttr = 'id';
|
||||
}
|
||||
|
||||
defaults(options) {
|
||||
/**
|
||||
* @param {ModelOptions<T, O>} options
|
||||
* @returns {ModelType<T>}
|
||||
*/
|
||||
defaultModel(options) {
|
||||
return {};
|
||||
}
|
||||
|
||||
initialize(model) {
|
||||
/**
|
||||
* @param {ModelOptions<T, O>} options
|
||||
*/
|
||||
initialize(options) {
|
||||
|
||||
}
|
||||
|
||||
@@ -69,14 +89,29 @@ export default class Model extends EventEmitter {
|
||||
return this.get(this.idAttr);
|
||||
}
|
||||
|
||||
/**
|
||||
* @template {keyof ModelType<T>} K
|
||||
* @param {K} attribute
|
||||
* @returns {ModelType<T>[K]}
|
||||
*/
|
||||
get(attribute) {
|
||||
return this.model[attribute];
|
||||
}
|
||||
|
||||
/**
|
||||
* @template {keyof ModelType<T>} K
|
||||
* @param {K} attribute
|
||||
* @returns boolean
|
||||
*/
|
||||
has(attribute) {
|
||||
return _.has(this.model, attribute);
|
||||
}
|
||||
|
||||
/**
|
||||
* @template {keyof ModelType<T>} K
|
||||
* @param {K} attribute
|
||||
* @param {ModelType<T>[K]} value
|
||||
*/
|
||||
set(attribute, value) {
|
||||
const oldValue = this.model[attribute];
|
||||
this.model[attribute] = value;
|
||||
@@ -84,6 +119,10 @@ export default class Model extends EventEmitter {
|
||||
this.emit('change:' + attribute, value, oldValue, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @template {keyof ModelType<T>} K
|
||||
* @param {K} attribute
|
||||
*/
|
||||
unset(attribute) {
|
||||
const oldValue = this.model[attribute];
|
||||
delete this.model[attribute];
|
||||
@@ -91,3 +130,26 @@ export default class Model extends EventEmitter {
|
||||
this.emit('change:' + attribute, undefined, oldValue, this);
|
||||
}
|
||||
}
|
||||
|
||||
/** @typedef {any} TODO */
|
||||
|
||||
/** @typedef {TODO} OpenMCT */
|
||||
|
||||
/**
|
||||
@template {object} T
|
||||
@typedef {{
|
||||
id?: string
|
||||
} & T} ModelType
|
||||
*/
|
||||
|
||||
/**
|
||||
@template {object} T
|
||||
@template {object} O
|
||||
@typedef {{
|
||||
model?: ModelType<T>
|
||||
models?: T[]
|
||||
openmct: OpenMCT
|
||||
id?: string
|
||||
[k: string]: unknown
|
||||
} & O} ModelOptions
|
||||
*/
|
||||
|
||||
@@ -26,20 +26,30 @@ import SeriesCollection from "./SeriesCollection";
|
||||
import XAxisModel from "./XAxisModel";
|
||||
import YAxisModel from "./YAxisModel";
|
||||
import LegendModel from "./LegendModel";
|
||||
|
||||
/**
|
||||
* PlotConfiguration model stores the configuration of a plot and some
|
||||
* limited state. The indiidual parts of the plot configuration model
|
||||
* handle setting defaults and updating in response to various changes.
|
||||
*
|
||||
* @extends {Model<PlotConfigModelType, PlotConfigModelOptions>}
|
||||
*/
|
||||
export default class PlotConfigurationModel extends Model {
|
||||
/**
|
||||
* Initializes all sub models and then passes references to submodels
|
||||
* to those that need it.
|
||||
*
|
||||
* @override
|
||||
* @param {import('./Model').ModelOptions<PlotConfigModelType, PlotConfigModelOptions>} options
|
||||
*/
|
||||
initialize(options) {
|
||||
this.openmct = options.openmct;
|
||||
|
||||
// This is a type assertion for TypeScript, this error is never thrown in practice.
|
||||
if (!options.model) {
|
||||
throw new Error('Not a collection model.');
|
||||
}
|
||||
|
||||
this.xAxis = new XAxisModel({
|
||||
model: options.model.xAxis,
|
||||
plot: this,
|
||||
@@ -76,6 +86,8 @@ export default class PlotConfigurationModel extends Model {
|
||||
}
|
||||
/**
|
||||
* Retrieve the persisted series config for a given identifier.
|
||||
* @param {import('./PlotSeries').Identifier} identifier
|
||||
* @returns {import('./PlotSeries').PlotSeriesModelType=}
|
||||
*/
|
||||
getPersistedSeriesConfig(identifier) {
|
||||
const domainObject = this.get('domainObject');
|
||||
@@ -123,15 +135,48 @@ export default class PlotConfigurationModel extends Model {
|
||||
/**
|
||||
* Return defaults, which are extracted from the passed in domain
|
||||
* object.
|
||||
* @override
|
||||
* @param {import('./Model').ModelOptions<PlotConfigModelType, PlotConfigModelOptions>} options
|
||||
*/
|
||||
defaults(options) {
|
||||
defaultModel(options) {
|
||||
return {
|
||||
series: [],
|
||||
domainObject: options.domainObject,
|
||||
xAxis: {
|
||||
},
|
||||
yAxis: _.cloneDeep(_.get(options.domainObject, 'configuration.yAxis', {})),
|
||||
legend: _.cloneDeep(_.get(options.domainObject, 'configuration.legend', {}))
|
||||
xAxis: {},
|
||||
yAxis: _.cloneDeep(options.domainObject.configuration?.yAxis ?? {}),
|
||||
legend: _.cloneDeep(options.domainObject.configuration?.legend ?? {})
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/** @typedef {any} TODO */
|
||||
|
||||
/** @typedef {import('./PlotSeries').default} PlotSeries */
|
||||
|
||||
/**
|
||||
@typedef {{
|
||||
configuration: {
|
||||
series: import('./PlotSeries').PlotSeriesModelType[]
|
||||
}
|
||||
}} SomeDomainObject_NeedsName
|
||||
*/
|
||||
|
||||
/**
|
||||
@typedef {{
|
||||
xAxis: import('./XAxisModel').XAxisModelType
|
||||
yAxis: import('./YAxisModel').YAxisModelType
|
||||
legend: TODO
|
||||
series: PlotSeries[]
|
||||
domainObject: SomeDomainObject_NeedsName
|
||||
}} PlotConfigModelType
|
||||
*/
|
||||
|
||||
/** @typedef {TODO} SomeOtherDomainObject */
|
||||
|
||||
/**
|
||||
TODO: Is SomeOtherDomainObject the same domain object as with SomeDomainObject_NeedsName?
|
||||
@typedef {{
|
||||
plot: import('./PlotConfigurationModel').default
|
||||
domainObject: SomeOtherDomainObject
|
||||
}} PlotConfigModelOptions
|
||||
*/
|
||||
|
||||
@@ -59,9 +59,15 @@ import configStore from "../configuration/ConfigStore";
|
||||
* `metadata`: the Open MCT Telemetry Metadata Manager for the associated
|
||||
* telemetry point.
|
||||
* `formats`: the Open MCT format map for this telemetry point.
|
||||
*
|
||||
* @extends {Model<PlotSeriesModelType, PlotSeriesModelOptions>}
|
||||
*/
|
||||
export default class PlotSeries extends Model {
|
||||
/**
|
||||
@param {import('./Model').ModelOptions<PlotSeriesModelType, PlotSeriesModelOptions>} options
|
||||
*/
|
||||
constructor(options) {
|
||||
|
||||
super(options);
|
||||
|
||||
this.listenTo(this, 'change:xKey', this.onXKeyChange, this);
|
||||
@@ -76,8 +82,10 @@ export default class PlotSeries extends Model {
|
||||
|
||||
/**
|
||||
* Set defaults for telemetry series.
|
||||
* @param {import('./Model').ModelOptions<PlotSeriesModelType, PlotSeriesModelOptions>} options
|
||||
* @override
|
||||
*/
|
||||
defaults(options) {
|
||||
defaultModel(options) {
|
||||
this.metadata = options
|
||||
.openmct
|
||||
.telemetry
|
||||
@@ -109,13 +117,21 @@ export default class PlotSeries extends Model {
|
||||
|
||||
/**
|
||||
* Remove real-time subscription when destroyed.
|
||||
* @override
|
||||
*/
|
||||
onDestroy(model) {
|
||||
destroy() {
|
||||
super.destroy();
|
||||
|
||||
if (this.unsubscribe) {
|
||||
this.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set defaults for telemetry series.
|
||||
* @override
|
||||
* @param {import('./Model').ModelOptions<PlotSeriesModelType, PlotSeriesModelOptions>} options
|
||||
*/
|
||||
initialize(options) {
|
||||
this.openmct = options.openmct;
|
||||
this.domainObject = options.domainObject;
|
||||
@@ -136,9 +152,11 @@ export default class PlotSeries extends Model {
|
||||
|
||||
});
|
||||
this.openmct.time.on('bounds', this.updateLimits);
|
||||
this.on('destroy', this.onDestroy, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Bounds} bounds
|
||||
*/
|
||||
updateLimits(bounds) {
|
||||
this.emit('limitBounds', bounds);
|
||||
}
|
||||
@@ -188,7 +206,7 @@ export default class PlotSeries extends Model {
|
||||
return this.openmct
|
||||
.telemetry
|
||||
.request(this.domainObject, options)
|
||||
.then(function (points) {
|
||||
.then((points) => {
|
||||
const data = this.getSeriesData();
|
||||
const newPoints = _(data)
|
||||
.concat(points)
|
||||
@@ -196,7 +214,7 @@ export default class PlotSeries extends Model {
|
||||
.uniq(true, point => [this.getXVal(point), this.getYVal(point)].join())
|
||||
.value();
|
||||
this.reset(newPoints);
|
||||
}.bind(this))
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn('Error fetching data', error);
|
||||
});
|
||||
@@ -501,8 +519,55 @@ export default class PlotSeries extends Model {
|
||||
|
||||
/**
|
||||
* Update the series data with the given value.
|
||||
* @returns {Array<{
|
||||
cos: number
|
||||
sin: number
|
||||
mctLimitState: {
|
||||
cssClass: string
|
||||
high: number
|
||||
low: {sin: number, cos: number}
|
||||
name: string
|
||||
}
|
||||
utc: number
|
||||
wavelength: number
|
||||
yesterday: number
|
||||
}>}
|
||||
*/
|
||||
getSeriesData() {
|
||||
return configStore.get(this.dataStoreId) || [];
|
||||
}
|
||||
}
|
||||
|
||||
/** @typedef {any} TODO */
|
||||
|
||||
/** @typedef {{key: string, namespace: string}} Identifier */
|
||||
|
||||
/**
|
||||
@typedef {{
|
||||
identifier: Identifier
|
||||
name: string
|
||||
unit: string
|
||||
xKey: string
|
||||
yKey: string
|
||||
markers: boolean
|
||||
markerShape: keyof typeof MARKER_SHAPES
|
||||
markerSize: number
|
||||
alarmMarkers: boolean
|
||||
limitLines: boolean
|
||||
interpolate: boolean
|
||||
stats: TODO
|
||||
}} PlotSeriesModelType
|
||||
*/
|
||||
|
||||
/**
|
||||
@typedef {{
|
||||
model: PlotSeriesModelType
|
||||
collection: import('./SeriesCollection').default
|
||||
persistedConfig: PlotSeriesModelType
|
||||
filters: TODO
|
||||
}} PlotSeriesModelOptions
|
||||
*/
|
||||
|
||||
/**
|
||||
@typedef {import('@/api/time/TimeContext').Bounds} Bounds
|
||||
*/
|
||||
|
||||
@@ -26,8 +26,14 @@ import Collection from "./Collection";
|
||||
import Color from "@/ui/color/Color";
|
||||
import ColorPalette from "@/ui/color/ColorPalette";
|
||||
|
||||
/**
|
||||
* @extends {Collection<SeriesCollectionModelType, SeriesCollectionOptions>}
|
||||
*/
|
||||
export default class SeriesCollection extends Collection {
|
||||
|
||||
/**
|
||||
@override
|
||||
@param {import('./Model').ModelOptions<SeriesCollectionModelType, SeriesCollectionOptions>} options
|
||||
*/
|
||||
initialize(options) {
|
||||
super.initialize(options);
|
||||
this.modelClass = PlotSeries;
|
||||
@@ -83,11 +89,15 @@ export default class SeriesCollection extends Collection {
|
||||
// Clone to prevent accidental mutation by ref.
|
||||
seriesConfig = JSON.parse(JSON.stringify(seriesConfig));
|
||||
|
||||
if (!seriesConfig) {
|
||||
throw "not possible";
|
||||
}
|
||||
|
||||
this.add(new PlotSeries({
|
||||
model: seriesConfig,
|
||||
domainObject: domainObject,
|
||||
collection: this,
|
||||
openmct: this.openmct,
|
||||
collection: this,
|
||||
persistedConfig: this.plot
|
||||
.getPersistedSeriesConfig(domainObject.identifier),
|
||||
filters: filters
|
||||
@@ -163,3 +173,13 @@ export default class SeriesCollection extends Collection {
|
||||
})[0];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@typedef {PlotSeries} SeriesCollectionModelType
|
||||
*/
|
||||
|
||||
/**
|
||||
@typedef {{
|
||||
plot: import('./PlotConfigurationModel').default
|
||||
}} SeriesCollectionOptions
|
||||
*/
|
||||
|
||||
@@ -20,22 +20,38 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import Model from "./Model";
|
||||
|
||||
/**
|
||||
* TODO: doc strings.
|
||||
*/
|
||||
* @extends {Model<XAxisModelType, XAxisModelOptions>}
|
||||
*/
|
||||
export default class XAxisModel extends Model {
|
||||
// Despite providing template types to the Model class, we still need to
|
||||
// re-define the type of the following initialize() method's options arg. Tracking
|
||||
// issue for this: https://github.com/microsoft/TypeScript/issues/32082
|
||||
// When they fix it, we can remove the `@param` we have here.
|
||||
/**
|
||||
* @override
|
||||
* @param {import('./Model').ModelOptions<XAxisModelType, XAxisModelOptions>} options
|
||||
*/
|
||||
initialize(options) {
|
||||
this.plot = options.plot;
|
||||
|
||||
// This is a type assertion for TypeScript, this error is not thrown in practice.
|
||||
if (!options.model) {
|
||||
throw new Error('Not a collection model.');
|
||||
}
|
||||
|
||||
this.set('label', options.model.name || '');
|
||||
this.on('change:range', function (newValue, oldValue, model) {
|
||||
if (!model.get('frozen')) {
|
||||
model.set('displayRange', newValue);
|
||||
|
||||
this.on('change:range', (newValue) => {
|
||||
if (!this.get('frozen')) {
|
||||
this.set('displayRange', newValue);
|
||||
}
|
||||
});
|
||||
|
||||
this.on('change:frozen', ((frozen, oldValue, model) => {
|
||||
this.on('change:frozen', ((frozen) => {
|
||||
if (!frozen) {
|
||||
model.set('range', this.get('range'));
|
||||
this.set('range', this.get('range'));
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -45,6 +61,10 @@ export default class XAxisModel extends Model {
|
||||
|
||||
this.listenTo(this, 'change:key', this.changeKey, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} newKey
|
||||
*/
|
||||
changeKey(newKey) {
|
||||
const series = this.plot.series.first();
|
||||
if (series) {
|
||||
@@ -68,12 +88,17 @@ export default class XAxisModel extends Model {
|
||||
plotSeries.reset();
|
||||
});
|
||||
}
|
||||
defaults(options) {
|
||||
/**
|
||||
* @param {import('./Model').ModelOptions<XAxisModelType, XAxisModelOptions>} options
|
||||
* @override
|
||||
*/
|
||||
defaultModel(options) {
|
||||
const bounds = options.openmct.time.bounds();
|
||||
const timeSystem = options.openmct.time.timeSystem();
|
||||
const format = options.openmct.telemetry.getFormatter(timeSystem.timeFormat);
|
||||
|
||||
return {
|
||||
/** @type {XAxisModelType} */
|
||||
const defaultModel = {
|
||||
name: timeSystem.name,
|
||||
key: timeSystem.key,
|
||||
format: format.format.bind(format),
|
||||
@@ -83,5 +108,42 @@ export default class XAxisModel extends Model {
|
||||
},
|
||||
frozen: false
|
||||
};
|
||||
|
||||
return defaultModel;
|
||||
}
|
||||
}
|
||||
|
||||
/** @typedef {any} TODO */
|
||||
|
||||
/** @typedef {TODO} OpenMCT */
|
||||
|
||||
/**
|
||||
@typedef {{
|
||||
min: number
|
||||
max: number
|
||||
}} NumberRange
|
||||
*/
|
||||
|
||||
/**
|
||||
@typedef {import("./Model").ModelType<{
|
||||
range: NumberRange
|
||||
displayRange: NumberRange
|
||||
frozen: boolean
|
||||
label: string
|
||||
format: (n: number) => string
|
||||
values: Array<TODO>
|
||||
}>} AxisModelType
|
||||
*/
|
||||
|
||||
/**
|
||||
@typedef {AxisModelType & {
|
||||
name: string
|
||||
key: string
|
||||
}} XAxisModelType
|
||||
*/
|
||||
|
||||
/**
|
||||
@typedef {{
|
||||
plot: import('./PlotConfigurationModel').default
|
||||
}} XAxisModelOptions
|
||||
*/
|
||||
|
||||
@@ -23,27 +23,32 @@ import _ from 'lodash';
|
||||
import Model from './Model';
|
||||
|
||||
/**
|
||||
* YAxis model
|
||||
*
|
||||
* TODO: docstrings.
|
||||
*
|
||||
* has the following Model properties:
|
||||
*
|
||||
* `autoscale`: boolean, whether or not to autoscale.
|
||||
* `autoscalePadding`: float, percent of padding to display in plots.
|
||||
* `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.
|
||||
* `label`: label to display on axis.
|
||||
* `stats`: Min and Max Values of data, automatically updated by observing
|
||||
* plot series.
|
||||
* `values`: for enumerated types, an array of possible display values.
|
||||
* `range`: the user-configured range to use for display, when autoscale is
|
||||
* disabled.
|
||||
*
|
||||
*/
|
||||
* YAxis model
|
||||
*
|
||||
* TODO: docstrings.
|
||||
*
|
||||
* has the following Model properties:
|
||||
*
|
||||
* `autoscale`: boolean, whether or not to autoscale.
|
||||
* `autoscalePadding`: float, percent of padding to display in plots.
|
||||
* `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.
|
||||
* `label`: label to display on axis.
|
||||
* `stats`: Min and Max Values of data, automatically updated by observing
|
||||
* plot series.
|
||||
* `values`: for enumerated types, an array of possible display values.
|
||||
* `range`: the user-configured range to use for display, when autoscale is
|
||||
* disabled.
|
||||
*
|
||||
* @extends {Model<YAxisModelType, YAxisModelOptions>}
|
||||
*/
|
||||
export default class YAxisModel extends Model {
|
||||
/**
|
||||
* @override
|
||||
* @param {import('./Model').ModelOptions<YAxisModelType, YAxisModelOptions>} options
|
||||
*/
|
||||
initialize(options) {
|
||||
this.plot = options.plot;
|
||||
this.listenTo(this, 'change:stats', this.calculateAutoscaleExtents, this);
|
||||
@@ -53,6 +58,9 @@ export default class YAxisModel extends Model {
|
||||
this.listenTo(this, 'change:range', this.updateDisplayRange, this);
|
||||
this.updateDisplayRange(this.get('range'));
|
||||
}
|
||||
/**
|
||||
* @param {import('./SeriesCollection').default} seriesCollection
|
||||
*/
|
||||
listenToSeriesCollection(seriesCollection) {
|
||||
this.seriesCollection = seriesCollection;
|
||||
this.listenTo(this.seriesCollection, 'add', (series => {
|
||||
@@ -138,6 +146,9 @@ export default class YAxisModel extends Model {
|
||||
}
|
||||
}, this);
|
||||
}
|
||||
/**
|
||||
* @param {import('./PlotSeries').default} series
|
||||
*/
|
||||
trackSeries(series) {
|
||||
this.listenTo(series, 'change:stats', seriesStats => {
|
||||
if (!seriesStats) {
|
||||
@@ -163,12 +174,13 @@ export default class YAxisModel extends Model {
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Update yAxis format, values, and label from known series.
|
||||
*/
|
||||
updateFromSeries(series) {
|
||||
* Update yAxis format, values, and label from known series.
|
||||
* @param {import('./SeriesCollection').default} seriesCollection
|
||||
*/
|
||||
updateFromSeries(seriesCollection) {
|
||||
const plotModel = this.plot.get('domainObject');
|
||||
const label = _.get(plotModel, 'configuration.yAxis.label');
|
||||
const sampleSeries = series.first();
|
||||
const sampleSeries = seriesCollection.first();
|
||||
if (!sampleSeries) {
|
||||
if (!label) {
|
||||
this.unset('label');
|
||||
@@ -183,7 +195,7 @@ export default class YAxisModel extends Model {
|
||||
this.set('format', yFormat.format.bind(yFormat));
|
||||
this.set('values', yMetadata.values);
|
||||
if (!label) {
|
||||
const labelName = series.map(function (s) {
|
||||
const labelName = seriesCollection.map(function (s) {
|
||||
return s.metadata ? s.metadata.value(s.get('yKey')).name : '';
|
||||
}).reduce(function (a, b) {
|
||||
if (a === undefined) {
|
||||
@@ -203,7 +215,7 @@ export default class YAxisModel extends Model {
|
||||
return;
|
||||
}
|
||||
|
||||
const labelUnits = series.map(function (s) {
|
||||
const labelUnits = seriesCollection.map(function (s) {
|
||||
return s.metadata ? s.metadata.value(s.get('yKey')).units : '';
|
||||
}).reduce(function (a, b) {
|
||||
if (a === undefined) {
|
||||
@@ -224,7 +236,13 @@ export default class YAxisModel extends Model {
|
||||
}
|
||||
}
|
||||
}
|
||||
defaults(options) {
|
||||
/**
|
||||
* @override
|
||||
* @param {import('./Model').ModelOptions<YAxisModelType, YAxisModelOptions>} options
|
||||
* @returns {YAxisModelType}
|
||||
*/
|
||||
defaultModel(options) {
|
||||
// @ts-ignore incomplete YAxisModelType object used for default value.
|
||||
return {
|
||||
frozen: false,
|
||||
autoscale: true,
|
||||
@@ -232,3 +250,20 @@ export default class YAxisModel extends Model {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/** @typedef {any} TODO */
|
||||
|
||||
/**
|
||||
@typedef {import('./XAxisModel').AxisModelType & {
|
||||
autoscale: boolean
|
||||
autoscalePadding: number
|
||||
stats: import('./XAxisModel').NumberRange
|
||||
values: Array<TODO>
|
||||
}} YAxisModelType
|
||||
*/
|
||||
|
||||
/**
|
||||
@typedef {{
|
||||
plot: import('./PlotConfigurationModel').default
|
||||
}} YAxisModelOptions
|
||||
*/
|
||||
|
||||
@@ -110,6 +110,7 @@ DrawWebGL.prototype.onContextLost = function (event) {
|
||||
this.emit('error');
|
||||
this.isContextLost = true;
|
||||
this.destroy();
|
||||
// TODO re-initialize and re-draw on context restored
|
||||
};
|
||||
|
||||
DrawWebGL.prototype.initContext = function () {
|
||||
|
||||
@@ -90,3 +90,10 @@ const helperFunctions = {
|
||||
};
|
||||
|
||||
export default helperFunctions;
|
||||
|
||||
/**
|
||||
@typedef {{
|
||||
listenTo: (object: any, event: any, callback: any, context: any) => void
|
||||
stopListening: (object: any, event: any, callback: any, context: any) => void
|
||||
}} EventHelpers
|
||||
*/
|
||||
|
||||
@@ -571,6 +571,34 @@ describe("the plugin", function () {
|
||||
range: 2
|
||||
}
|
||||
}]
|
||||
},
|
||||
configuration: {
|
||||
objectStyles: {
|
||||
staticStyle: {
|
||||
style: {
|
||||
backgroundColor: 'rgb(0, 200, 0)',
|
||||
color: '',
|
||||
border: ''
|
||||
}
|
||||
},
|
||||
conditionSetIdentifier: {
|
||||
namespace: '',
|
||||
key: 'testConditionSetId'
|
||||
},
|
||||
selectedConditionId: 'conditionId1',
|
||||
defaultConditionId: 'conditionId1',
|
||||
styles: [
|
||||
{
|
||||
conditionId: 'conditionId1',
|
||||
style: {
|
||||
backgroundColor: 'rgb(0, 155, 0)',
|
||||
color: '',
|
||||
output: '',
|
||||
border: ''
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -815,6 +843,20 @@ describe("the plugin", function () {
|
||||
});
|
||||
});
|
||||
|
||||
it("shows styles for telemetry objects if available", (done) => {
|
||||
Vue.nextTick(() => {
|
||||
let conditionalStylesContainer = element.querySelectorAll(".c-plot--stacked-container .js-style-receiver");
|
||||
let hasStyles = 0;
|
||||
conditionalStylesContainer.forEach(el => {
|
||||
if (el.style.backgroundColor !== '') {
|
||||
hasStyles++;
|
||||
}
|
||||
});
|
||||
expect(hasStyles).toBe(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('limits', () => {
|
||||
|
||||
it('lines are not displayed by default', () => {
|
||||
|
||||
@@ -26,8 +26,10 @@
|
||||
|
||||
import MctPlot from '../MctPlot.vue';
|
||||
import Vue from "vue";
|
||||
import conditionalStylesMixin from "./mixins/objectStyles-mixin";
|
||||
|
||||
export default {
|
||||
mixins: [conditionalStylesMixin],
|
||||
inject: ['openmct', 'domainObject', 'path'],
|
||||
props: {
|
||||
object: {
|
||||
|
||||
143
src/plugins/plot/stackedPlot/mixins/objectStyles-mixin.js
Normal file
143
src/plugins/plot/stackedPlot/mixins/objectStyles-mixin.js
Normal file
@@ -0,0 +1,143 @@
|
||||
/*****************************************************************************
|
||||
* 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 StyleRuleManager from "@/plugins/condition/StyleRuleManager";
|
||||
import {STYLE_CONSTANTS} from "@/plugins/condition/utils/constants";
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject', 'path'],
|
||||
data() {
|
||||
return {
|
||||
objectStyle: undefined
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.objectStyles = this.getObjectStyleForItem(this.object.configuration);
|
||||
this.initObjectStyles();
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.stopListeningStyles) {
|
||||
this.stopListeningStyles();
|
||||
}
|
||||
|
||||
if (this.styleRuleManager) {
|
||||
this.styleRuleManager.destroy();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getObjectStyleForItem(config) {
|
||||
if (config && config.objectStyles) {
|
||||
return config.objectStyles ? Object.assign({}, config.objectStyles) : undefined;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
initObjectStyles() {
|
||||
if (!this.styleRuleManager) {
|
||||
this.styleRuleManager = new StyleRuleManager(this.objectStyles, this.openmct, this.updateStyle.bind(this), true);
|
||||
} else {
|
||||
this.styleRuleManager.updateObjectStyleConfig(this.objectStyles);
|
||||
}
|
||||
|
||||
if (this.stopListeningStyles) {
|
||||
this.stopListeningStyles();
|
||||
}
|
||||
|
||||
this.stopListeningStyles = this.openmct.objects.observe(this.object, 'configuration.objectStyles', (newObjectStyle) => {
|
||||
//Updating styles in the inspector view will trigger this so that the changes are reflected immediately
|
||||
this.styleRuleManager.updateObjectStyleConfig(newObjectStyle);
|
||||
});
|
||||
|
||||
if (this.object && this.object.configuration && this.object.configuration.fontStyle) {
|
||||
const { fontSize, font } = this.object.configuration.fontStyle;
|
||||
this.setFontSize(fontSize);
|
||||
this.setFont(font);
|
||||
}
|
||||
|
||||
this.stopListeningFontStyles = this.openmct.objects.observe(this.object, 'configuration.fontStyle', (newFontStyle) => {
|
||||
this.setFontSize(newFontStyle.fontSize);
|
||||
this.setFont(newFontStyle.font);
|
||||
});
|
||||
},
|
||||
getStyleReceiver() {
|
||||
let styleReceiver;
|
||||
|
||||
if (this.$el !== undefined) {
|
||||
styleReceiver = this.$el.querySelector('.js-style-receiver')
|
||||
|| this.$el.querySelector(':first-child');
|
||||
|
||||
if (styleReceiver === null) {
|
||||
styleReceiver = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return styleReceiver;
|
||||
},
|
||||
setFontSize(newSize) {
|
||||
let elemToStyle = this.getStyleReceiver();
|
||||
|
||||
if (elemToStyle !== undefined) {
|
||||
elemToStyle.dataset.fontSize = newSize;
|
||||
}
|
||||
},
|
||||
setFont(newFont) {
|
||||
let elemToStyle = this.getStyleReceiver();
|
||||
|
||||
if (elemToStyle !== undefined) {
|
||||
elemToStyle.dataset.font = newFont;
|
||||
}
|
||||
},
|
||||
updateStyle(styleObj) {
|
||||
let elemToStyle = this.getStyleReceiver();
|
||||
|
||||
if (!styleObj || elemToStyle === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
let keys = Object.keys(styleObj);
|
||||
|
||||
keys.forEach(key => {
|
||||
if (elemToStyle) {
|
||||
if ((typeof styleObj[key] === 'string') && (styleObj[key].indexOf('__no_value') > -1)) {
|
||||
if (elemToStyle.style[key]) {
|
||||
elemToStyle.style[key] = '';
|
||||
}
|
||||
} else {
|
||||
if (!styleObj.isStyleInvisible && elemToStyle.classList.contains(STYLE_CONSTANTS.isStyleInvisible)) {
|
||||
elemToStyle.classList.remove(STYLE_CONSTANTS.isStyleInvisible);
|
||||
} else if (styleObj.isStyleInvisible && !elemToStyle.classList.contains(styleObj.isStyleInvisible)) {
|
||||
elemToStyle.classList.add(styleObj.isStyleInvisible);
|
||||
}
|
||||
|
||||
elemToStyle.style[key] = styleObj[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (this.object && this.object.type === 'conditionWidget' && keys.includes('output')) {
|
||||
this.openmct.objects.mutate(this.object, 'conditionalLabel', styleObj.output);
|
||||
} else {
|
||||
this.openmct.objects.mutate(this.object, 'conditionalLabel', '');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -76,7 +76,9 @@ define([
|
||||
'./timer/plugin',
|
||||
'./userIndicator/plugin',
|
||||
'../../example/exampleUser/plugin',
|
||||
'./localStorage/plugin'
|
||||
'./localStorage/plugin',
|
||||
'./gauge/GaugePlugin',
|
||||
'./timelist/plugin'
|
||||
], function (
|
||||
_,
|
||||
UTCTimeSystem,
|
||||
@@ -133,7 +135,9 @@ define([
|
||||
Timer,
|
||||
UserIndicator,
|
||||
ExampleUser,
|
||||
LocalStorage
|
||||
LocalStorage,
|
||||
GaugePlugin,
|
||||
TimeList
|
||||
) {
|
||||
const plugins = {};
|
||||
|
||||
@@ -210,6 +214,8 @@ define([
|
||||
plugins.DeviceClassifier = DeviceClassifier.default;
|
||||
plugins.UserIndicator = UserIndicator.default;
|
||||
plugins.LocalStorage = LocalStorage.default;
|
||||
plugins.Gauge = GaugePlugin.default;
|
||||
plugins.Timelist = TimeList.default;
|
||||
|
||||
return plugins;
|
||||
});
|
||||
|
||||
394
src/plugins/timelist/Timelist.vue
Normal file
394
src/plugins/timelist/Timelist.vue
Normal file
@@ -0,0 +1,394 @@
|
||||
<!--
|
||||
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
|
||||
ref="timelistHolder"
|
||||
class="c-timelist"
|
||||
>
|
||||
<list-view
|
||||
:items="planActivities"
|
||||
:header-items="headerItems"
|
||||
:default-sort="defaultSort"
|
||||
class="sticky"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {getValidatedPlan} from "../plan/util";
|
||||
import ListView from '../../ui/components/List/ListView.vue';
|
||||
import {getPreciseDuration} from "../../utils/duration";
|
||||
import ticker from 'utils/clock/Ticker';
|
||||
import {SORT_ORDER_OPTIONS} from "./constants";
|
||||
|
||||
import moment from "moment";
|
||||
import uuid from "uuid";
|
||||
|
||||
const SCROLL_TIMEOUT = 10000;
|
||||
const ROW_HEIGHT = 30;
|
||||
const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss:SSS';
|
||||
const headerItems = [
|
||||
{
|
||||
defaultDirection: true,
|
||||
isSortable: true,
|
||||
property: 'start',
|
||||
name: 'Start Time',
|
||||
format: function (value, object) {
|
||||
return `${moment(value).format(TIME_FORMAT)}Z`;
|
||||
}
|
||||
}, {
|
||||
defaultDirection: true,
|
||||
isSortable: true,
|
||||
property: 'end',
|
||||
name: 'End Time',
|
||||
format: function (value, object) {
|
||||
return `${moment(value).format(TIME_FORMAT)}Z`;
|
||||
}
|
||||
}, {
|
||||
defaultDirection: false,
|
||||
property: 'duration',
|
||||
name: 'Time To/From',
|
||||
format: function (value) {
|
||||
let result;
|
||||
if (value < 0) {
|
||||
result = `-${getPreciseDuration(Math.abs(value))}`;
|
||||
} else if (value > 0) {
|
||||
result = `+${getPreciseDuration(value)}`;
|
||||
} else {
|
||||
result = 'Now';
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}, {
|
||||
defaultDirection: true,
|
||||
property: 'name',
|
||||
name: 'Activity'
|
||||
}
|
||||
];
|
||||
|
||||
const defaultSort = {
|
||||
property: 'start',
|
||||
defaultDirection: true
|
||||
};
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ListView
|
||||
},
|
||||
inject: ['openmct', 'domainObject', 'path'],
|
||||
data() {
|
||||
return {
|
||||
viewBounds: undefined,
|
||||
height: 0,
|
||||
planActivities: [],
|
||||
headerItems: headerItems,
|
||||
defaultSort: defaultSort
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.isEditing = this.openmct.editor.isEditing();
|
||||
this.timestamp = Date.now();
|
||||
this.getPlanDataAndSetConfig(this.domainObject);
|
||||
|
||||
this.unlisten = this.openmct.objects.observe(this.domainObject, 'selectFile', this.getPlanDataAndSetConfig);
|
||||
this.unlistenConfig = this.openmct.objects.observe(this.domainObject, 'configuration', this.setViewFromConfig);
|
||||
this.removeStatusListener = this.openmct.status.observe(this.domainObject.identifier, this.setStatus);
|
||||
this.status = this.openmct.status.get(this.domainObject.identifier);
|
||||
this.unlistenTicker = ticker.listen(this.clearPreviousActivities);
|
||||
this.openmct.editor.on('isEditing', this.setEditState);
|
||||
|
||||
this.deferAutoScroll = _.debounce(this.deferAutoScroll, 500);
|
||||
this.$el.parentElement.addEventListener('scroll', this.deferAutoScroll, true);
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.unlisten) {
|
||||
this.unlisten();
|
||||
}
|
||||
|
||||
if (this.unlistenConfig) {
|
||||
this.unlistenConfig();
|
||||
}
|
||||
|
||||
if (this.unlistenTicker) {
|
||||
this.unlistenTicker();
|
||||
}
|
||||
|
||||
if (this.removeStatusListener) {
|
||||
this.removeStatusListener();
|
||||
}
|
||||
|
||||
this.openmct.editor.off('isEditing', this.setEditState);
|
||||
|
||||
this.$el.parentElement.removeEventListener('scroll', this.deferAutoScroll, true);
|
||||
if (this.clearAutoScrollDisabledTimer) {
|
||||
clearTimeout(this.clearAutoScrollDisabledTimer);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getPlanDataAndSetConfig(mutatedObject) {
|
||||
this.getPlanData(mutatedObject);
|
||||
this.setViewFromConfig(mutatedObject.configuration);
|
||||
},
|
||||
setViewFromConfig(configuration) {
|
||||
if (this.isEditing) {
|
||||
this.filterValue = configuration.filter;
|
||||
this.hideAll = false;
|
||||
this.showAll = true;
|
||||
this.listActivities();
|
||||
} else {
|
||||
this.filterValue = configuration.filter;
|
||||
this.setSort();
|
||||
this.setViewBounds();
|
||||
this.listActivities();
|
||||
}
|
||||
},
|
||||
getPlanData(domainObject) {
|
||||
this.planData = getValidatedPlan(domainObject);
|
||||
},
|
||||
setViewBounds() {
|
||||
const pastEventsIndex = this.domainObject.configuration.pastEventsIndex;
|
||||
const currentEventsIndex = this.domainObject.configuration.currentEventsIndex;
|
||||
const futureEventsIndex = this.domainObject.configuration.futureEventsIndex;
|
||||
const pastEventsDuration = this.domainObject.configuration.pastEventsDuration;
|
||||
const pastEventsDurationIndex = this.domainObject.configuration.pastEventsDurationIndex;
|
||||
const futureEventsDuration = this.domainObject.configuration.futureEventsDuration;
|
||||
const futureEventsDurationIndex = this.domainObject.configuration.futureEventsDurationIndex;
|
||||
|
||||
if (pastEventsIndex === 0 && futureEventsIndex === 0 && currentEventsIndex === 0) {
|
||||
//show all events
|
||||
this.showAll = false;
|
||||
this.viewBounds = undefined;
|
||||
this.hideAll = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.hideAll = false;
|
||||
|
||||
if (pastEventsIndex === 1 && futureEventsIndex === 1 && currentEventsIndex === 1) {
|
||||
//show all events
|
||||
this.showAll = true;
|
||||
this.viewBounds = undefined;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.showAll = false;
|
||||
|
||||
this.viewBounds = {};
|
||||
|
||||
this.noCurrent = currentEventsIndex === 0;
|
||||
|
||||
if (pastEventsIndex !== 1) {
|
||||
const pastDurationInMS = this.getDurationInMilliSeconds(pastEventsDuration, pastEventsDurationIndex);
|
||||
this.viewBounds.pastEnd = (timestamp) => {
|
||||
if (pastEventsIndex === 2) {
|
||||
return timestamp - pastDurationInMS;
|
||||
} else if (pastEventsIndex === 0) {
|
||||
return timestamp + 1;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (futureEventsIndex !== 1) {
|
||||
const futureDurationInMS = this.getDurationInMilliSeconds(futureEventsDuration, futureEventsDurationIndex);
|
||||
this.viewBounds.futureStart = (timestamp) => {
|
||||
if (futureEventsIndex === 2) {
|
||||
return timestamp + futureDurationInMS;
|
||||
} else if (futureEventsIndex === 0) {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
getDurationInMilliSeconds(duration, durationIndex) {
|
||||
if (duration > 0) {
|
||||
if (durationIndex === 0) {
|
||||
return duration * 1000;
|
||||
} else if (durationIndex === 1) {
|
||||
return duration * 60 * 1000;
|
||||
} else if (durationIndex === 2) {
|
||||
return duration * 60 * 24 * 1000;
|
||||
}
|
||||
}
|
||||
},
|
||||
listActivities() {
|
||||
let groups = Object.keys(this.planData);
|
||||
let activities = [];
|
||||
|
||||
groups.forEach((key) => {
|
||||
activities = activities.concat(this.planData[key]);
|
||||
});
|
||||
activities = activities.filter(this.filterActivities);
|
||||
activities = this.applyStyles(activities);
|
||||
this.setScrollTop();
|
||||
// sort by start time
|
||||
this.planActivities = activities.sort(this.sortByStartTime);
|
||||
},
|
||||
clearPreviousActivities(time) {
|
||||
if (time instanceof Date) {
|
||||
this.timestamp = time.getTime();
|
||||
} else {
|
||||
this.timestamp = time;
|
||||
}
|
||||
|
||||
this.listActivities();
|
||||
},
|
||||
filterActivities(activity, index) {
|
||||
|
||||
const hasFilterMatch = this.filterByName(activity.name);
|
||||
|
||||
if (hasFilterMatch === false || this.hideAll === true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.showAll === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
//current event or future start event or past end event
|
||||
const isCurrent = (this.noCurrent === false && this.timestamp >= activity.start && this.timestamp <= activity.end);
|
||||
const isPast = (this.timestamp > activity.end && (this.viewBounds.pastEnd === undefined || activity.end >= this.viewBounds.pastEnd(this.timestamp)));
|
||||
const isFuture = (this.timestamp < activity.start && (this.viewBounds.futureStart === undefined || activity.start <= this.viewBounds.futureStart(this.timestamp)));
|
||||
|
||||
return isCurrent || isPast || isFuture;
|
||||
},
|
||||
filterByName(name) {
|
||||
const filters = this.filterValue.split(',');
|
||||
|
||||
return filters.some((search => {
|
||||
const normalized = search.trim().toLowerCase();
|
||||
const regex = new RegExp(normalized);
|
||||
|
||||
return regex.test(name.toLowerCase());
|
||||
}));
|
||||
},
|
||||
applyStyles(activities) {
|
||||
let firstCurrentActivityIndex = -1;
|
||||
let currentActivitiesCount = 0;
|
||||
const styledActivities = activities.map((activity, index) => {
|
||||
if (this.timestamp >= activity.start && this.timestamp <= activity.end) {
|
||||
activity.cssClass = '--is-current';
|
||||
if (firstCurrentActivityIndex < 0) {
|
||||
firstCurrentActivityIndex = index;
|
||||
}
|
||||
|
||||
currentActivitiesCount = currentActivitiesCount + 1;
|
||||
} else if (this.timestamp < activity.start) {
|
||||
activity.cssClass = '--is-future';
|
||||
} else {
|
||||
activity.cssClass = '--is-past';
|
||||
}
|
||||
|
||||
if (!activity.key) {
|
||||
activity.key = uuid();
|
||||
}
|
||||
|
||||
activity.duration = activity.start - this.timestamp;
|
||||
|
||||
return activity;
|
||||
});
|
||||
|
||||
this.firstCurrentActivityIndex = firstCurrentActivityIndex;
|
||||
this.currentActivitiesCount = currentActivitiesCount;
|
||||
|
||||
return styledActivities;
|
||||
},
|
||||
canAutoScroll() {
|
||||
//this distinguishes between programmatic vs user-triggered scroll events
|
||||
this.autoScrolled = (this.dontAutoScroll !== true);
|
||||
|
||||
return this.autoScrolled;
|
||||
},
|
||||
resetScroll() {
|
||||
if (this.canAutoScroll() === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.firstCurrentActivityIndex = -1;
|
||||
this.currentActivitiesCount = 0;
|
||||
this.$el.parentElement.scrollTo({top: 0});
|
||||
this.autoScrolled = false;
|
||||
},
|
||||
setScrollTop() {
|
||||
//scroll to somewhere mid-way of the current activities
|
||||
if (this.firstCurrentActivityIndex > -1) {
|
||||
if (this.canAutoScroll() === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scrollOffset = this.currentActivitiesCount > 0 ? Math.floor(this.currentActivitiesCount / 2) : 0;
|
||||
this.$el.parentElement.scrollTo({
|
||||
top: ROW_HEIGHT * (this.firstCurrentActivityIndex + scrollOffset),
|
||||
behavior: "smooth"
|
||||
});
|
||||
this.autoScrolled = false;
|
||||
} else {
|
||||
this.resetScroll();
|
||||
}
|
||||
},
|
||||
deferAutoScroll() {
|
||||
//if this is not a user-triggered event, don't defer auto scrolling
|
||||
if (this.autoScrolled) {
|
||||
this.autoScrolled = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.dontAutoScroll = true;
|
||||
const self = this;
|
||||
if (this.clearAutoScrollDisabledTimer) {
|
||||
clearTimeout(this.clearAutoScrollDisabledTimer);
|
||||
}
|
||||
|
||||
this.clearAutoScrollDisabledTimer = setTimeout(() => {
|
||||
self.dontAutoScroll = false;
|
||||
self.setScrollTop();
|
||||
}, SCROLL_TIMEOUT);
|
||||
},
|
||||
setSort() {
|
||||
const sortOrder = SORT_ORDER_OPTIONS[this.domainObject.configuration.sortOrderIndex];
|
||||
const property = sortOrder.property;
|
||||
const direction = sortOrder.direction.toLowerCase() === 'asc';
|
||||
this.defaultSort = {
|
||||
property,
|
||||
defaultDirection: direction
|
||||
};
|
||||
},
|
||||
sortByStartTime(a, b) {
|
||||
const numA = parseInt(a.start, 10);
|
||||
const numB = parseInt(b.start, 10);
|
||||
|
||||
return numA - numB;
|
||||
},
|
||||
setStatus(status) {
|
||||
this.status = status;
|
||||
},
|
||||
setEditState(isEditing) {
|
||||
this.isEditing = isEditing;
|
||||
this.setViewFromConfig(this.domainObject.configuration);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
67
src/plugins/timelist/TimelistViewProvider.js
Normal file
67
src/plugins/timelist/TimelistViewProvider.js
Normal file
@@ -0,0 +1,67 @@
|
||||
/*****************************************************************************
|
||||
* 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 Timelist from './Timelist.vue';
|
||||
import { TIMELIST_TYPE } from './constants';
|
||||
import Vue from 'vue';
|
||||
|
||||
export default function TimelistViewProvider(openmct) {
|
||||
|
||||
return {
|
||||
key: 'timelist.view',
|
||||
name: 'Time List',
|
||||
cssClass: 'icon-timelist',
|
||||
canView(domainObject) {
|
||||
return domainObject.type === TIMELIST_TYPE;
|
||||
},
|
||||
|
||||
canEdit(domainObject) {
|
||||
return domainObject.type === TIMELIST_TYPE;
|
||||
},
|
||||
|
||||
view: function (domainObject, objectPath) {
|
||||
let component;
|
||||
|
||||
return {
|
||||
show: function (element) {
|
||||
|
||||
component = new Vue({
|
||||
el: element,
|
||||
components: {
|
||||
Timelist
|
||||
},
|
||||
provide: {
|
||||
openmct,
|
||||
domainObject,
|
||||
path: objectPath
|
||||
},
|
||||
template: '<timelist></timelist>'
|
||||
});
|
||||
},
|
||||
destroy: function () {
|
||||
component.$destroy();
|
||||
component = undefined;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
24
src/plugins/timelist/constants.js
Normal file
24
src/plugins/timelist/constants.js
Normal file
@@ -0,0 +1,24 @@
|
||||
export const SORT_ORDER_OPTIONS = [
|
||||
{
|
||||
label: 'Start ascending',
|
||||
property: 'start',
|
||||
direction: 'ASC'
|
||||
},
|
||||
{
|
||||
label: 'Start descending',
|
||||
property: 'start',
|
||||
direction: 'DESC'
|
||||
},
|
||||
{
|
||||
label: 'End ascending',
|
||||
property: 'end',
|
||||
direction: 'ASC'
|
||||
},
|
||||
{
|
||||
label: 'End descending',
|
||||
property: 'end',
|
||||
direction: 'DESC'
|
||||
}
|
||||
];
|
||||
|
||||
export const TIMELIST_TYPE = 'timelist';
|
||||
124
src/plugins/timelist/inspector/EventProperties.vue
Normal file
124
src/plugins/timelist/inspector/EventProperties.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<li class="c-inspect-properties__row">
|
||||
<div
|
||||
class="c-inspect-properties__label"
|
||||
title="Options for future events."
|
||||
>{{ label }}</div>
|
||||
<div
|
||||
v-if="canEdit"
|
||||
class="c-inspect-properties__value"
|
||||
>
|
||||
<select
|
||||
v-model="index"
|
||||
@change="updateForm('index')"
|
||||
>
|
||||
<option
|
||||
v-for="(activityOption, activityKey) in activitiesOptions"
|
||||
:key="activityKey"
|
||||
:value="activityKey"
|
||||
>{{ activityOption }}</option>
|
||||
</select>
|
||||
<input
|
||||
v-if="index === 2"
|
||||
v-model="duration"
|
||||
class="c-input c-input--sm"
|
||||
type="text"
|
||||
@change="updateForm('duration')"
|
||||
>
|
||||
<select
|
||||
v-if="index === 2"
|
||||
v-model="durationIndex"
|
||||
@change="updateForm('durationIndex')"
|
||||
>
|
||||
<option
|
||||
v-for="(durationOption, durationKey) in durationOptions"
|
||||
:key="durationKey"
|
||||
:value="durationKey"
|
||||
>{{ durationOption }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="c-inspect-properties__value"
|
||||
>
|
||||
{{ activitiesOptions[index] }} <span v-if="index > 1">{{ duration }} {{ durationOptions[durationIndex] }}</span>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const ACTIVITIES_OPTIONS = [
|
||||
'Don\'t show',
|
||||
'Show all',
|
||||
'Show starts within',
|
||||
'Show after end for'
|
||||
];
|
||||
|
||||
const DURATION_OPTIONS = [
|
||||
'seconds',
|
||||
'minutes',
|
||||
'hours'
|
||||
];
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject'],
|
||||
props: {
|
||||
label: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
prefix: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
index: this.domainObject.configuration[`${this.prefix}Index`],
|
||||
durationIndex: this.domainObject.configuration[`${this.prefix}DurationIndex`],
|
||||
duration: this.domainObject.configuration[`${this.prefix}Duration`],
|
||||
activitiesOptions: ACTIVITIES_OPTIONS,
|
||||
durationOptions: DURATION_OPTIONS,
|
||||
isEditing: this.openmct.editor.isEditing()
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
canEdit() {
|
||||
return this.isEditing && !this.domainObject.locked;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.prefix === 'futureEvents') {
|
||||
this.activitiesOptions = ACTIVITIES_OPTIONS.slice(0, 3);
|
||||
} else if (this.prefix === 'pastEvents') {
|
||||
this.activitiesOptions = ACTIVITIES_OPTIONS.filter((item, index) => index !== 2);
|
||||
} else if (this.prefix === 'currentEvents') {
|
||||
this.activitiesOptions = ACTIVITIES_OPTIONS.slice(0, 2);
|
||||
}
|
||||
|
||||
this.openmct.editor.on('isEditing', this.setEditState);
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.openmct.editor.off('isEditing', this.setEditState);
|
||||
},
|
||||
methods: {
|
||||
updateForm(property) {
|
||||
if (!this.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const capitalized = property.charAt(0).toUpperCase() + property.substr(1);
|
||||
this.$emit('updated', {
|
||||
property: `${this.prefix}${capitalized}`,
|
||||
value: this[property]
|
||||
});
|
||||
},
|
||||
isValid() {
|
||||
return this.index < 2 || (this.durationIndex >= 0 && this.duration > 0);
|
||||
},
|
||||
setEditState(isEditing) {
|
||||
this.isEditing = isEditing;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
91
src/plugins/timelist/inspector/Filtering.vue
Normal file
91
src/plugins/timelist/inspector/Filtering.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<li class="c-inspect-properties__row">
|
||||
<div
|
||||
v-if="canEdit"
|
||||
class="c-inspect-properties__hint span-all"
|
||||
>Filter this view by comma-separated keywords.</div>
|
||||
<div
|
||||
class="c-inspect-properties__label"
|
||||
title="Filter by keyword."
|
||||
>Filters</div>
|
||||
<div
|
||||
v-if="canEdit"
|
||||
class="c-inspect-properties__value"
|
||||
:class="{'form-error': hasError}"
|
||||
>
|
||||
<textarea
|
||||
v-model="filterValue"
|
||||
class="c-input--flex"
|
||||
type="text"
|
||||
@keydown.enter.exact.stop="forceBlur($event)"
|
||||
@keyup="updateForm($event, 'filter')"
|
||||
></textarea>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="c-inspect-properties__value"
|
||||
>
|
||||
{{ filterValue }}
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject'],
|
||||
data() {
|
||||
return {
|
||||
isEditing: this.openmct.editor.isEditing(),
|
||||
filterValue: this.domainObject.configuration.filter,
|
||||
hasError: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
canEdit() {
|
||||
return this.isEditing && !this.domainObject.locked;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.openmct.editor.on('isEditing', this.setEditState);
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.openmct.editor.off('isEditing', this.setEditState);
|
||||
},
|
||||
methods: {
|
||||
setEditState(isEditing) {
|
||||
this.isEditing = isEditing;
|
||||
if (!this.isEditing && this.hasError) {
|
||||
this.filterValue = this.domainObject.configuration.filter;
|
||||
this.hasError = false;
|
||||
}
|
||||
},
|
||||
forceBlur(event) {
|
||||
event.target.blur();
|
||||
},
|
||||
updateForm(event, property) {
|
||||
if (!this.isValid()) {
|
||||
this.hasError = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.hasError = false;
|
||||
|
||||
this.$emit('updated', {
|
||||
property,
|
||||
value: this.filterValue.replace(/,(\s)*$/, '')
|
||||
});
|
||||
},
|
||||
isValid() {
|
||||
// Test for any word character, any whitespace character or comma
|
||||
if (this.filterValue === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
const regex = new RegExp(/^([a-zA-Z0-9_\-\s,])+$/g);
|
||||
|
||||
return regex.test(this.filterValue);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,70 @@
|
||||
/*****************************************************************************
|
||||
* 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 TimelistPropertiesView from "./TimelistPropertiesView.vue";
|
||||
import { TIMELIST_TYPE } from '../constants';
|
||||
import Vue from 'vue';
|
||||
|
||||
export default function TimeListInspectorViewProvider(openmct) {
|
||||
return {
|
||||
key: 'timelist-inspector',
|
||||
name: 'Timelist Inspector View',
|
||||
canView: function (selection) {
|
||||
if (selection.length === 0 || selection[0].length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let context = selection[0][0].context;
|
||||
|
||||
return context && context.item
|
||||
&& context.item.type === TIMELIST_TYPE;
|
||||
},
|
||||
view: function (selection) {
|
||||
let component;
|
||||
|
||||
return {
|
||||
show: function (element) {
|
||||
component = new Vue({
|
||||
el: element,
|
||||
components: {
|
||||
TimelistPropertiesView: TimelistPropertiesView
|
||||
},
|
||||
provide: {
|
||||
openmct,
|
||||
domainObject: selection[0][0].context.item
|
||||
},
|
||||
template: '<timelist-properties-view></timelist-properties-view>'
|
||||
});
|
||||
},
|
||||
destroy: function () {
|
||||
if (component) {
|
||||
component.$destroy();
|
||||
component = undefined;
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
priority: function () {
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
}
|
||||
146
src/plugins/timelist/inspector/TimelistPropertiesView.vue
Normal file
146
src/plugins/timelist/inspector/TimelistPropertiesView.vue
Normal file
@@ -0,0 +1,146 @@
|
||||
<!--
|
||||
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-timelist-properties">
|
||||
<div class="c-inspect-properties">
|
||||
<ul class="c-inspect-properties__section">
|
||||
<div
|
||||
class="c-inspect-properties_header"
|
||||
title="'Timeframe options'"
|
||||
>Timeframe</div>
|
||||
<li class="c-inspect-properties__row">
|
||||
<div
|
||||
v-if="canEdit"
|
||||
class="c-inspect-properties__hint span-all"
|
||||
>These settings are not previewed and will be applied after editing is completed.</div>
|
||||
<div
|
||||
class="c-inspect-properties__label"
|
||||
title="Sort order of the timelist."
|
||||
>Sort Order</div>
|
||||
<div
|
||||
v-if="canEdit"
|
||||
class="c-inspect-properties__value"
|
||||
>
|
||||
<select
|
||||
v-model="sortOrderIndex"
|
||||
@change="updateSortOrder()"
|
||||
>
|
||||
<option
|
||||
v-for="(sortOrderOption, index) in sortOrderOptions"
|
||||
:key="index"
|
||||
:value="index"
|
||||
>{{ sortOrderOption.label }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="c-inspect-properties__value"
|
||||
>
|
||||
{{ sortOrderOptions[sortOrderIndex].label }}
|
||||
</div>
|
||||
</li>
|
||||
<event-properties
|
||||
v-for="type in eventTypes"
|
||||
:key="type.prefix"
|
||||
:label="type.label"
|
||||
:prefix="type.prefix"
|
||||
@updated="eventPropertiesUpdated"
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="c-inspect-properties">
|
||||
<ul class="c-inspect-properties__section">
|
||||
<div
|
||||
class="c-inspect-properties_header"
|
||||
title="'Filters'"
|
||||
>Filtering</div>
|
||||
<filtering @updated="eventPropertiesUpdated" />
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import EventProperties from './EventProperties.vue';
|
||||
import { SORT_ORDER_OPTIONS } from '../constants';
|
||||
import Filtering from './Filtering.vue';
|
||||
|
||||
const EVENT_TYPES = [
|
||||
{
|
||||
label: 'Future Events',
|
||||
prefix: 'futureEvents'
|
||||
},
|
||||
{
|
||||
label: 'Current Events',
|
||||
prefix: 'currentEvents'
|
||||
},
|
||||
{
|
||||
label: 'Past Events',
|
||||
prefix: 'pastEvents'
|
||||
}
|
||||
];
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Filtering,
|
||||
EventProperties
|
||||
},
|
||||
inject: ['openmct', 'domainObject'],
|
||||
data() {
|
||||
return {
|
||||
sortOrderIndex: this.domainObject.configuration.sortOrderIndex,
|
||||
sortOrderOptions: SORT_ORDER_OPTIONS,
|
||||
eventTypes: EVENT_TYPES,
|
||||
isEditing: this.openmct.editor.isEditing()
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
canEdit() {
|
||||
return this.isEditing && !this.domainObject.locked;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.openmct.editor.on('isEditing', this.setEditState);
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.openmct.editor.off('isEditing', this.setEditState);
|
||||
},
|
||||
methods: {
|
||||
setEditState(isEditing) {
|
||||
this.isEditing = isEditing;
|
||||
},
|
||||
updateSortOrder() {
|
||||
this.updateProperty('sortOrderIndex', this.sortOrderIndex);
|
||||
},
|
||||
updateProperty(key, value) {
|
||||
this.openmct.objects.mutate(this.domainObject, `configuration.${key}`, value);
|
||||
},
|
||||
eventPropertiesUpdated(data) {
|
||||
const key = data.property;
|
||||
const value = data.value;
|
||||
this.updateProperty(key, value);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
68
src/plugins/timelist/plugin.js
Normal file
68
src/plugins/timelist/plugin.js
Normal file
@@ -0,0 +1,68 @@
|
||||
/*****************************************************************************
|
||||
* 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 TimelistViewProvider from './TimelistViewProvider';
|
||||
import { TIMELIST_TYPE } from './constants';
|
||||
import TimeListInspectorViewProvider from "./inspector/TimeListInspectorViewProvider";
|
||||
|
||||
export default function () {
|
||||
return function install(openmct) {
|
||||
openmct.types.addType(TIMELIST_TYPE, {
|
||||
name: 'Time List',
|
||||
key: TIMELIST_TYPE,
|
||||
description: 'A configurable, time-ordered list view of activities for a compatible mission plan file.',
|
||||
creatable: true,
|
||||
cssClass: 'icon-timelist',
|
||||
form: [
|
||||
{
|
||||
name: 'Upload Plan (JSON File)',
|
||||
key: 'selectFile',
|
||||
control: 'file-input',
|
||||
required: true,
|
||||
text: 'Select File...',
|
||||
type: 'application/json',
|
||||
property: [
|
||||
"selectFile"
|
||||
]
|
||||
}
|
||||
],
|
||||
initialize: function (domainObject) {
|
||||
domainObject.configuration = {
|
||||
sortOrderIndex: 0,
|
||||
futureEventsIndex: 0,
|
||||
futureEventsDurationIndex: 0,
|
||||
futureEventsDuration: 20,
|
||||
currentEventsIndex: 1,
|
||||
currentEventsDurationIndex: 0,
|
||||
currentEventsDuration: 20,
|
||||
pastEventsIndex: 0,
|
||||
pastEventsDurationIndex: 0,
|
||||
pastEventsDuration: 20,
|
||||
filter: ''
|
||||
};
|
||||
}
|
||||
});
|
||||
openmct.objectViews.addProvider(new TimelistViewProvider(openmct));
|
||||
openmct.inspectorViews.addProvider(new TimeListInspectorViewProvider(openmct));
|
||||
|
||||
};
|
||||
}
|
||||
181
src/plugins/timelist/pluginSpec.js
Normal file
181
src/plugins/timelist/pluginSpec.js
Normal file
@@ -0,0 +1,181 @@
|
||||
/*****************************************************************************
|
||||
* 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 TimelistPlugin from "./plugin";
|
||||
import { TIMELIST_TYPE } from "./constants";
|
||||
import Vue from 'vue';
|
||||
import moment from "moment";
|
||||
|
||||
const LIST_ITEM_CLASS = '.js-table__body .js-list-item';
|
||||
const LIST_ITEM_VALUE_CLASS = '.js-list-item__value';
|
||||
const LIST_ITEM_BODY_CLASS = '.js-table__body th';
|
||||
|
||||
describe('the plugin', function () {
|
||||
let timelistDefinition;
|
||||
let element;
|
||||
let child;
|
||||
let openmct;
|
||||
let appHolder;
|
||||
let originalRouterPath;
|
||||
|
||||
beforeEach((done) => {
|
||||
appHolder = document.createElement('div');
|
||||
appHolder.style.width = '640px';
|
||||
appHolder.style.height = '480px';
|
||||
|
||||
openmct = createOpenMct();
|
||||
openmct.install(new TimelistPlugin());
|
||||
|
||||
timelistDefinition = openmct.types.get(TIMELIST_TYPE).definition;
|
||||
|
||||
element = document.createElement('div');
|
||||
element.style.width = '640px';
|
||||
element.style.height = '480px';
|
||||
child = document.createElement('div');
|
||||
child.style.width = '640px';
|
||||
child.style.height = '480px';
|
||||
element.appendChild(child);
|
||||
|
||||
originalRouterPath = openmct.router.path;
|
||||
|
||||
openmct.on('start', done);
|
||||
openmct.start(appHolder);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
openmct.router.path = originalRouterPath;
|
||||
|
||||
return resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
let mockTimelistObject = {
|
||||
name: 'Timelist',
|
||||
key: TIMELIST_TYPE,
|
||||
creatable: true
|
||||
};
|
||||
|
||||
it('defines a timelist object type with the correct key', () => {
|
||||
expect(timelistDefinition.key).toEqual(mockTimelistObject.key);
|
||||
});
|
||||
|
||||
it('is creatable', () => {
|
||||
expect(timelistDefinition.creatable).toEqual(mockTimelistObject.creatable);
|
||||
});
|
||||
|
||||
describe('the timelist view', () => {
|
||||
it('provides a timelist view', () => {
|
||||
const testViewObject = {
|
||||
id: "test-object",
|
||||
type: TIMELIST_TYPE
|
||||
};
|
||||
openmct.router.path = [testViewObject];
|
||||
|
||||
const applicableViews = openmct.objectViews.get(testViewObject, [testViewObject]);
|
||||
let timelistView = applicableViews.find((viewProvider) => viewProvider.key === 'timelist.view');
|
||||
expect(timelistView).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('the timelist view displays activities', () => {
|
||||
let timelistDomainObject;
|
||||
let timelistView;
|
||||
|
||||
beforeEach(() => {
|
||||
timelistDomainObject = {
|
||||
identifier: {
|
||||
key: 'test-object',
|
||||
namespace: ''
|
||||
},
|
||||
type: TIMELIST_TYPE,
|
||||
id: "test-object",
|
||||
configuration: {
|
||||
sortOrderIndex: 0,
|
||||
futureEventsIndex: 1,
|
||||
futureEventsDurationIndex: 0,
|
||||
futureEventsDuration: 20,
|
||||
currentEventsIndex: 1,
|
||||
currentEventsDurationIndex: 0,
|
||||
currentEventsDuration: 20,
|
||||
pastEventsIndex: 1,
|
||||
pastEventsDurationIndex: 0,
|
||||
pastEventsDuration: 20,
|
||||
filter: ''
|
||||
},
|
||||
selectFile: {
|
||||
body: JSON.stringify({
|
||||
"TEST-GROUP": [
|
||||
{
|
||||
"name": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua",
|
||||
"start": 1597170002854,
|
||||
"end": 1597171032854,
|
||||
"type": "TEST-GROUP",
|
||||
"color": "fuchsia",
|
||||
"textColor": "black"
|
||||
},
|
||||
{
|
||||
"name": "Sed ut perspiciatis",
|
||||
"start": 1597171132854,
|
||||
"end": 1597171232854,
|
||||
"type": "TEST-GROUP",
|
||||
"color": "fuchsia",
|
||||
"textColor": "black"
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
openmct.router.path = [timelistDomainObject];
|
||||
|
||||
const applicableViews = openmct.objectViews.get(timelistDomainObject, [timelistDomainObject]);
|
||||
timelistView = applicableViews.find((viewProvider) => viewProvider.key === 'timelist.view');
|
||||
let view = timelistView.view(timelistDomainObject, []);
|
||||
view.show(child, true);
|
||||
|
||||
return Vue.nextTick();
|
||||
});
|
||||
|
||||
it('displays the activities', () => {
|
||||
const items = element.querySelectorAll(LIST_ITEM_CLASS);
|
||||
expect(items.length).toEqual(2);
|
||||
});
|
||||
|
||||
it('displays the activity headers', () => {
|
||||
const headers = element.querySelectorAll(LIST_ITEM_BODY_CLASS);
|
||||
expect(headers.length).toEqual(4);
|
||||
});
|
||||
|
||||
it('displays activity details', (done) => {
|
||||
Vue.nextTick(() => {
|
||||
const itemEls = element.querySelectorAll(LIST_ITEM_CLASS);
|
||||
const itemValues = itemEls[0].querySelectorAll(LIST_ITEM_VALUE_CLASS);
|
||||
expect(itemValues.length).toEqual(4);
|
||||
expect(itemValues[3].innerHTML.trim()).toEqual('Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua');
|
||||
expect(itemValues[0].innerHTML.trim()).toEqual(`${moment(1597170002854).format('YYYY-MM-DD HH:mm:ss:SSS')}Z`);
|
||||
expect(itemValues[1].innerHTML.trim()).toEqual(`${moment(1597171032854).format('YYYY-MM-DD HH:mm:ss:SSS')}Z`);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
55
src/plugins/timelist/timelist.scss
Normal file
55
src/plugins/timelist/timelist.scss
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.
|
||||
*****************************************************************************/
|
||||
|
||||
.c-timelist {
|
||||
& .nowMarker.hasCurrent {
|
||||
height: 2px;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
background: cyan;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.c-list-item {
|
||||
/* Time Lists */
|
||||
|
||||
&.--is-current {
|
||||
background-color: $colorCurrentBg;
|
||||
border-top: 1px solid $colorCurrentBorder !important;
|
||||
color: $colorCurrentFg;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&.--is-future {
|
||||
background-color: $colorFutureBg;
|
||||
border-top-color: $colorFutureBorder !important;
|
||||
color: $colorFutureFg;
|
||||
}
|
||||
|
||||
&__value {
|
||||
&.--duration {
|
||||
width: 5%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -38,41 +38,41 @@ export default class ViewLargeAction {
|
||||
}
|
||||
|
||||
invoke(objectPath, view) {
|
||||
const parentElement = view.parentElement;
|
||||
let childElement = parentElement && parentElement.firstChild;
|
||||
performance.mark('viewlarge.start');
|
||||
const childElement = view?.parentElement?.firstChild;
|
||||
if (!childElement) {
|
||||
const message = "ViewLargeAction: missing element";
|
||||
this.openmct.notifications.error(message);
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
this._expand(objectPath, childElement);
|
||||
this._expand(objectPath, view);
|
||||
}
|
||||
|
||||
appliesTo(objectPath, view = {}) {
|
||||
const parentElement = view.parentElement;
|
||||
const element = parentElement && parentElement.firstChild;
|
||||
const viewLargeAction = element && !element.classList.contains('js-main-container')
|
||||
appliesTo(objectPath, view) {
|
||||
const childElement = view?.parentElement?.firstChild;
|
||||
|
||||
return childElement && !childElement.classList.contains('js-main-container')
|
||||
&& !this.openmct.router.isNavigatedObject(objectPath);
|
||||
|
||||
return viewLargeAction;
|
||||
}
|
||||
|
||||
_expand(objectPath, childElement) {
|
||||
const parentElement = childElement.parentElement;
|
||||
_expand(objectPath, view) {
|
||||
const element = this._getPreview(objectPath, view);
|
||||
|
||||
this.overlay = this.openmct.overlays.overlay({
|
||||
element: this._getPreview(objectPath),
|
||||
element,
|
||||
size: 'large',
|
||||
autoHide: false,
|
||||
onDestroy() {
|
||||
parentElement.append(childElement);
|
||||
onDestroy: () => {
|
||||
this.preview.$destroy();
|
||||
this.preview = undefined;
|
||||
delete this.preview;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_getPreview(objectPath) {
|
||||
const preview = new Vue({
|
||||
_getPreview(objectPath, view) {
|
||||
this.preview = new Vue({
|
||||
components: {
|
||||
Preview
|
||||
},
|
||||
@@ -80,9 +80,14 @@ export default class ViewLargeAction {
|
||||
openmct: this.openmct,
|
||||
objectPath
|
||||
},
|
||||
template: '<Preview></Preview>'
|
||||
data() {
|
||||
return {
|
||||
view
|
||||
};
|
||||
},
|
||||
template: '<Preview :existing-view="view"></Preview>'
|
||||
});
|
||||
|
||||
return preview.$mount().$el;
|
||||
return this.preview.$mount().$el;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,7 +345,7 @@ $shdwItemText: none;
|
||||
$colorTabBorder: pullForward($colorBodyBg, 10%);
|
||||
$colorTabBodyBg: $colorBodyBg;
|
||||
$colorTabBodyFg: pullForward($colorBodyFg, 20%);
|
||||
$colorTabHeaderBg: rgba($colorBodyFg, 0.15);
|
||||
$colorTabHeaderBg: #575757;
|
||||
$colorTabHeaderFg: $colorBodyFg;
|
||||
$colorTabHeaderBorder: $colorBodyBg;
|
||||
$colorTabGroupHeaderBg: pullForward($colorBodyBg, 5%);
|
||||
@@ -366,6 +366,24 @@ $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%);
|
||||
$colorCurrentBorder: $colorBodyBg;
|
||||
$colorFutureBg: rgba($colorKey, 0.2);
|
||||
$colorFutureFg: $colorCurrentFg;
|
||||
$colorFutureBorder: $colorCurrentBorder;
|
||||
|
||||
// Tree
|
||||
$colorTreeBg: transparent;
|
||||
$colorItemTreeHoverBg: rgba(#fff, 0.1);
|
||||
|
||||
@@ -349,7 +349,7 @@ $shdwItemText: none;
|
||||
$colorTabBorder: pullForward($colorBodyBg, 10%);
|
||||
$colorTabBodyBg: $colorBodyBg;
|
||||
$colorTabBodyFg: pullForward($colorBodyFg, 20%);
|
||||
$colorTabHeaderBg: rgba($colorBodyFg, 0.15);
|
||||
$colorTabHeaderBg: #575757;
|
||||
$colorTabHeaderFg: $colorBodyFg;
|
||||
$colorTabHeaderBorder: $colorBodyBg;
|
||||
$colorTabGroupHeaderBg: pullForward($colorBodyBg, 5%);
|
||||
@@ -370,6 +370,24 @@ $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%);
|
||||
$colorCurrentBorder: #fff;
|
||||
$colorFutureBg: rgba($colorKey, 0.2);
|
||||
$colorFutureFg: $colorCurrentFg;
|
||||
$colorFutureBorder: $colorCurrentBorder;
|
||||
|
||||
// Tree
|
||||
$colorTreeBg: transparent;
|
||||
$colorItemTreeHoverBg: rgba(#fff, 0.03);
|
||||
|
||||
@@ -345,7 +345,7 @@ $shdwItemText: none;
|
||||
$colorTabBorder: pullForward($colorBodyBg, 10%);
|
||||
$colorTabBodyBg: $colorBodyBg;
|
||||
$colorTabBodyFg: pullForward($colorBodyFg, 20%);
|
||||
$colorTabHeaderBg: rgba($colorBodyFg, 0.2);
|
||||
$colorTabHeaderBg: #e2e2e2;
|
||||
$colorTabHeaderFg: $colorBodyFg;
|
||||
$colorTabHeaderBorder: $colorBodyBg;
|
||||
$colorTabGroupHeaderBg: pullForward($colorBodyBg, 5%);
|
||||
@@ -366,6 +366,24 @@ $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%);
|
||||
$colorCurrentBorder: #fff;
|
||||
$colorFutureBg: rgba($colorKey, 0.2);
|
||||
$colorFutureFg: $colorCurrentFg;
|
||||
$colorFutureBorder: $colorCurrentBorder;
|
||||
|
||||
// Tree
|
||||
$colorTreeBg: transparent;
|
||||
$colorItemTreeHoverBg: rgba(black, 0.07);
|
||||
|
||||
@@ -263,6 +263,7 @@ $glyph-icon-telemetry-aggregate: '\eb2b';
|
||||
$glyph-icon-bar-chart: '\eb2c';
|
||||
$glyph-icon-map: '\eb2d';
|
||||
$glyph-icon-plan: '\eb2e';
|
||||
$glyph-icon-timelist: '\eb2f';
|
||||
|
||||
/************************** GLYPHS AS DATA URI */
|
||||
// Only objects have been converted, for use in Create menu and folder views
|
||||
@@ -317,3 +318,4 @@ $bg-icon-condition-widget: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='h
|
||||
$bg-icon-bar-chart: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M416 0H96C43.2 0 0 43.2 0 96v320c0 52.8 43.2 96 96 96h320c52.8 0 96-43.2 96-96V96c0-52.8-43.2-96-96-96ZM133.82 448H64V224h69.82Zm104.73 0h-69.82V64h69.82Zm104.72 0h-69.82V288h69.82ZM448 448h-69.82V128H448Z'/%3e%3c/svg%3e");
|
||||
$bg-icon-map: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cpath fill='%23000000' d='M448 32.7 384 64v448l64-31.3c35.2-17.21 64-60.1 64-95.3v-320c0-35.2-28.8-49.91-64-32.7ZM160 456l193.6 48.4v-448L160 8v448zM129.6.4 128 0 64 31.3C28.8 48.51 0 91.4 0 126.6v320c0 35.2 28.8 49.91 64 32.7l64-31.3 1.6.4Z'/%3e%3c/svg%3e");
|
||||
$bg-icon-plan: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cg data-name='Layer 2'%3e%3cg data-name='Layer 1'%3e%3cpath fill='%23000000' d='M128 96V64a64.19 64.19 0 0 1 64-64h128a64.19 64.19 0 0 1 64 64v32Z'/%3e%3cpath fill='%23000000' d='M416 64v64H96V64c-52.8 0-96 43.2-96 96v256c0 52.8 43.2 96 96 96h320c52.8 0 96-43.2 96-96V160c0-52.8-43.2-96-96-96ZM64 288v-64h128v64Zm256 128H128v-64h192Zm128 0h-64v-64h64Zm0-128H256v-64h192Z'/%3e%3c/g%3e%3c/g%3e%3c/svg%3e");
|
||||
$bg-icon-timelist: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3e%3cg data-name='Layer 2'%3e%3cpath d='M448 0H64A64.19 64.19 0 0 0 0 64v384a64.19 64.19 0 0 0 64 64h384a64.19 64.19 0 0 0 64-64V64a64.19 64.19 0 0 0-64-64ZM213.47 266.73a24 24 0 0 1-32.2 10.74L104 238.83V128a24 24 0 0 1 48 0v81.17l50.73 25.36a24 24 0 0 1 10.74 32.2ZM448 448H288v-64h160Zm0-96H288v-64h160Zm0-96H288v-64h160Zm0-96H288V96h160Z' data-name='Layer 1'/%3e%3c/g%3e%3c/svg%3e");
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -194,6 +194,7 @@
|
||||
.icon-bar-chart { @include glyphBefore($glyph-icon-bar-chart); }
|
||||
.icon-map { @include glyphBefore($glyph-icon-map); }
|
||||
.icon-plan { @include glyphBefore($glyph-icon-plan); }
|
||||
.icon-timelist { @include glyphBefore($glyph-icon-timelist); }
|
||||
|
||||
/************************** 12 PX CLASSES */
|
||||
// TODO: sync with 16px redo as of 10/25/18
|
||||
@@ -256,3 +257,4 @@
|
||||
.bg-icon-bar-chart { @include glyphBg($bg-icon-bar-chart); }
|
||||
.bg-icon-map { @include glyphBg($bg-icon-map); }
|
||||
.bg-icon-plan { @include glyphBg($bg-icon-plan); }
|
||||
.bg-icon-timelist { @include glyphBg($bg-icon-timelist); }
|
||||
|
||||
@@ -160,6 +160,10 @@ mct-plot {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
.alt-pressed {
|
||||
// When alt is being pressed and user is hovering over the plot, set the cursor
|
||||
@include cursorGrab();
|
||||
}
|
||||
|
||||
.gl-plot-axis-area.gl-plot-x {
|
||||
top: auto;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -162,4 +162,5 @@
|
||||
<glyph unicode="" glyph-name="icon-bar-graph" d="M832 832h-640c-105.6 0-192-86.4-192-192v-640c0-105.6 86.4-192 192-192h640c105.6 0 192 86.4 192 192v640c0 105.6-86.4 192-192 192zM267.64-64h-139.64v448h139.64zM477.1-64h-139.64v768h139.64zM686.54-64h-139.64v320h139.64zM896-64h-139.64v640h139.64z" />
|
||||
<glyph unicode="" glyph-name="icon-map" d="M896 766.6l-128-62.6v-896l128 62.6c70.4 34.42 128 120.2 128 190.6v640c0 70.4-57.6 99.82-128 65.4zM320-80l387.2-96.8v896l-387.2 96.8v-896zM259.2 831.2l-3.2 0.8-128-62.6c-70.4-34.42-128-120.2-128-190.6v-640c0-70.4 57.6-99.82 128-65.4l128 62.6 3.2-0.8z" />
|
||||
<glyph unicode="" glyph-name="icon-plan" d="M256 640v64c0.215 70.606 57.394 127.785 127.979 128h256.021c70.606-0.215 127.785-57.394 128-127.979v-64.021zM832 704v-128h-640v128c-105.6 0-192-86.4-192-192v-512c0-105.6 86.4-192 192-192h640c105.6 0 192 86.4 192 192v512c0 105.6-86.4 192-192 192zM128 256v128h256v-128zM640 0h-384v128h384zM896 0h-128v128h128zM896 256h-384v128h384z" />
|
||||
<glyph unicode="" glyph-name="icon-timelist" d="M896 832h-768c-70.606-0.215-127.785-57.394-128-127.979v-768.021c0.215-70.606 57.394-127.785 127.979-128h768.021c70.606 0.215 127.785 57.394 128 127.979v768.021c-0.215 70.606-57.394 127.785-127.979 128h-0.021zM426.94 298.54c-8.054-15.864-24.249-26.545-42.938-26.545-7.823 0-15.209 1.871-21.734 5.191l0.273-0.126-154.54 77.28v221.66c0 26.51 21.49 48 48 48s48-21.49 48-48v0-162.34l101.46-50.72c15.864-8.054 26.545-24.249 26.545-42.938 0-7.823-1.871-15.209-5.191-21.734l0.126 0.273zM896-64h-320v128h320zM896 128h-320v128h320zM896 320h-320v128h320zM896 512h-320v128h320z" />
|
||||
</font></defs></svg>
|
||||
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 63 KiB |
Binary file not shown.
Binary file not shown.
@@ -15,7 +15,6 @@
|
||||
@import "../plugins/flexibleLayout/components/flexible-layout.scss";
|
||||
@import "../plugins/folderView/components/grid-view.scss";
|
||||
@import "../plugins/folderView/components/list-item.scss";
|
||||
@import "../plugins/folderView/components/list-view.scss";
|
||||
@import "../plugins/imagery/components/imagery-view.scss";
|
||||
@import "../plugins/imagery/components/Compass/compass.scss";
|
||||
@import "../plugins/telemetryTable/components/table-row.scss";
|
||||
@@ -28,6 +27,7 @@
|
||||
@import "../plugins/timeConductor/conductor-mode-icon.scss";
|
||||
@import "../plugins/timeConductor/date-picker.scss";
|
||||
@import "../plugins/timeline/timeline.scss";
|
||||
@import "../plugins/timelist/timelist.scss";
|
||||
@import "../plugins/plan/plan";
|
||||
@import "../plugins/viewDatumAction/components/metadata-list.scss";
|
||||
@import "../ui/components/object-frame.scss";
|
||||
@@ -37,6 +37,7 @@
|
||||
@import "../ui/components/swim-lane/swimlane.scss";
|
||||
@import "../ui/components/toggle-switch.scss";
|
||||
@import "../ui/components/timesystem-axis.scss";
|
||||
@import "../ui/components/List/list-view.scss";
|
||||
@import "../ui/inspector/elements.scss";
|
||||
@import "../ui/inspector/inspector.scss";
|
||||
@import "../ui/inspector/location.scss";
|
||||
@@ -52,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;
|
||||
|
||||
56
src/ui/components/List/ListHeader.vue
Normal file
56
src/ui/components/List/ListHeader.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<th
|
||||
v-if="isSortable"
|
||||
class="is-sortable"
|
||||
:class="{
|
||||
'is-sorting': currentSort === property,
|
||||
'asc': direction,
|
||||
'desc': !direction
|
||||
}"
|
||||
@click="sort(property, direction)"
|
||||
>
|
||||
{{ title }}
|
||||
</th>
|
||||
<th v-else>
|
||||
{{ title }}
|
||||
</th>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
props: {
|
||||
property: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
currentSort: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
direction: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
isSortable: {
|
||||
type: Boolean,
|
||||
default() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
sort(property, direction) {
|
||||
this.$emit('sort', {
|
||||
property,
|
||||
direction
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
53
src/ui/components/List/ListItem.vue
Normal file
53
src/ui/components/List/ListItem.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<tr
|
||||
class="c-list-item js-list-item"
|
||||
:class="item.cssClass || ''"
|
||||
>
|
||||
<td
|
||||
v-for="itemValue in formattedItemValues"
|
||||
:key="itemValue.key"
|
||||
class="c-list-item__value js-list-item__value"
|
||||
:class="['--' + itemValue.key]"
|
||||
:title="itemValue.text"
|
||||
>
|
||||
{{ itemValue.text }}
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash';
|
||||
|
||||
export default {
|
||||
inject: ['openmct'],
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
itemProperties: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
formattedItemValues() {
|
||||
let values = [];
|
||||
this.itemProperties.forEach((property) => {
|
||||
// eslint-disable-next-line you-dont-need-lodash-underscore/get
|
||||
let value = _.get(this.item, property.key);
|
||||
if (property.format) {
|
||||
value = property.format(value, this.item, property.key);
|
||||
}
|
||||
|
||||
values.push({
|
||||
text: value,
|
||||
key: property.key
|
||||
});
|
||||
});
|
||||
|
||||
return values;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
142
src/ui/components/List/ListView.vue
Normal file
142
src/ui/components/List/ListView.vue
Normal file
@@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<div class="c-table c-table--sortable c-list-view c-list-view--sticky-header">
|
||||
<table class="c-table__body js-table__body">
|
||||
<thead class="c-table__header">
|
||||
<tr>
|
||||
<list-header
|
||||
v-for="headerItem in headerItems"
|
||||
:key="headerItem.property"
|
||||
:direction="sortBy === headerItem.property ? ascending : headerItem.defaultDirection"
|
||||
:is-sortable="headerItem.isSortable"
|
||||
:title="headerItem.name"
|
||||
:property="headerItem.property"
|
||||
:current-sort="sortBy"
|
||||
@sort="sort"
|
||||
/>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<list-item
|
||||
v-for="item in sortedItems"
|
||||
:key="item.key"
|
||||
:item="item"
|
||||
:item-properties="itemProperties"
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ListItem from './ListItem.vue';
|
||||
import ListHeader from './ListHeader.vue';
|
||||
import _ from 'lodash';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ListItem,
|
||||
ListHeader
|
||||
},
|
||||
inject: ['domainObject', 'openmct'],
|
||||
props: {
|
||||
headerItems: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
items: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
defaultSort: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {
|
||||
property: '',
|
||||
defaultDirection: true
|
||||
};
|
||||
}
|
||||
},
|
||||
storageKey: {
|
||||
type: String,
|
||||
default() {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
let sortBy = this.defaultSort.property;
|
||||
let ascending = this.defaultSort.defaultDirection;
|
||||
if (this.storageKey) {
|
||||
let persistedSortOrder = window.localStorage.getItem(this.storageKey);
|
||||
|
||||
if (persistedSortOrder) {
|
||||
let parsed = JSON.parse(persistedSortOrder);
|
||||
|
||||
sortBy = parsed.sortBy;
|
||||
ascending = parsed.ascending;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
sortBy,
|
||||
ascending
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
sortedItems() {
|
||||
let sortedItems = _.sortBy(this.items, this.sortBy);
|
||||
if (!this.ascending) {
|
||||
sortedItems = sortedItems.reverse();
|
||||
}
|
||||
|
||||
return sortedItems;
|
||||
},
|
||||
itemProperties() {
|
||||
return this.headerItems.map((headerItem) => {
|
||||
return {
|
||||
key: headerItem.property,
|
||||
format: headerItem.format
|
||||
};
|
||||
});
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
defaultSort: {
|
||||
handler() {
|
||||
this.setSort();
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setSort() {
|
||||
this.sortBy = this.defaultSort.property;
|
||||
this.ascending = this.defaultSort.defaultDirection;
|
||||
},
|
||||
sort(data) {
|
||||
const property = data.property;
|
||||
const direction = data.direction;
|
||||
|
||||
if (this.sortBy === property) {
|
||||
this.ascending = !this.ascending;
|
||||
} else {
|
||||
this.sortBy = property;
|
||||
this.ascending = direction;
|
||||
}
|
||||
|
||||
if (this.storageKey) {
|
||||
window.localStorage
|
||||
.setItem(
|
||||
this.storageKey,
|
||||
JSON.stringify(
|
||||
{
|
||||
sortBy: this.sortBy,
|
||||
ascending: this.ascending
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
40
src/ui/components/List/list-view.scss
Normal file
40
src/ui/components/List/list-view.scss
Normal file
@@ -0,0 +1,40 @@
|
||||
/******************************* LIST VIEW */
|
||||
.c-list-view {
|
||||
tbody tr {
|
||||
background: $colorListItemBg;
|
||||
transition: $transOut;
|
||||
}
|
||||
|
||||
td {
|
||||
$p: floor($interiorMargin * 1.5);
|
||||
@include ellipsize();
|
||||
line-height: 120%; // Needed for icon alignment
|
||||
max-width: 0;
|
||||
padding-top: $p;
|
||||
padding-bottom: $p;
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
&--selectable {
|
||||
body.desktop & {
|
||||
tbody tr {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: $colorListItemBgHov;
|
||||
filter: $filterHov;
|
||||
transition: $transIn;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--sticky-header {
|
||||
thead tr {
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
|
||||
@@ -79,6 +79,11 @@
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
textarea {
|
||||
// When a textarea is in the Inspector, only allow vertical resize
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/************************************************************** LEGACY */
|
||||
.l-inspector-part {
|
||||
display: contents;
|
||||
@@ -151,7 +156,8 @@
|
||||
padding: 3px $interiorMarginLg 3px 0;
|
||||
}
|
||||
|
||||
&__label {
|
||||
&__label,
|
||||
&__hint {
|
||||
color: $colorInspectorPropName;
|
||||
|
||||
&[title]:not([title=""]) {
|
||||
@@ -167,6 +173,14 @@
|
||||
grid-column: 1 / 3;
|
||||
}
|
||||
}
|
||||
|
||||
.is-editing & {
|
||||
.c-inspect-properties {
|
||||
&__value, &__label {
|
||||
line-height: 160%; // Prevent buttons/selects from overlapping when wrapping
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/********************************************* INSPECTOR PROPERTIES TAB */
|
||||
|
||||
@@ -22,10 +22,10 @@
|
||||
<template>
|
||||
<div class="l-preview-window js-preview-window">
|
||||
<PreviewHeader
|
||||
:current-view="currentView"
|
||||
:current-view="currentViewProvider"
|
||||
:action-collection="actionCollection"
|
||||
:domain-object="domainObject"
|
||||
:views="views"
|
||||
:views="viewProviders"
|
||||
/>
|
||||
<div class="l-preview-window__object-view js-notebook-snapshot-item">
|
||||
<div ref="objectView"></div>
|
||||
@@ -52,6 +52,12 @@ export default {
|
||||
default() {
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
existingView: {
|
||||
type: Object,
|
||||
default() {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -60,21 +66,25 @@ export default {
|
||||
return {
|
||||
domainObject: domainObject,
|
||||
viewKey: undefined,
|
||||
views: [],
|
||||
currentView: {},
|
||||
actionCollection: undefined
|
||||
viewProviders: [],
|
||||
currentViewProvider: {},
|
||||
actionCollection: undefined,
|
||||
existingViewIndex: 0
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.views = this.openmct.objectViews.get(this.domainObject, this.objectPath).map((view) => {
|
||||
view.onItemClicked = () => {
|
||||
return this.setView(view);
|
||||
};
|
||||
this.viewProviders = this.openmct.objectViews.get(this.domainObject, this.objectPath);
|
||||
this.viewProviders.forEach((provider, index) => {
|
||||
provider.onItemClicked = () => {
|
||||
if (this.existingView && provider.key === this.existingView.key) {
|
||||
this.existingViewIndex = index;
|
||||
}
|
||||
|
||||
return view;
|
||||
this.setView(provider);
|
||||
};
|
||||
});
|
||||
|
||||
this.setView(this.views[0]);
|
||||
this.setView(this.viewProviders[0]);
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.stopListeningStyles) {
|
||||
@@ -91,41 +101,74 @@ export default {
|
||||
}
|
||||
},
|
||||
destroyed() {
|
||||
this.view.destroy();
|
||||
if (!this.existingView) {
|
||||
this.view.destroy();
|
||||
} else if (this.existingViewElement) {
|
||||
// if the existing view element is populated, it's the currently viewed view
|
||||
// in preview and we need to add it back to the parent.
|
||||
this.addExistingViewBackToParent();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
clear() {
|
||||
if (this.view) {
|
||||
this.view.destroy();
|
||||
this.$refs.objectView.innerHTML = '';
|
||||
}
|
||||
if (this.view !== this.existingView) {
|
||||
this.view.destroy();
|
||||
} else {
|
||||
this.addExistingViewBackToParent();
|
||||
}
|
||||
|
||||
delete this.view;
|
||||
delete this.viewContainer;
|
||||
this.$refs.objectView.innerHTML = '';
|
||||
delete this.view;
|
||||
delete this.viewContainer;
|
||||
}
|
||||
},
|
||||
setView(view) {
|
||||
if (this.viewKey === view.key) {
|
||||
setView(viewProvider) {
|
||||
if (this.viewKey === viewProvider.key) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isExistingView = viewProvider.key === this.existingView.key;
|
||||
|
||||
this.clear();
|
||||
this.viewKey = view.key;
|
||||
this.currentView = view;
|
||||
|
||||
this.viewKey = viewProvider.key;
|
||||
this.initializeViewContainer();
|
||||
|
||||
if (isExistingView) {
|
||||
this.view = this.existingView;
|
||||
this.existingViewElement = this.existingView.parentElement.firstChild;
|
||||
this.currentViewProvider = this.viewProviders[this.existingViewIndex];
|
||||
} else {
|
||||
this.currentViewProvider = viewProvider;
|
||||
this.view = this.currentViewProvider.view(this.domainObject, this.objectPath);
|
||||
}
|
||||
|
||||
this.getActionsCollection(this.view);
|
||||
|
||||
if (isExistingView) {
|
||||
this.viewContainer.appendChild(this.existingViewElement);
|
||||
} else {
|
||||
this.view.show(this.viewContainer, false, this.viewOptions);
|
||||
}
|
||||
|
||||
this.initObjectStyles();
|
||||
},
|
||||
addExistingViewBackToParent() {
|
||||
this.existingView.parentElement.appendChild(this.existingViewElement);
|
||||
delete this.existingViewElement;
|
||||
},
|
||||
initializeViewContainer() {
|
||||
this.viewContainer = document.createElement('div');
|
||||
this.viewContainer.classList.add('l-angular-ov-wrapper');
|
||||
this.$refs.objectView.append(this.viewContainer);
|
||||
this.view = this.currentView.view(this.domainObject, this.objectPath);
|
||||
|
||||
this.getActionsCollection();
|
||||
this.view.show(this.viewContainer, false, this.viewOptions);
|
||||
this.initObjectStyles();
|
||||
},
|
||||
getActionsCollection() {
|
||||
getActionsCollection(view) {
|
||||
if (this.actionCollection) {
|
||||
this.actionCollection.destroy();
|
||||
}
|
||||
|
||||
this.actionCollection = this.openmct.actions.getActionsCollection(this.objectPath, this.view);
|
||||
this.actionCollection = this.openmct.actions.getActionsCollection(this.objectPath, view);
|
||||
},
|
||||
initObjectStyles() {
|
||||
if (!this.styleRuleManager) {
|
||||
|
||||
@@ -90,6 +90,7 @@ define(['EventEmitter'], function (EventEmitter) {
|
||||
provider.view = (domainObject, objectPath) => {
|
||||
const viewObject = wrappedView(domainObject, objectPath);
|
||||
const wrappedShow = viewObject.show.bind(viewObject);
|
||||
viewObject.key = key; // provide access to provider key on view object
|
||||
viewObject.show = (element, isEditing, viewOptions) => {
|
||||
viewObject.parentElement = element.parentElement;
|
||||
wrappedShow(element, isEditing, viewOptions);
|
||||
|
||||
@@ -7,7 +7,14 @@
|
||||
"allowJs": true,
|
||||
"checkJs": false,
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"noImplicitOverride": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node"
|
||||
"moduleResolution": "node",
|
||||
|
||||
"paths": {
|
||||
// matches the alias in webpack config, so that types for those imports are visible.
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ const gitBranch = require('child_process')
|
||||
const config = {
|
||||
entry: {
|
||||
openmct: './openmct.js',
|
||||
generatorWorker: './example/generator/generatorWorker.js',
|
||||
couchDBChangesFeed: './src/plugins/persistence/couch/CouchChangesFeed.js',
|
||||
inMemorySearchWorker: './src/api/objects/InMemorySearchWorker.js',
|
||||
espressoTheme: './src/plugins/themes/espresso-theme.scss',
|
||||
@@ -98,7 +99,7 @@ const config = {
|
||||
},
|
||||
{
|
||||
test: /\.html$/,
|
||||
use: 'html-loader'
|
||||
type: 'asset/source'
|
||||
},
|
||||
{
|
||||
test: /zepto/,
|
||||
@@ -108,25 +109,24 @@ const config = {
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.(jpg|jpeg|png|svg|ico|woff|woff2?|eot|ttf)$/,
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: '[name].[ext]',
|
||||
outputPath(url, resourcePath, context) {
|
||||
if (/\.(jpg|jpeg|png|svg)$/.test(url)) {
|
||||
return `images/${url}`;
|
||||
}
|
||||
|
||||
if (/\.ico$/.test(url)) {
|
||||
return `icons/${url}`;
|
||||
}
|
||||
|
||||
if (/\.(woff|woff2?|eot|ttf)$/.test(url)) {
|
||||
return `fonts/${url}`;
|
||||
} else {
|
||||
return `${url}`;
|
||||
}
|
||||
}
|
||||
test: /\.(jpg|jpeg|png|svg)$/,
|
||||
type: 'asset/resource',
|
||||
generator: {
|
||||
filename: 'images/[name][ext]'
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.ico$/,
|
||||
type: 'asset/resource',
|
||||
generator: {
|
||||
filename: 'icons/[name][ext]'
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.(woff|woff2?|eot|ttf)$/,
|
||||
type: 'asset/resource',
|
||||
generator: {
|
||||
filename: 'fonts/[name][ext]'
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -134,4 +134,5 @@ const config = {
|
||||
stats: 'errors-warnings'
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
module.exports = config;
|
||||
|
||||
Reference in New Issue
Block a user