mirror of
https://github.com/nasa/openmct.git
synced 2025-06-25 18:50:11 +00:00
Compare commits
40 Commits
fix-github
...
condition-
Author | SHA1 | Date | |
---|---|---|---|
bfa604efa0 | |||
e75befafbd | |||
d7d06b59ea | |||
8b4a55a7ec | |||
2519e601d7 | |||
b7b205621b | |||
3d2d932323 | |||
ce28dd2b9f | |||
286a533dad | |||
378a4ca282 | |||
fff3ce0acf | |||
db5cb2517f | |||
5236f1c796 | |||
1ed253cb07 | |||
a6553ba010 | |||
cf6bc5be2d | |||
a53a3a0297 | |||
402cd15726 | |||
a5580912e3 | |||
54d1b8991c | |||
7b6acee793 | |||
04e1c60e5c | |||
91bcd78d40 | |||
a3c0e073c8 | |||
21ae9f45c1 | |||
0a40c8dd0b | |||
ef1ea8e712 | |||
5c4fad77ff | |||
dbac9e6cd2 | |||
4b7bcf9c89 | |||
2b42abd495 | |||
1f2102b845 | |||
2ccb90aa41 | |||
525496fbca | |||
47099786cb | |||
3a11291a3b | |||
476f1b2579 | |||
6153ad8e1e | |||
77c0b16050 | |||
d19088cec6 |
@ -149,11 +149,15 @@ workflows:
|
||||
node-version: lts/fermium
|
||||
browser: ChromeHeadless
|
||||
post-steps:
|
||||
- upload_code_covio
|
||||
- upload_code_covio
|
||||
- unit-test:
|
||||
name: node16-chrome
|
||||
node-version: lts/gallium
|
||||
browser: ChromeHeadless
|
||||
browser: ChromeHeadless
|
||||
- unit-test:
|
||||
name: node18-chrome
|
||||
node-version: "18"
|
||||
browser: ChromeHeadless
|
||||
- e2e-test:
|
||||
name: e2e-ci
|
||||
node-version: lts/gallium
|
||||
@ -176,6 +180,10 @@ workflows:
|
||||
name: node16-chrome-nightly
|
||||
node-version: lts/gallium
|
||||
browser: ChromeHeadless
|
||||
- unit-test:
|
||||
name: node18-chrome
|
||||
node-version: "18"
|
||||
browser: ChromeHeadless
|
||||
- npm-audit:
|
||||
node-version: lts/gallium
|
||||
- e2e-test:
|
||||
|
16
.github/workflows/e2e-pr.yml
vendored
16
.github/workflows/e2e-pr.yml
vendored
@ -8,7 +8,7 @@ on:
|
||||
|
||||
jobs:
|
||||
e2e-full:
|
||||
if: ${{ github.event_name == 'pull_request' && github.event.action == 'labeled' && github.event.label.name == 'pr:e2e' }} || ${{ github.event_name == 'pull_request' && github.event.action == 'opened' }}
|
||||
if: ${{ github.event.label.name == 'pr:e2e' }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
@ -30,21 +30,11 @@ jobs:
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
check-latest: true
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v3
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
|
||||
- run: npx playwright@1.19.2 install
|
||||
- run: npm install
|
||||
- run: npx playwright install
|
||||
- run: npm run test:e2e:full
|
||||
- name: Archive test results
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: test-results
|
||||
- name: Test success
|
||||
|
14
.github/workflows/e2e-visual.yml
vendored
14
.github/workflows/e2e-visual.yml
vendored
@ -10,25 +10,15 @@ on:
|
||||
|
||||
jobs:
|
||||
e2e-visual:
|
||||
if: ${{ github.event.label.name == 'pr:visual' }} || ${{ github.event.workflow_dispatch }} || ${{ github.event.schedule }} || ${{ github.event_name == 'pull_request' && github.event.action == 'opened' }}
|
||||
if: ${{ github.event.label.name == 'pr:visual' }} || ${{ github.event.workflow_dispatch }} || ${{ github.event.schedule }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
check-latest: true
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v3
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
|
||||
- run: npx playwright@1.19.2 install
|
||||
- run: npm install
|
||||
- run: npx playwright install
|
||||
- name: Run the e2e visual tests
|
||||
run: npm run test:e2e:visual
|
||||
env:
|
||||
|
21
.github/workflows/e2e.yml
vendored
Normal file
21
.github/workflows/e2e.yml
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
name: "e2e"
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Which branch do you want to test?' # Limited to branch for now
|
||||
required: false
|
||||
default: 'master'
|
||||
jobs:
|
||||
e2e:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.inputs.version }}
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
- run: npm install
|
||||
- name: Run the e2e tests
|
||||
run: npm run test:e2e:ci
|
5
.github/workflows/lighthouse.yml
vendored
5
.github/workflows/lighthouse.yml
vendored
@ -6,6 +6,9 @@ on:
|
||||
description: 'Which branch do you want to test?' # Limited to branch for now
|
||||
required: false
|
||||
default: 'master'
|
||||
pull_request:
|
||||
types:
|
||||
- labeled
|
||||
jobs:
|
||||
lighthouse-pr:
|
||||
if: ${{ github.event.label.name == 'pr:lighthouse' }}
|
||||
@ -53,7 +56,6 @@ jobs:
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
check-latest: true
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v2
|
||||
env:
|
||||
@ -80,7 +82,6 @@ jobs:
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
check-latest: true
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v3
|
||||
env:
|
||||
|
1
.github/workflows/npm-prerelease.yml
vendored
1
.github/workflows/npm-prerelease.yml
vendored
@ -27,7 +27,6 @@ jobs:
|
||||
with:
|
||||
node-version: 16
|
||||
registry-url: https://registry.npmjs.org/
|
||||
check-latest: true
|
||||
- run: npm install
|
||||
- run: npm publish --access public --tag unstable
|
||||
env:
|
||||
|
15
.github/workflows/pr-platform.yml
vendored
15
.github/workflows/pr-platform.yml
vendored
@ -12,14 +12,13 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu-20.04 #MMOC Ubuntu version
|
||||
- ubuntu-latest
|
||||
- macos-latest
|
||||
- macos-10.15
|
||||
- windows-latest
|
||||
node_version:
|
||||
- 14
|
||||
- 16
|
||||
- 17
|
||||
- 18
|
||||
architecture:
|
||||
- x64
|
||||
name: Node ${{ matrix.node_version }} - ${{ matrix.architecture }} on ${{ matrix.os }}
|
||||
@ -30,16 +29,6 @@ jobs:
|
||||
with:
|
||||
node-version: ${{ matrix.node_version }}
|
||||
architecture: ${{ matrix.architecture }}
|
||||
check-latest: true
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v3
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-${{ matrix.node_version }}--build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ matrix.node_version }}--build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
|
||||
- run: npm install
|
||||
- run: npm test
|
||||
- run: npm run lint -- --quiet
|
||||
|
190
e2e/tests/plugins/plot/autoscale.e2e.spec.js
Normal file
190
e2e/tests/plugins/plot/autoscale.e2e.spec.js
Normal file
@ -0,0 +1,190 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
Test for plot autoscale.
|
||||
*/
|
||||
|
||||
const { test: _test, expect } = require('@playwright/test');
|
||||
|
||||
// create a new `test` API that will not append platform details to snapshot
|
||||
// file names, only for the tests in this file, so that the same snapshots will
|
||||
// be used for all platforms.
|
||||
const test = _test.extend({
|
||||
_autoSnapshotSuffix: [
|
||||
async ({}, use, testInfo) => {
|
||||
testInfo.snapshotSuffix = '';
|
||||
await use();
|
||||
},
|
||||
{ auto: true }
|
||||
]
|
||||
});
|
||||
|
||||
test.use({
|
||||
viewport: {
|
||||
width: 1280,
|
||||
height: 720
|
||||
}
|
||||
});
|
||||
|
||||
test.describe('ExportAsJSON', () => {
|
||||
test('autoscale off causes no error from undefined user range', async ({ page }) => {
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
await setTimeRange(page);
|
||||
|
||||
await createSinewaveOverlayPlot(page);
|
||||
|
||||
await testYTicks(page, ['-1.00', '-0.50', '0.00', '0.50', '1.00']);
|
||||
|
||||
await turnOffAutoscale(page);
|
||||
|
||||
const canvas = page.locator('canvas').nth(1);
|
||||
|
||||
// Make sure that after turning off autoscale, the user selected range values start at the same values the plot had prior.
|
||||
await Promise.all([
|
||||
testYTicks(page, ['-1.00', '-0.50', '0.00', '0.50', '1.00']),
|
||||
new Promise(r => setTimeout(r, 100))
|
||||
.then(() => canvas.screenshot())
|
||||
.then(shot => expect(shot).toMatchSnapshot('autoscale-canvas-prepan.png', { maxDiffPixels: 40 }))
|
||||
]);
|
||||
|
||||
let errorCount = 0;
|
||||
|
||||
function onError() {
|
||||
errorCount++;
|
||||
}
|
||||
|
||||
page.on('pageerror', onError);
|
||||
|
||||
await page.keyboard.down('Alt');
|
||||
|
||||
await canvas.dragTo(canvas, {
|
||||
sourcePosition: {
|
||||
x: 200,
|
||||
y: 200
|
||||
},
|
||||
targetPosition: {
|
||||
x: 400,
|
||||
y: 400
|
||||
}
|
||||
});
|
||||
|
||||
await page.keyboard.up('Alt');
|
||||
|
||||
page.off('pageerror', onError);
|
||||
|
||||
// There would have been an error at this point. So if there isn't, then
|
||||
// we fixed it.
|
||||
expect(errorCount).toBe(0);
|
||||
|
||||
// Ensure the drag worked.
|
||||
await Promise.all([
|
||||
testYTicks(page, ['0.00', '0.50', '1.00', '1.50', '2.00']),
|
||||
new Promise(r => setTimeout(r, 100))
|
||||
.then(() => canvas.screenshot())
|
||||
.then(shot => expect(shot).toMatchSnapshot('autoscale-canvas-panned.png', { maxDiffPixels: 40 }))
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} start
|
||||
* @param {string} end
|
||||
*/
|
||||
async function setTimeRange(page, start = '2022-03-29 22:00:00.000Z', end = '2022-03-29 22:00:30.000Z') {
|
||||
// Set a specific time range for consistency, otherwise it will change
|
||||
// on every test to a range based on the current time.
|
||||
|
||||
const timeInputs = page.locator('input.c-input--datetime');
|
||||
await timeInputs.first().click();
|
||||
await timeInputs.first().fill(start);
|
||||
|
||||
await timeInputs.nth(1).click();
|
||||
await timeInputs.nth(1).fill(end);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function createSinewaveOverlayPlot(page) {
|
||||
// click create button
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
|
||||
// add overlay plot with defaults
|
||||
await page.locator('li:has-text("Overlay Plot")').click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/a9268c6f-45cc-4bcd-a6a0-50ac4036e396?tc.mode=fixed&tc.startBound=1649305424163&tc.endBound=1649307224163&tc.timeSystem=utc&view=plot-overlay' }*/),
|
||||
page.locator('text=OK').click()
|
||||
]);
|
||||
|
||||
// save (exit edit mode)
|
||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
// click create button
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
|
||||
// add sine wave generator with defaults
|
||||
await page.locator('li:has-text("Sine Wave Generator")').click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/a9268c6f-45cc-4bcd-a6a0-50ac4036e396/5cfa5c69-17bc-4a99-9545-4da8125380c5?tc.mode=fixed&tc.startBound=1649305424163&tc.endBound=1649307224163&tc.timeSystem=utc&view=plot-single' }*/),
|
||||
page.locator('text=OK').click()
|
||||
]);
|
||||
|
||||
// focus the overlay plot
|
||||
await page.locator('text=Open MCT My Items >> span').nth(3).click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/a9268c6f-45cc-4bcd-a6a0-50ac4036e396?tc.mode=fixed&tc.startBound=1649305424163&tc.endBound=1649307224163&tc.timeSystem=utc&view=plot-overlay' }*/),
|
||||
page.locator('text=Unnamed Overlay Plot').first().click()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function turnOffAutoscale(page) {
|
||||
// enter edit mode
|
||||
await page.locator('text=Unnamed Overlay Plot Snapshot >> button').nth(3).click();
|
||||
|
||||
// uncheck autoscale
|
||||
await page.locator('text=Y Axis Scaling Auto scale Padding >> input[type="checkbox"]').uncheck();
|
||||
|
||||
// save
|
||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function testYTicks(page, values) {
|
||||
const yTicks = page.locator('.gl-plot-y-tick-label');
|
||||
let promises = [yTicks.count().then(c => expect(c).toBe(values.length))];
|
||||
|
||||
for (let i = 0, l = values.length; i < l; i += 1) {
|
||||
promises.push(expect(yTicks.nth(i)).toHaveText(values[i])); // eslint-disable-line
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
279
e2e/tests/plugins/plot/log-plot.e2e.spec.js
Normal file
279
e2e/tests/plugins/plot/log-plot.e2e.spec.js
Normal file
@ -0,0 +1,279 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
Tests to verify log plot functionality.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('@playwright/test');
|
||||
|
||||
test.describe('Log plot tests', () => {
|
||||
test.only('Can create a log plot.', async ({ page }) => {
|
||||
await makeOverlayPlot(page);
|
||||
await testRegularTicks(page);
|
||||
await enableEditMode(page);
|
||||
await enableLogMode(page);
|
||||
await testLogTicks(page);
|
||||
await disableLogMode(page);
|
||||
await testRegularTicks(page);
|
||||
await enableLogMode(page);
|
||||
await testLogTicks(page);
|
||||
await saveOverlayPlot(page);
|
||||
await testLogTicks(page);
|
||||
await testLogPlotPixels(page);
|
||||
|
||||
// refresh page
|
||||
await page.reload();
|
||||
|
||||
// test log ticks hold up after refresh
|
||||
await testLogTicks(page);
|
||||
await testLogPlotPixels(page);
|
||||
});
|
||||
|
||||
test.only('Verify that log mode option is reflected in import/export JSON', async ({ page }) => {
|
||||
await makeOverlayPlot(page);
|
||||
await enableEditMode(page);
|
||||
await enableLogMode(page);
|
||||
await saveOverlayPlot(page);
|
||||
|
||||
// TODO ...export, delete the overlay, then import it...
|
||||
|
||||
await testLogTicks(page);
|
||||
|
||||
// TODO, the plot is slightly at different position that in the other test, so this fails.
|
||||
// ...We can fix it by copying all steps from the first test...
|
||||
// await testLogPlotPixels(page);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Makes an overlay plot with a sine wave generator and clicks on the overlay plot in the sidebar so it is the active thing displayed.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function makeOverlayPlot(page) {
|
||||
// fresh page with time range from 2022-03-29 22:00:00.000Z to 2022-03-29 22:00:30.000Z
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
// Set a specific time range for consistency, otherwise it will change
|
||||
// on every test to a range based on the current time.
|
||||
|
||||
const timeInputs = page.locator('input.c-input--datetime');
|
||||
await timeInputs.first().click();
|
||||
await timeInputs.first().fill('2022-03-29 22:00:00.000Z');
|
||||
|
||||
await timeInputs.nth(1).click();
|
||||
await timeInputs.nth(1).fill('2022-03-29 22:00:30.000Z');
|
||||
|
||||
// create overlay plot
|
||||
|
||||
await page.locator('button.c-create-button').click();
|
||||
await page.locator('li:has-text("Overlay Plot")').click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/8caf7072-535b-4af6-8394-edd86e3ea35f?tc.mode=fixed&tc.startBound=1648590633191&tc.endBound=1648592433191&tc.timeSystem=utc&view=plot-overlay' }*/),
|
||||
page.locator('text=OK').click()
|
||||
]);
|
||||
|
||||
// save the overlay plot
|
||||
|
||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
// create a sinewave generator
|
||||
|
||||
await page.locator('button.c-create-button').click();
|
||||
await page.locator('li:has-text("Sine Wave Generator")').click();
|
||||
|
||||
// set amplitude to 6, offset 4, period 2
|
||||
|
||||
await page.locator('div:nth-child(5) .form-row .c-form-row__controls .form-control .field input').click();
|
||||
await page.locator('div:nth-child(5) .form-row .c-form-row__controls .form-control .field input').fill('6');
|
||||
|
||||
await page.locator('div:nth-child(6) .form-row .c-form-row__controls .form-control .field input').click();
|
||||
await page.locator('div:nth-child(6) .form-row .c-form-row__controls .form-control .field input').fill('4');
|
||||
|
||||
await page.locator('div:nth-child(7) .form-row .c-form-row__controls .form-control .field input').click();
|
||||
await page.locator('div:nth-child(7) .form-row .c-form-row__controls .form-control .field input').fill('2');
|
||||
|
||||
// Click OK to make generator
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/8caf7072-535b-4af6-8394-edd86e3ea35f/6e58b26a-8a73-4df6-b3a6-918decc0bbfa?tc.mode=fixed&tc.startBound=1648590633191&tc.endBound=1648592433191&tc.timeSystem=utc&view=plot-single' }*/),
|
||||
page.locator('text=OK').click()
|
||||
]);
|
||||
|
||||
// click on overlay plot
|
||||
|
||||
await page.locator('text=Open MCT My Items >> span').nth(3).click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/8caf7072-535b-4af6-8394-edd86e3ea35f?tc.mode=fixed&tc.startBound=1648590633191&tc.endBound=1648592433191&tc.timeSystem=utc&view=plot-overlay' }*/),
|
||||
page.locator('text=Unnamed Overlay Plot').first().click()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function testRegularTicks(page) {
|
||||
const yTicks = page.locator('.gl-plot-y-tick-label');
|
||||
expect(await yTicks.count()).toBe(7);
|
||||
await expect(yTicks.nth(0)).toHaveText('-2');
|
||||
await expect(yTicks.nth(1)).toHaveText('0');
|
||||
await expect(yTicks.nth(2)).toHaveText('2');
|
||||
await expect(yTicks.nth(3)).toHaveText('4');
|
||||
await expect(yTicks.nth(4)).toHaveText('6');
|
||||
await expect(yTicks.nth(5)).toHaveText('8');
|
||||
await expect(yTicks.nth(6)).toHaveText('10');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function testLogTicks(page) {
|
||||
const yTicks = page.locator('.gl-plot-y-tick-label');
|
||||
expect(await yTicks.count()).toBe(28);
|
||||
await expect(yTicks.nth(0)).toHaveText('-2.98');
|
||||
await expect(yTicks.nth(1)).toHaveText('-2.50');
|
||||
await expect(yTicks.nth(2)).toHaveText('-2.00');
|
||||
await expect(yTicks.nth(3)).toHaveText('-1.51');
|
||||
await expect(yTicks.nth(4)).toHaveText('-1.20');
|
||||
await expect(yTicks.nth(5)).toHaveText('-1.00');
|
||||
await expect(yTicks.nth(6)).toHaveText('-0.80');
|
||||
await expect(yTicks.nth(7)).toHaveText('-0.58');
|
||||
await expect(yTicks.nth(8)).toHaveText('-0.40');
|
||||
await expect(yTicks.nth(9)).toHaveText('-0.20');
|
||||
await expect(yTicks.nth(10)).toHaveText('-0.00');
|
||||
await expect(yTicks.nth(11)).toHaveText('0.20');
|
||||
await expect(yTicks.nth(12)).toHaveText('0.40');
|
||||
await expect(yTicks.nth(13)).toHaveText('0.58');
|
||||
await expect(yTicks.nth(14)).toHaveText('0.80');
|
||||
await expect(yTicks.nth(15)).toHaveText('1.00');
|
||||
await expect(yTicks.nth(16)).toHaveText('1.20');
|
||||
await expect(yTicks.nth(17)).toHaveText('1.51');
|
||||
await expect(yTicks.nth(18)).toHaveText('2.00');
|
||||
await expect(yTicks.nth(19)).toHaveText('2.50');
|
||||
await expect(yTicks.nth(20)).toHaveText('2.98');
|
||||
await expect(yTicks.nth(21)).toHaveText('3.50');
|
||||
await expect(yTicks.nth(22)).toHaveText('4.00');
|
||||
await expect(yTicks.nth(23)).toHaveText('4.50');
|
||||
await expect(yTicks.nth(24)).toHaveText('5.31');
|
||||
await expect(yTicks.nth(25)).toHaveText('7.00');
|
||||
await expect(yTicks.nth(26)).toHaveText('8.00');
|
||||
await expect(yTicks.nth(27)).toHaveText('9.00');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function enableEditMode(page) {
|
||||
// turn on edit mode
|
||||
await page.locator('text=Unnamed Overlay Plot Snapshot >> button').nth(3).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function enableLogMode(page) {
|
||||
// turn on log mode
|
||||
await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"]').first().check();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function disableLogMode(page) {
|
||||
// turn off log mode
|
||||
await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"]').first().uncheck();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function saveOverlayPlot(page) {
|
||||
// save overlay plot
|
||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function testLogPlotPixels(page) {
|
||||
const pixelsMatch = await page.evaluate(async () => {
|
||||
// TODO get canvas pixels at a few locations to make sure they're the correct color, to test that the plot comes out as expected.
|
||||
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
|
||||
// These are some pixels that should be blue points in the log plot.
|
||||
// If the plot changes shape to an unexpected shape, this will
|
||||
// likely fail, which is what we want.
|
||||
//
|
||||
// I found these pixels by pausing playwright in debug mode at this
|
||||
// point, and using similar code as below to output the pixel data, then
|
||||
// I logged those pixels here.
|
||||
const expectedBluePixels = [
|
||||
// TODO these pixel sets only work with the first test, but not the second test.
|
||||
|
||||
// [60, 35],
|
||||
// [121, 125],
|
||||
// [156, 377],
|
||||
// [264, 73],
|
||||
// [372, 186],
|
||||
// [576, 73],
|
||||
// [659, 439],
|
||||
// [675, 423]
|
||||
|
||||
[60, 35],
|
||||
[120, 125],
|
||||
[156, 375],
|
||||
[264, 73],
|
||||
[372, 185],
|
||||
[575, 72],
|
||||
[659, 437],
|
||||
[675, 421]
|
||||
];
|
||||
|
||||
// The first canvas in the DOM is the one that has the plot point
|
||||
// icons (canvas 2d), which is the one we are testing. The second
|
||||
// one in the DOM is the WebGL canvas with the line. (Why aren't
|
||||
// they both WebGL?)
|
||||
const canvas = document.querySelector('canvas');
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
for (const pixel of expectedBluePixels) {
|
||||
// XXX Possible optimization: call getImageData only once with
|
||||
// area including all pixels to be tested.
|
||||
const data = ctx.getImageData(pixel[0], pixel[1], 1, 1).data;
|
||||
|
||||
// #43b0ffff <-- openmct cyanish-blue with 100% opacity
|
||||
// if (data[0] !== 0x43 || data[1] !== 0xb0 || data[2] !== 0xff || data[3] !== 0xff) {
|
||||
if (data[0] === 0 && data[1] === 0 && data[2] === 0 && data[3] === 0) {
|
||||
// If any pixel is empty, it means we didn't hit a plot point.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
expect(pixelsMatch).toBe(true);
|
||||
}
|
@ -77,7 +77,7 @@
|
||||
|
||||
|
||||
openmct.install(openmct.plugins.LocalStorage());
|
||||
|
||||
|
||||
openmct.install(openmct.plugins.example.Generator());
|
||||
openmct.install(openmct.plugins.example.EventGeneratorPlugin());
|
||||
openmct.install(openmct.plugins.example.ExampleImagery());
|
||||
@ -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>
|
||||
|
23
package.json
23
package.json
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "openmct",
|
||||
"version": "2.0.2-SNAPSHOT",
|
||||
"version": "2.0.3",
|
||||
"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",
|
||||
@ -18,13 +18,12 @@
|
||||
"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",
|
||||
"d3-scale": "1.0.x",
|
||||
"d3-selection": "1.3.x",
|
||||
"eslint": "8.11.0",
|
||||
"eslint": "8.13.0",
|
||||
"eslint-plugin-compat": "4.0.2",
|
||||
"eslint-plugin-playwright": "0.8.0",
|
||||
"eslint-plugin-vue": "8.5.0",
|
||||
@ -38,7 +37,7 @@
|
||||
"imports-loader": "0.8.0",
|
||||
"jasmine-core": "4.0.1",
|
||||
"jsdoc": "3.5.5",
|
||||
"karma": "6.3.15",
|
||||
"karma": "6.3.18",
|
||||
"karma-chrome-launcher": "3.1.1",
|
||||
"karma-cli": "2.0.0",
|
||||
"karma-coverage": "2.1.1",
|
||||
@ -47,14 +46,14 @@
|
||||
"karma-jasmine": "4.0.1",
|
||||
"karma-junit-reporter": "2.0.1",
|
||||
"karma-sourcemap-loader": "0.3.8",
|
||||
"karma-spec-reporter": "0.0.33",
|
||||
"karma-spec-reporter": "0.0.34",
|
||||
"karma-webpack": "5.0.0",
|
||||
"lighthouse": "9.5.0",
|
||||
"location-bar": "3.0.1",
|
||||
"lodash": "4.17.21",
|
||||
"mini-css-extract-plugin": "2.6.0",
|
||||
"moment": "2.29.1",
|
||||
"moment-duration-format": "2.2.2",
|
||||
"moment-duration-format": "2.3.2",
|
||||
"moment-timezone": "0.5.34",
|
||||
"node-bourbon": "4.2.3",
|
||||
"painterro": "1.2.56",
|
||||
@ -62,14 +61,14 @@
|
||||
"plotly.js-gl2d-dist": "2.5.0",
|
||||
"printj": "1.3.1",
|
||||
"request": "2.88.2",
|
||||
"resolve-url-loader": "4.0.0",
|
||||
"resolve-url-loader": "5.0.0",
|
||||
"sass": "1.49.9",
|
||||
"sass-loader": "12.6.0",
|
||||
"sinon": "13.0.1",
|
||||
"style-loader": "^1.0.1",
|
||||
"uuid": "3.3.3",
|
||||
"vue": "2.6.14",
|
||||
"vue-eslint-parser": "8.2.0",
|
||||
"vue-eslint-parser": "8.3.0",
|
||||
"vue-loader": "15.9.8",
|
||||
"vue-template-compiler": "2.6.14",
|
||||
"webpack": "5.68.0",
|
||||
@ -95,6 +94,7 @@
|
||||
"test:debug": "cross-env NODE_ENV=debug karma start --no-single-run",
|
||||
"test:e2e:ci": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome smoke default condition timeConductor",
|
||||
"test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome",
|
||||
"test:e2e:debug": "npm run test:e2e:local -- --debug",
|
||||
"test:e2e:visual": "percy exec --config ./e2e/.percy.yml -- npx playwright test --config=e2e/playwright-visual.config.js default",
|
||||
"test:e2e:full": "npx playwright test --config=e2e/playwright-ci.config.js",
|
||||
"test:watch": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --no-single-run",
|
||||
@ -112,6 +112,9 @@
|
||||
"engines": {
|
||||
"node": ">=14.19.1"
|
||||
},
|
||||
"overrides": {
|
||||
"core-js": "3.21.1"
|
||||
},
|
||||
"browserslist": [
|
||||
"Firefox ESR",
|
||||
"not IE 11",
|
||||
|
@ -232,6 +232,7 @@ define([
|
||||
this.actions = new api.ActionsAPI(this);
|
||||
|
||||
this.status = new api.StatusAPI(this);
|
||||
this.styleManager = new api.StyleManagerAPI(this);
|
||||
|
||||
this.priority = api.PriorityAPI;
|
||||
|
||||
@ -241,7 +242,6 @@ define([
|
||||
this.branding = BrandingAPI.default;
|
||||
|
||||
// Plugins that are installed by default
|
||||
|
||||
this.install(this.plugins.Plot());
|
||||
this.install(this.plugins.Chart());
|
||||
this.install(this.plugins.TelemetryTable.default());
|
||||
|
@ -31,6 +31,7 @@ define([
|
||||
'./objects/ObjectAPI',
|
||||
'./priority/PriorityAPI',
|
||||
'./status/StatusAPI',
|
||||
'./styles/StyleManagerAPI',
|
||||
'./telemetry/TelemetryAPI',
|
||||
'./time/TimeAPI',
|
||||
'./types/TypeRegistry',
|
||||
@ -46,6 +47,7 @@ define([
|
||||
ObjectAPI,
|
||||
PriorityAPI,
|
||||
StatusAPI,
|
||||
StyleManagerAPI,
|
||||
TelemetryAPI,
|
||||
TimeAPI,
|
||||
TypeRegistry,
|
||||
@ -62,6 +64,7 @@ define([
|
||||
ObjectAPI: ObjectAPI,
|
||||
PriorityAPI: PriorityAPI.default,
|
||||
StatusAPI: StatusAPI.default,
|
||||
StyleManagerAPI: StyleManagerAPI.default,
|
||||
TelemetryAPI: TelemetryAPI,
|
||||
TimeAPI: TimeAPI.default,
|
||||
TypeRegistry: TypeRegistry,
|
||||
|
@ -1,5 +1,6 @@
|
||||
import AutoCompleteField from './components/controls/AutoCompleteField.vue';
|
||||
import ClockDisplayFormatField from './components/controls/ClockDisplayFormatField.vue';
|
||||
import CheckBoxField from './components/controls/CheckBoxField.vue';
|
||||
import Datetime from './components/controls/Datetime.vue';
|
||||
import FileInput from './components/controls/FileInput.vue';
|
||||
import Locator from './components/controls/Locator.vue';
|
||||
@ -7,11 +8,13 @@ import NumberField from './components/controls/NumberField.vue';
|
||||
import SelectField from './components/controls/SelectField.vue';
|
||||
import TextAreaField from './components/controls/TextAreaField.vue';
|
||||
import TextField from './components/controls/TextField.vue';
|
||||
import ToggleSwitchField from './components/controls/ToggleSwitchField.vue';
|
||||
|
||||
import Vue from 'vue';
|
||||
|
||||
export const DEFAULT_CONTROLS_MAP = {
|
||||
'autocomplete': AutoCompleteField,
|
||||
'checkbox': CheckBoxField,
|
||||
'composite': ClockDisplayFormatField,
|
||||
'datetime': Datetime,
|
||||
'file-input': FileInput,
|
||||
@ -19,7 +22,8 @@ export const DEFAULT_CONTROLS_MAP = {
|
||||
'numberfield': NumberField,
|
||||
'select': SelectField,
|
||||
'textarea': TextAreaField,
|
||||
'textfield': TextField
|
||||
'textfield': TextField,
|
||||
'toggleSwitch': ToggleSwitchField
|
||||
};
|
||||
|
||||
export default class FormControl {
|
||||
@ -94,4 +98,3 @@ export default class FormControl {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -79,10 +79,12 @@ export default {
|
||||
rowClass() {
|
||||
let cssClass = this.cssClass;
|
||||
|
||||
if (this.row.required) {
|
||||
cssClass = `${cssClass} req`;
|
||||
if (!this.row.required) {
|
||||
return;
|
||||
}
|
||||
|
||||
cssClass = `${cssClass} req`;
|
||||
|
||||
if (this.visited && this.valid !== undefined) {
|
||||
if (this.valid === true) {
|
||||
cssClass = `${cssClass} valid`;
|
||||
|
55
src/api/forms/components/controls/CheckBoxField.vue
Normal file
55
src/api/forms/components/controls/CheckBoxField.vue
Normal file
@ -0,0 +1,55 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
<template>
|
||||
<span class="form-control shell">
|
||||
<span
|
||||
class="field control"
|
||||
:class="model.cssClass"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="isChecked"
|
||||
@input="toggleCheckBox"
|
||||
>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import toggleMixin from '../../toggle-check-box-mixin';
|
||||
|
||||
export default {
|
||||
mixins: [toggleMixin],
|
||||
props: {
|
||||
model: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isChecked: this.model.value
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
@ -58,7 +58,6 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
updateText() {
|
||||
console.log('updateText', this.field);
|
||||
const data = {
|
||||
model: this.model,
|
||||
value: this.field
|
||||
|
62
src/api/forms/components/controls/ToggleSwitchField.vue
Normal file
62
src/api/forms/components/controls/ToggleSwitchField.vue
Normal file
@ -0,0 +1,62 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
<template>
|
||||
<span class="form-control shell">
|
||||
<span
|
||||
class="field control"
|
||||
:class="model.cssClass"
|
||||
>
|
||||
<ToggleSwitch
|
||||
id="switchId"
|
||||
:checked="isChecked"
|
||||
@change="toggleCheckBox"
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import toggleMixin from '../../toggle-check-box-mixin';
|
||||
import ToggleSwitch from '@/ui/components/ToggleSwitch.vue';
|
||||
|
||||
import uuid from 'uuid';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ToggleSwitch
|
||||
},
|
||||
mixins: [toggleMixin],
|
||||
props: {
|
||||
model: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
switchId: `toggleSwitch-${uuid}`,
|
||||
isChecked: this.model.value
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
19
src/api/forms/toggle-check-box-mixin.js
Normal file
19
src/api/forms/toggle-check-box-mixin.js
Normal file
@ -0,0 +1,19 @@
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
isChecked: false
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
toggleCheckBox(event) {
|
||||
this.isChecked = !this.isChecked;
|
||||
|
||||
const data = {
|
||||
model: this.model,
|
||||
value: this.isChecked
|
||||
};
|
||||
|
||||
this.$emit('onChange', data);
|
||||
}
|
||||
}
|
||||
};
|
@ -36,13 +36,14 @@ class InMemorySearchProvider {
|
||||
*/
|
||||
this.MAX_CONCURRENT_REQUESTS = 100;
|
||||
/**
|
||||
* If max results is not specified in query, use this as default.
|
||||
*/
|
||||
* If max results is not specified in query, use this as default.
|
||||
*/
|
||||
this.DEFAULT_MAX_RESULTS = 100;
|
||||
|
||||
this.openmct = openmct;
|
||||
|
||||
this.indexedIds = {};
|
||||
this.indexedCompositions = {};
|
||||
this.idsToIndex = [];
|
||||
this.pendingIndex = {};
|
||||
this.pendingRequests = 0;
|
||||
@ -58,7 +59,6 @@ class InMemorySearchProvider {
|
||||
this.onWorkerMessageError = this.onWorkerMessageError.bind(this);
|
||||
this.onerror = this.onWorkerError.bind(this);
|
||||
this.startIndexing = this.startIndexing.bind(this);
|
||||
this.onMutationOfIndexedObject = this.onMutationOfIndexedObject.bind(this);
|
||||
|
||||
this.openmct.on('start', this.startIndexing);
|
||||
this.openmct.on('destroy', () => {
|
||||
@ -68,6 +68,9 @@ class InMemorySearchProvider {
|
||||
this.worker.port.onmessageerror = null;
|
||||
this.worker.port.close();
|
||||
}
|
||||
|
||||
this.destroyObservers(this.indexedIds);
|
||||
this.destroyObservers(this.indexedCompositions);
|
||||
});
|
||||
}
|
||||
|
||||
@ -137,7 +140,7 @@ class InMemorySearchProvider {
|
||||
};
|
||||
modelResults.hits = await Promise.all(event.data.results.map(async (hit) => {
|
||||
const identifier = this.openmct.objects.parseKeyString(hit.keyString);
|
||||
const domainObject = await this.openmct.objects.get(identifier.key);
|
||||
const domainObject = await this.openmct.objects.get(identifier);
|
||||
|
||||
return domainObject;
|
||||
}));
|
||||
@ -213,29 +216,52 @@ class InMemorySearchProvider {
|
||||
}
|
||||
}
|
||||
|
||||
onMutationOfIndexedObject(domainObject) {
|
||||
onNameMutation(domainObject, name) {
|
||||
const provider = this;
|
||||
provider.index(domainObject.identifier, domainObject);
|
||||
|
||||
domainObject.name = name;
|
||||
provider.index(domainObject);
|
||||
}
|
||||
|
||||
onCompositionMutation(domainObject, composition) {
|
||||
const provider = this;
|
||||
const indexedComposition = domainObject.composition;
|
||||
const identifiersToIndex = composition
|
||||
.filter(identifier => !indexedComposition
|
||||
.some(indexedIdentifier => this.openmct.objects
|
||||
.areIdsEqual([identifier, indexedIdentifier])));
|
||||
|
||||
identifiersToIndex.forEach(identifier => {
|
||||
this.openmct.objects.get(identifier).then(objectToIndex => provider.index(objectToIndex));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass an id and model to the worker to be indexed. If the model has
|
||||
* composition, schedule those ids for later indexing.
|
||||
* Pass a domainObject to the worker to be indexed.
|
||||
* If the object has composition, schedule those ids for later indexing.
|
||||
* Watch for object changes and re-index object and children if so
|
||||
*
|
||||
* @private
|
||||
* @param id a model id
|
||||
* @param model a model
|
||||
* @param domainObject a domainObject
|
||||
*/
|
||||
async index(id, domainObject) {
|
||||
async index(domainObject) {
|
||||
const provider = this;
|
||||
const keyString = this.openmct.objects.makeKeyString(id);
|
||||
const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
|
||||
|
||||
if (!this.indexedIds[keyString]) {
|
||||
this.openmct.objects.observe(domainObject, `*`, this.onMutationOfIndexedObject);
|
||||
this.indexedIds[keyString] = this.openmct.objects.observe(
|
||||
domainObject,
|
||||
'name',
|
||||
this.onNameMutation.bind(this, domainObject)
|
||||
);
|
||||
this.indexedCompositions[keyString] = this.openmct.objects.observe(
|
||||
domainObject,
|
||||
'composition',
|
||||
this.onCompositionMutation.bind(this, domainObject)
|
||||
);
|
||||
}
|
||||
|
||||
this.indexedIds[keyString] = true;
|
||||
|
||||
if ((id.key !== 'ROOT')) {
|
||||
if ((keyString !== 'ROOT')) {
|
||||
if (this.worker) {
|
||||
this.worker.port.postMessage({
|
||||
request: 'index',
|
||||
@ -247,15 +273,12 @@ class InMemorySearchProvider {
|
||||
}
|
||||
}
|
||||
|
||||
const composition = this.openmct.composition.registry.find(foundComposition => {
|
||||
return foundComposition.appliesTo(domainObject);
|
||||
});
|
||||
const composition = this.openmct.composition.get(domainObject);
|
||||
|
||||
if (composition) {
|
||||
const childIdentifiers = await composition.load(domainObject);
|
||||
childIdentifiers.forEach(function (childIdentifier) {
|
||||
provider.scheduleForIndexing(childIdentifier);
|
||||
});
|
||||
if (composition !== undefined) {
|
||||
const children = await composition.load();
|
||||
|
||||
children.forEach(child => provider.scheduleForIndexing(child.identifier));
|
||||
}
|
||||
}
|
||||
|
||||
@ -271,12 +294,12 @@ class InMemorySearchProvider {
|
||||
const provider = this;
|
||||
|
||||
this.pendingRequests += 1;
|
||||
const identifier = await this.openmct.objects.parseKeyString(keyString);
|
||||
const domainObject = await this.openmct.objects.get(identifier.key);
|
||||
const domainObject = await this.openmct.objects.get(keyString);
|
||||
delete provider.pendingIndex[keyString];
|
||||
|
||||
try {
|
||||
if (domainObject) {
|
||||
await provider.index(identifier, domainObject);
|
||||
await provider.index(domainObject);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to index domain object ' + keyString, error);
|
||||
@ -305,9 +328,9 @@ class InMemorySearchProvider {
|
||||
}
|
||||
|
||||
/**
|
||||
* A local version of the same SharedWorker function
|
||||
* if we don't have SharedWorkers available (e.g., iOS)
|
||||
*/
|
||||
* A local version of the same SharedWorker function
|
||||
* if we don't have SharedWorkers available (e.g., iOS)
|
||||
*/
|
||||
localIndexItem(keyString, model) {
|
||||
this.localIndexedItems[keyString] = {
|
||||
type: model.type,
|
||||
@ -347,6 +370,16 @@ class InMemorySearchProvider {
|
||||
};
|
||||
this.onWorkerMessage(eventToReturn);
|
||||
}
|
||||
|
||||
destroyObservers(observers) {
|
||||
Object.entries(observers).forEach(([keyString, unobserve]) => {
|
||||
if (typeof unobserve === 'function') {
|
||||
unobserve();
|
||||
}
|
||||
|
||||
delete observers[keyString];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default InMemorySearchProvider;
|
||||
|
@ -105,13 +105,18 @@ describe("The Object API Search Function", () => {
|
||||
|
||||
beforeEach((done) => {
|
||||
openmct = createOpenMct();
|
||||
const defaultObjectProvider = openmct.objects.getProvider({
|
||||
key: '',
|
||||
namespace: ''
|
||||
});
|
||||
openmct.objects.addProvider('foo', defaultObjectProvider);
|
||||
spyOn(openmct.objects.inMemorySearchProvider, "query").and.callThrough();
|
||||
spyOn(openmct.objects.inMemorySearchProvider, "localSearch").and.callThrough();
|
||||
|
||||
openmct.on('start', async () => {
|
||||
mockIdentifier1 = {
|
||||
key: 'some-object',
|
||||
namespace: 'some-namespace'
|
||||
namespace: 'foo'
|
||||
};
|
||||
mockDomainObject1 = {
|
||||
type: 'clock',
|
||||
@ -120,7 +125,7 @@ describe("The Object API Search Function", () => {
|
||||
};
|
||||
mockIdentifier2 = {
|
||||
key: 'some-other-object',
|
||||
namespace: 'some-namespace'
|
||||
namespace: 'foo'
|
||||
};
|
||||
mockDomainObject2 = {
|
||||
type: 'clock',
|
||||
@ -129,16 +134,16 @@ describe("The Object API Search Function", () => {
|
||||
};
|
||||
mockIdentifier3 = {
|
||||
key: 'yet-another-object',
|
||||
namespace: 'some-namespace'
|
||||
namespace: 'foo'
|
||||
};
|
||||
mockDomainObject3 = {
|
||||
type: 'clock',
|
||||
name: 'redBear',
|
||||
identifier: mockIdentifier3
|
||||
};
|
||||
await openmct.objects.inMemorySearchProvider.index(mockIdentifier1, mockDomainObject1);
|
||||
await openmct.objects.inMemorySearchProvider.index(mockIdentifier2, mockDomainObject2);
|
||||
await openmct.objects.inMemorySearchProvider.index(mockIdentifier3, mockDomainObject3);
|
||||
await openmct.objects.inMemorySearchProvider.index(mockDomainObject1);
|
||||
await openmct.objects.inMemorySearchProvider.index(mockDomainObject2);
|
||||
await openmct.objects.inMemorySearchProvider.index(mockDomainObject3);
|
||||
done();
|
||||
});
|
||||
openmct.startHeadless();
|
||||
@ -175,9 +180,9 @@ describe("The Object API Search Function", () => {
|
||||
beforeEach(async () => {
|
||||
openmct.objects.inMemorySearchProvider.worker = null;
|
||||
// reindex locally
|
||||
await openmct.objects.inMemorySearchProvider.index(mockIdentifier1, mockDomainObject1);
|
||||
await openmct.objects.inMemorySearchProvider.index(mockIdentifier2, mockDomainObject2);
|
||||
await openmct.objects.inMemorySearchProvider.index(mockIdentifier3, mockDomainObject3);
|
||||
await openmct.objects.inMemorySearchProvider.index(mockDomainObject1);
|
||||
await openmct.objects.inMemorySearchProvider.index(mockDomainObject2);
|
||||
await openmct.objects.inMemorySearchProvider.index(mockDomainObject3);
|
||||
});
|
||||
it("calls local search", () => {
|
||||
openmct.objects.search('foo');
|
||||
|
@ -22,12 +22,14 @@
|
||||
|
||||
export default class Transaction {
|
||||
constructor(objectAPI) {
|
||||
this.dirtyObjects = new Set();
|
||||
this.dirtyObjects = {};
|
||||
this.objectAPI = objectAPI;
|
||||
}
|
||||
|
||||
add(object) {
|
||||
this.dirtyObjects.add(object);
|
||||
const key = this.objectAPI.makeKeyString(object.identifier);
|
||||
|
||||
this.dirtyObjects[key] = object;
|
||||
}
|
||||
|
||||
cancel() {
|
||||
@ -37,7 +39,8 @@ export default class Transaction {
|
||||
commit() {
|
||||
const promiseArray = [];
|
||||
const save = this.objectAPI.save.bind(this.objectAPI);
|
||||
this.dirtyObjects.forEach(object => {
|
||||
|
||||
Object.values(this.dirtyObjects).forEach(object => {
|
||||
promiseArray.push(this.createDirtyObjectPromise(object, save));
|
||||
});
|
||||
|
||||
@ -48,7 +51,9 @@ export default class Transaction {
|
||||
return new Promise((resolve, reject) => {
|
||||
action(object)
|
||||
.then((success) => {
|
||||
this.dirtyObjects.delete(object);
|
||||
const key = this.objectAPI.makeKeyString(object.identifier);
|
||||
|
||||
delete this.dirtyObjects[key];
|
||||
resolve(success);
|
||||
})
|
||||
.catch(reject);
|
||||
@ -57,7 +62,8 @@ export default class Transaction {
|
||||
|
||||
getDirtyObject(identifier) {
|
||||
let dirtyObject;
|
||||
this.dirtyObjects.forEach(object => {
|
||||
|
||||
Object.values(this.dirtyObjects).forEach(object => {
|
||||
const areIdsEqual = this.objectAPI.areIdsEqual(object.identifier, identifier);
|
||||
if (areIdsEqual) {
|
||||
dirtyObject = object;
|
||||
@ -67,14 +73,11 @@ export default class Transaction {
|
||||
return dirtyObject;
|
||||
}
|
||||
|
||||
start() {
|
||||
this.dirtyObjects = new Set();
|
||||
}
|
||||
|
||||
_clear() {
|
||||
const promiseArray = [];
|
||||
const refresh = this.objectAPI.refresh.bind(this.objectAPI);
|
||||
this.dirtyObjects.forEach(object => {
|
||||
|
||||
Object.values(this.dirtyObjects).forEach(object => {
|
||||
promiseArray.push(this.createDirtyObjectPromise(object, refresh));
|
||||
});
|
||||
|
||||
|
@ -34,24 +34,24 @@ describe("Transaction Class", () => {
|
||||
});
|
||||
|
||||
it('has no dirty objects', () => {
|
||||
expect(transaction.dirtyObjects.size).toEqual(0);
|
||||
expect(Object.keys(transaction.dirtyObjects).length).toEqual(0);
|
||||
});
|
||||
|
||||
it('add(), adds object to dirtyObjects', () => {
|
||||
const mockDomainObjects = createMockDomainObjects();
|
||||
transaction.add(mockDomainObjects[0]);
|
||||
expect(transaction.dirtyObjects.size).toEqual(1);
|
||||
expect(Object.keys(transaction.dirtyObjects).length).toEqual(1);
|
||||
});
|
||||
|
||||
it('cancel(), clears all dirtyObjects', (done) => {
|
||||
const mockDomainObjects = createMockDomainObjects(3);
|
||||
mockDomainObjects.forEach(transaction.add.bind(transaction));
|
||||
|
||||
expect(transaction.dirtyObjects.size).toEqual(3);
|
||||
expect(Object.keys(transaction.dirtyObjects).length).toEqual(3);
|
||||
|
||||
transaction.cancel()
|
||||
.then(success => {
|
||||
expect(transaction.dirtyObjects.size).toEqual(0);
|
||||
expect(Object.keys(transaction.dirtyObjects).length).toEqual(0);
|
||||
}).finally(done);
|
||||
});
|
||||
|
||||
@ -59,12 +59,12 @@ describe("Transaction Class", () => {
|
||||
const mockDomainObjects = createMockDomainObjects(3);
|
||||
mockDomainObjects.forEach(transaction.add.bind(transaction));
|
||||
|
||||
expect(transaction.dirtyObjects.size).toEqual(3);
|
||||
expect(Object.keys(transaction.dirtyObjects).length).toEqual(3);
|
||||
spyOn(objectAPI, 'save').and.callThrough();
|
||||
|
||||
transaction.commit()
|
||||
.then(success => {
|
||||
expect(transaction.dirtyObjects.size).toEqual(0);
|
||||
expect(Object.keys(transaction.dirtyObjects).length).toEqual(0);
|
||||
expect(objectAPI.save.calls.count()).toEqual(3);
|
||||
}).finally(done);
|
||||
});
|
||||
@ -73,7 +73,7 @@ describe("Transaction Class", () => {
|
||||
const mockDomainObjects = createMockDomainObjects();
|
||||
transaction.add(mockDomainObjects[0]);
|
||||
|
||||
expect(transaction.dirtyObjects.size).toEqual(1);
|
||||
expect(Object.keys(transaction.dirtyObjects).length).toEqual(1);
|
||||
const dirtyObject = transaction.getDirtyObject(mockDomainObjects[0].identifier);
|
||||
|
||||
expect(dirtyObject).toEqual(mockDomainObjects[0]);
|
||||
@ -82,7 +82,7 @@ describe("Transaction Class", () => {
|
||||
it('getDirtyObject(), returns empty dirtyObject for no active transaction', () => {
|
||||
const mockDomainObjects = createMockDomainObjects();
|
||||
|
||||
expect(transaction.dirtyObjects.size).toEqual(0);
|
||||
expect(Object.keys(transaction.dirtyObjects).length).toEqual(0);
|
||||
const dirtyObject = transaction.getDirtyObject(mockDomainObjects[0].identifier);
|
||||
|
||||
expect(dirtyObject).toEqual(undefined);
|
||||
|
67
src/api/styles/StyleManagerAPI.js
Normal file
67
src/api/styles/StyleManagerAPI.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 EventEmitter from 'EventEmitter';
|
||||
|
||||
export default class StyleManagerAPI extends EventEmitter {
|
||||
constructor(openmct) {
|
||||
super();
|
||||
|
||||
this._openmct = openmct;
|
||||
this._styleCache = {};
|
||||
|
||||
this.get = this.get.bind(this);
|
||||
this.set = this.set.bind(this);
|
||||
this.observe = this.observe.bind(this);
|
||||
}
|
||||
|
||||
get(identifier) {
|
||||
let keyString = this._openmct.objects.makeKeyString(identifier);
|
||||
|
||||
return this._styleCache[keyString];
|
||||
}
|
||||
|
||||
set(identifier, value) {
|
||||
let keyString = this._openmct.objects.makeKeyString(identifier);
|
||||
|
||||
this._styleCache[keyString] = value;
|
||||
this.emit(keyString, value);
|
||||
}
|
||||
|
||||
delete(identifier) {
|
||||
let keyString = this._openmct.objects.makeKeyString(identifier);
|
||||
|
||||
this._styleCache[keyString] = undefined;
|
||||
this.emit(keyString, undefined);
|
||||
delete this._styleCache[keyString];
|
||||
}
|
||||
|
||||
observe(identifier, callback) {
|
||||
let key = this._openmct.objects.makeKeyString(identifier);
|
||||
|
||||
this.on(key, callback);
|
||||
|
||||
return () => {
|
||||
this.off(key, callback);
|
||||
};
|
||||
}
|
||||
}
|
@ -172,6 +172,7 @@ export class TelemetryCollection extends EventEmitter {
|
||||
* @private
|
||||
*/
|
||||
_processNewTelemetry(telemetryData) {
|
||||
performance.mark('tlm:process:start');
|
||||
if (telemetryData === undefined) {
|
||||
return;
|
||||
}
|
||||
@ -184,8 +185,8 @@ export class TelemetryCollection extends EventEmitter {
|
||||
|
||||
for (let datum of data) {
|
||||
parsedValue = this.parseTime(datum);
|
||||
beforeStartOfBounds = parsedValue < this.lastBounds.start;
|
||||
afterEndOfBounds = parsedValue > this.lastBounds.end;
|
||||
beforeStartOfBounds = parsedValue <= this.lastBounds.start;
|
||||
afterEndOfBounds = parsedValue >= this.lastBounds.end;
|
||||
|
||||
if (!afterEndOfBounds && !beforeStartOfBounds) {
|
||||
let isDuplicate = false;
|
||||
@ -352,6 +353,7 @@ export class TelemetryCollection extends EventEmitter {
|
||||
* @todo handle subscriptions more granually
|
||||
*/
|
||||
_reset() {
|
||||
performance.mark('tlm:reset');
|
||||
this.boundedTelemetry = [];
|
||||
this.futureBuffer = [];
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
@ -114,14 +113,12 @@ export default {
|
||||
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
|
||||
this.formats = this.openmct.telemetry.getFormatMap(this.metadata);
|
||||
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
this.bounds = this.openmct.time.bounds();
|
||||
|
||||
this.limitEvaluator = this.openmct
|
||||
.telemetry
|
||||
.limitEvaluator(this.domainObject);
|
||||
|
||||
this.openmct.time.on('timeSystem', this.updateTimeSystem);
|
||||
this.openmct.time.on('bounds', this.updateBounds);
|
||||
|
||||
this.timestampKey = this.openmct.time.timeSystem().key;
|
||||
|
||||
@ -135,72 +132,39 @@ export default {
|
||||
|
||||
this.valueKey = this.valueMetadata ? this.valueMetadata.key : undefined;
|
||||
|
||||
this.unsubscribe = this.openmct
|
||||
.telemetry
|
||||
.subscribe(this.domainObject, this.setLatestValues);
|
||||
|
||||
this.requestHistory();
|
||||
this.telemetryCollection = this.openmct.telemetry.requestCollection(this.domainObject, {
|
||||
size: 1,
|
||||
strategy: 'latest'
|
||||
});
|
||||
this.telemetryCollection.on('add', this.setLatestValues);
|
||||
this.telemetryCollection.on('clear', this.resetValues);
|
||||
this.telemetryCollection.load();
|
||||
|
||||
if (this.hasUnits) {
|
||||
this.setUnit();
|
||||
}
|
||||
},
|
||||
destroyed() {
|
||||
this.unsubscribe();
|
||||
this.openmct.time.off('timeSystem', this.updateTimeSystem);
|
||||
this.openmct.time.off('bounds', this.updateBounds);
|
||||
this.telemetryCollection.off('add', this.setLatestValues);
|
||||
this.telemetryCollection.off('clear', this.resetValues);
|
||||
},
|
||||
methods: {
|
||||
updateView() {
|
||||
if (!this.updatingView) {
|
||||
this.updatingView = true;
|
||||
requestAnimationFrame(() => {
|
||||
let newTimestamp = this.getParsedTimestamp(this.latestDatum);
|
||||
|
||||
if (this.shouldUpdate(newTimestamp)) {
|
||||
this.timestamp = newTimestamp;
|
||||
this.datum = this.latestDatum;
|
||||
}
|
||||
|
||||
this.timestamp = this.getParsedTimestamp(this.latestDatum);
|
||||
this.datum = this.latestDatum;
|
||||
this.updatingView = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
setLatestValues(datum) {
|
||||
this.latestDatum = datum;
|
||||
|
||||
setLatestValues(data) {
|
||||
this.latestDatum = data[data.length - 1];
|
||||
this.updateView();
|
||||
},
|
||||
shouldUpdate(newTimestamp) {
|
||||
return this.inBounds(newTimestamp)
|
||||
&& (this.timestamp === undefined || newTimestamp > this.timestamp);
|
||||
},
|
||||
requestHistory() {
|
||||
this.openmct
|
||||
.telemetry
|
||||
.request(this.domainObject, {
|
||||
start: this.bounds.start,
|
||||
end: this.bounds.end,
|
||||
size: 1,
|
||||
strategy: 'latest'
|
||||
})
|
||||
.then((array) => this.setLatestValues(array[array.length - 1]))
|
||||
.catch((error) => {
|
||||
console.warn('Error fetching data', error);
|
||||
});
|
||||
},
|
||||
updateBounds(bounds, isTick) {
|
||||
this.bounds = bounds;
|
||||
if (!isTick) {
|
||||
this.resetValues();
|
||||
this.requestHistory();
|
||||
}
|
||||
},
|
||||
inBounds(timestamp) {
|
||||
return timestamp >= this.bounds.start && timestamp <= this.bounds.end;
|
||||
},
|
||||
updateTimeSystem(timeSystem) {
|
||||
this.resetValues();
|
||||
this.timestampKey = timeSystem.key;
|
||||
},
|
||||
updateViewContext() {
|
||||
@ -241,4 +205,3 @@ export default {
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -46,6 +46,7 @@ describe("The LAD Table", () => {
|
||||
|
||||
let openmct;
|
||||
let ladPlugin;
|
||||
let historicalProvider;
|
||||
let parent;
|
||||
let child;
|
||||
let telemetryCount = 3;
|
||||
@ -81,6 +82,13 @@ describe("The LAD Table", () => {
|
||||
|
||||
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({}));
|
||||
|
||||
historicalProvider = {
|
||||
request: () => {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
};
|
||||
spyOn(openmct.telemetry, 'findRequestProvider').and.returnValue(historicalProvider);
|
||||
|
||||
openmct.time.bounds({
|
||||
start: bounds.start,
|
||||
end: bounds.end
|
||||
@ -147,7 +155,7 @@ describe("The LAD Table", () => {
|
||||
// add another telemetry object as composition in lad table to test multi rows
|
||||
mockObj.ladTable.composition.push(anotherTelemetryObj.identifier);
|
||||
|
||||
beforeEach(async () => {
|
||||
beforeEach(async (done) => {
|
||||
let telemetryRequestResolve;
|
||||
let telemetryObjectResolve;
|
||||
let anotherTelemetryObjectResolve;
|
||||
@ -166,11 +174,12 @@ describe("The LAD Table", () => {
|
||||
callBack();
|
||||
});
|
||||
|
||||
openmct.telemetry.request.and.callFake(() => {
|
||||
historicalProvider.request = () => {
|
||||
telemetryRequestResolve(mockTelemetry);
|
||||
|
||||
return telemetryRequestPromise;
|
||||
});
|
||||
};
|
||||
|
||||
openmct.objects.get.and.callFake((obj) => {
|
||||
if (obj.key === 'telemetry-object') {
|
||||
telemetryObjectResolve(mockObj.telemetry);
|
||||
@ -195,6 +204,8 @@ describe("The LAD Table", () => {
|
||||
|
||||
await Promise.all([telemetryRequestPromise, telemetryObjectPromise, anotherTelemetryObjectPromise]);
|
||||
await Vue.nextTick();
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it("should show one row per object in the composition", () => {
|
||||
|
@ -25,6 +25,7 @@ import EventEmitter from 'EventEmitter';
|
||||
export default class StyleRuleManager extends EventEmitter {
|
||||
constructor(styleConfiguration, openmct, callback, suppressSubscriptionOnEdit) {
|
||||
super();
|
||||
|
||||
this.openmct = openmct;
|
||||
this.callback = callback;
|
||||
this.refreshData = this.refreshData.bind(this);
|
||||
@ -152,6 +153,7 @@ export default class StyleRuleManager extends EventEmitter {
|
||||
|
||||
updateDomainObjectStyle() {
|
||||
if (this.callback) {
|
||||
this.emit('updateStyles', this.currentStyle);
|
||||
this.callback(Object.assign({}, this.currentStyle));
|
||||
}
|
||||
}
|
||||
|
@ -27,7 +27,7 @@
|
||||
:href="url"
|
||||
>
|
||||
<div class="c-condition-widget__label">
|
||||
{{ internalDomainObject.conditionalLabel || internalDomainObject.label }}
|
||||
{{ label }}
|
||||
</div>
|
||||
</component>
|
||||
</template>
|
||||
@ -39,10 +39,16 @@ export default {
|
||||
inject: ['openmct', 'domainObject'],
|
||||
data: function () {
|
||||
return {
|
||||
internalDomainObject: this.domainObject
|
||||
internalDomainObject: this.domainObject,
|
||||
conditionalLabel: ''
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
label() {
|
||||
return this.conditionalLabel.length
|
||||
? this.conditionalLabel
|
||||
: this.internalDomainObject.label;
|
||||
},
|
||||
urlDefined() {
|
||||
return this.internalDomainObject.url && this.internalDomainObject.url.length > 0;
|
||||
},
|
||||
@ -52,13 +58,31 @@ export default {
|
||||
},
|
||||
mounted() {
|
||||
this.unlisten = this.openmct.objects.observe(this.internalDomainObject, '*', this.updateInternalDomainObject);
|
||||
|
||||
this.unobserve = this.openmct.styleManager.observe(this.internalDomainObject.identifier, this.observeStyleManagerChanges.bind(this));
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.unlisten) {
|
||||
this.unlisten();
|
||||
}
|
||||
|
||||
if (this.unobserve) {
|
||||
this.openmct.styleManager.delete(this.internalDomainObject.identifier);
|
||||
this.unobserve();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
observeStyleManagerChanges(styleManager) {
|
||||
if (styleManager) {
|
||||
this.styleManager = styleManager;
|
||||
this.styleManager.on('updateStyles', this.updateConditionLabel);
|
||||
} else {
|
||||
this.styleManager.off('updateStyles', this.updateConditionLabel);
|
||||
}
|
||||
},
|
||||
updateConditionLabel(styleObj = {}) {
|
||||
this.conditionalLabel = styleObj.output || '';
|
||||
},
|
||||
updateInternalDomainObject(domainObject) {
|
||||
this.internalDomainObject = domainObject;
|
||||
}
|
||||
|
@ -222,20 +222,18 @@ export default {
|
||||
.then(this.setObject);
|
||||
}
|
||||
|
||||
this.openmct.time.on("bounds", this.refreshData);
|
||||
|
||||
this.status = this.openmct.status.get(this.item.identifier);
|
||||
this.removeStatusListener = this.openmct.status.observe(this.item.identifier, this.setStatus);
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.removeSubscription();
|
||||
this.removeStatusListener();
|
||||
|
||||
if (this.removeSelectable) {
|
||||
this.removeSelectable();
|
||||
}
|
||||
|
||||
this.openmct.time.off("bounds", this.refreshData);
|
||||
this.telemetryCollection.off('add', this.setLatestValues);
|
||||
this.telemetryCollection.off('clear', this.refreshData);
|
||||
|
||||
if (this.mutablePromise) {
|
||||
this.mutablePromise.then(() => {
|
||||
@ -253,34 +251,9 @@ export default {
|
||||
|
||||
return `At ${timeFormatter.format(this.datum)} ${this.domainObject.name} had a value of ${this.telemetryValue}${unit}`;
|
||||
},
|
||||
requestHistoricalData() {
|
||||
let bounds = this.openmct.time.bounds();
|
||||
let options = {
|
||||
start: bounds.start,
|
||||
end: bounds.end,
|
||||
size: 1,
|
||||
strategy: 'latest'
|
||||
};
|
||||
this.openmct.telemetry.request(this.domainObject, options)
|
||||
.then(data => {
|
||||
if (data.length > 0) {
|
||||
this.latestDatum = data[data.length - 1];
|
||||
this.updateView();
|
||||
}
|
||||
});
|
||||
},
|
||||
subscribeToObject() {
|
||||
this.subscription = this.openmct.telemetry.subscribe(this.domainObject, function (datum) {
|
||||
const key = this.openmct.time.timeSystem().key;
|
||||
const datumTimeStamp = datum[key];
|
||||
if (this.openmct.time.clock() !== undefined
|
||||
|| (datumTimeStamp
|
||||
&& (this.openmct.time.bounds().end >= datumTimeStamp))
|
||||
) {
|
||||
this.latestDatum = datum;
|
||||
this.updateView();
|
||||
}
|
||||
}.bind(this));
|
||||
setLatestValues(data) {
|
||||
this.latestDatum = data[data.length - 1];
|
||||
this.updateView();
|
||||
},
|
||||
updateView() {
|
||||
if (!this.updatingView) {
|
||||
@ -291,17 +264,10 @@ export default {
|
||||
});
|
||||
}
|
||||
},
|
||||
removeSubscription() {
|
||||
if (this.subscription) {
|
||||
this.subscription();
|
||||
this.subscription = undefined;
|
||||
}
|
||||
},
|
||||
refreshData(bounds, isTick) {
|
||||
if (!isTick) {
|
||||
this.latestDatum = undefined;
|
||||
this.updateView();
|
||||
this.requestHistoricalData(this.domainObject);
|
||||
}
|
||||
},
|
||||
setObject(domainObject) {
|
||||
@ -315,8 +281,13 @@ export default {
|
||||
const valueMetadata = this.metadata.value(this.item.value);
|
||||
this.customStringformatter = this.openmct.telemetry.customStringFormatter(valueMetadata, this.item.format);
|
||||
|
||||
this.requestHistoricalData();
|
||||
this.subscribeToObject();
|
||||
this.telemetryCollection = this.openmct.telemetry.requestCollection(this.domainObject, {
|
||||
size: 1,
|
||||
strategy: 'latest'
|
||||
});
|
||||
this.telemetryCollection.on('add', this.setLatestValues);
|
||||
this.telemetryCollection.on('clear', this.refreshData);
|
||||
this.telemetryCollection.load();
|
||||
|
||||
this.currentObjectPath = this.objectPath.slice();
|
||||
this.currentObjectPath.unshift(this.domainObject);
|
||||
|
@ -57,7 +57,7 @@
|
||||
/>
|
||||
|
||||
<drop-hint
|
||||
:key="i"
|
||||
:key="'hint-' + i"
|
||||
class="c-fl-frame__drop-hint"
|
||||
:index="i"
|
||||
:allow-drop="allowDrop"
|
||||
@ -66,7 +66,7 @@
|
||||
|
||||
<resize-handle
|
||||
v-if="(i !== frames.length - 1)"
|
||||
:key="i"
|
||||
:key="'handle-' + i"
|
||||
:index="i"
|
||||
:orientation="rowsLayout ? 'horizontal' : 'vertical'"
|
||||
:is-editing="isEditing"
|
||||
|
@ -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)
|
||||
|
@ -51,41 +51,29 @@ export default class EditPropertiesAction extends PropertiesAction {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
async _onSave(changes) {
|
||||
Object.entries(changes).forEach(([key, value]) => {
|
||||
const properties = key.split('.');
|
||||
let object = this.domainObject;
|
||||
const propertiesLength = properties.length;
|
||||
properties.forEach((property, index) => {
|
||||
const isComplexProperty = propertiesLength > 1 && index !== propertiesLength - 1;
|
||||
if (isComplexProperty && object[property] !== null) {
|
||||
object = object[property];
|
||||
} else {
|
||||
object[property] = value;
|
||||
}
|
||||
_onSave(changes) {
|
||||
try {
|
||||
Object.entries(changes).forEach(([key, value]) => {
|
||||
const properties = key.split('.');
|
||||
let object = this.domainObject;
|
||||
const propertiesLength = properties.length;
|
||||
properties.forEach((property, index) => {
|
||||
const isComplexProperty = propertiesLength > 1 && index !== propertiesLength - 1;
|
||||
if (isComplexProperty && object[property] !== null) {
|
||||
object = object[property];
|
||||
} else {
|
||||
object[property] = value;
|
||||
}
|
||||
});
|
||||
|
||||
object = value;
|
||||
this.openmct.objects.mutate(this.domainObject, key, value);
|
||||
this.openmct.notifications.info('Save successful');
|
||||
});
|
||||
|
||||
object = value;
|
||||
});
|
||||
|
||||
this.domainObject.modified = Date.now();
|
||||
|
||||
// Show saving progress dialog
|
||||
let dialog = this.openmct.overlays.progressDialog({
|
||||
progressPerc: 'unknown',
|
||||
message: 'Do not navigate away from this page or close this browser tab while this message is displayed.',
|
||||
iconClass: 'info',
|
||||
title: 'Saving'
|
||||
});
|
||||
|
||||
const success = await this.openmct.objects.save(this.domainObject);
|
||||
if (success) {
|
||||
this.openmct.notifications.info('Save successful');
|
||||
} else {
|
||||
} catch (error) {
|
||||
this.openmct.notifications.error('Error saving objects');
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
dialog.dismiss();
|
||||
}
|
||||
|
||||
/**
|
||||
|
199
src/plugins/gauge/GaugePlugin.js
Normal file
199
src/plugins/gauge/GaugePlugin.js
Normal file
@ -0,0 +1,199 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import GaugeViewProvider from './GaugeViewProvider';
|
||||
import GaugeFormController from './components/GaugeFormController.vue';
|
||||
import Vue from 'vue';
|
||||
|
||||
export const GAUGE_TYPES = [
|
||||
['Filled Dial', 'dial-filled'],
|
||||
['Needle Dial', 'dial-needle'],
|
||||
['Vertical Meter', 'meter-vertical'],
|
||||
['Vertical Meter Inverted', 'meter-vertical-inverted'],
|
||||
['Horizontal Meter', 'meter-horizontal']
|
||||
];
|
||||
|
||||
export default function () {
|
||||
return function install(openmct) {
|
||||
openmct.objectViews.addProvider(new GaugeViewProvider(openmct));
|
||||
|
||||
openmct.forms.addNewFormControl('gauge-controller', getGaugeFormController(openmct));
|
||||
openmct.types.addType('gauge', {
|
||||
name: "Gauge",
|
||||
creatable: true,
|
||||
description: "Graphically visualize a telemetry element's current value between a minimum and maximum.",
|
||||
cssClass: 'icon-gauge',
|
||||
initialize(domainObject) {
|
||||
domainObject.composition = [];
|
||||
domainObject.configuration = {
|
||||
gaugeController: {
|
||||
gaugeType: GAUGE_TYPES[0][1],
|
||||
isDisplayMinMax: true,
|
||||
isDisplayCurVal: true,
|
||||
isUseTelemetryLimits: true,
|
||||
limitLow: 10,
|
||||
limitHigh: 90,
|
||||
max: 100,
|
||||
min: 0,
|
||||
precision: 2
|
||||
}
|
||||
};
|
||||
},
|
||||
form: [
|
||||
{
|
||||
name: "Display current value",
|
||||
control: "toggleSwitch",
|
||||
cssClass: "l-input",
|
||||
key: "isDisplayCurVal",
|
||||
property: [
|
||||
"configuration",
|
||||
"gaugeController",
|
||||
"isDisplayCurVal"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Display range values",
|
||||
control: "toggleSwitch",
|
||||
cssClass: "l-input",
|
||||
key: "isDisplayMinMax",
|
||||
property: [
|
||||
"configuration",
|
||||
"gaugeController",
|
||||
"isDisplayMinMax"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Float precision",
|
||||
control: "numberfield",
|
||||
cssClass: "l-input-sm",
|
||||
key: "precision",
|
||||
property: [
|
||||
"configuration",
|
||||
"gaugeController",
|
||||
"precision"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Gauge type",
|
||||
options: GAUGE_TYPES.map(type => {
|
||||
return {
|
||||
name: type[0],
|
||||
value: type[1]
|
||||
};
|
||||
}),
|
||||
control: "select",
|
||||
cssClass: "l-input-sm",
|
||||
key: "gaugeController",
|
||||
property: [
|
||||
"configuration",
|
||||
"gaugeController",
|
||||
"gaugeType"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Value ranges and limits",
|
||||
control: "gauge-controller",
|
||||
cssClass: "l-input",
|
||||
key: "gaugeController",
|
||||
required: false,
|
||||
hideFromInspector: true,
|
||||
property: [
|
||||
"configuration",
|
||||
"gaugeController"
|
||||
],
|
||||
validate: ({ value }, callback) => {
|
||||
if (value.isUseTelemetryLimits) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const { min, max, limitLow, limitHigh } = value;
|
||||
const valid = {
|
||||
min: true,
|
||||
max: true,
|
||||
limitLow: true,
|
||||
limitHigh: true
|
||||
};
|
||||
|
||||
if (min === '') {
|
||||
valid.min = false;
|
||||
}
|
||||
|
||||
if (max === '') {
|
||||
valid.max = false;
|
||||
}
|
||||
|
||||
if (max < min) {
|
||||
valid.min = false;
|
||||
valid.max = false;
|
||||
}
|
||||
|
||||
if (limitLow !== '') {
|
||||
valid.limitLow = min <= limitLow && limitLow < max;
|
||||
}
|
||||
|
||||
if (limitHigh !== '') {
|
||||
valid.limitHigh = min < limitHigh && limitHigh <= max;
|
||||
}
|
||||
|
||||
if (valid.limitLow && valid.limitHigh
|
||||
&& limitLow !== '' && limitHigh !== ''
|
||||
&& limitLow > limitHigh) {
|
||||
valid.limitLow = false;
|
||||
valid.limitHigh = false;
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
callback(valid);
|
||||
}
|
||||
|
||||
return valid.min && valid.max && valid.limitLow && valid.limitHigh;
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
};
|
||||
|
||||
function getGaugeFormController(openmct) {
|
||||
return {
|
||||
show(element, model, onChange) {
|
||||
const rowComponent = new Vue({
|
||||
el: element,
|
||||
components: {
|
||||
GaugeFormController
|
||||
},
|
||||
provide: {
|
||||
openmct
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
model,
|
||||
onChange
|
||||
};
|
||||
},
|
||||
template: `<GaugeFormController :model="model" @onChange="onChange"></GaugeFormController>`
|
||||
});
|
||||
|
||||
return rowComponent;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
801
src/plugins/gauge/GaugePluginSpec.js
Normal file
801
src/plugins/gauge/GaugePluginSpec.js
Normal file
@ -0,0 +1,801 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* 'License'); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import {
|
||||
createOpenMct,
|
||||
resetApplicationState
|
||||
} from 'utils/testing';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
import Vue from 'vue';
|
||||
|
||||
let gaugeDomainObject = {
|
||||
identifier: {
|
||||
key: 'gauge',
|
||||
namespace: 'test-namespace'
|
||||
},
|
||||
type: 'gauge',
|
||||
composition: []
|
||||
};
|
||||
|
||||
describe('Gauge plugin', () => {
|
||||
let openmct;
|
||||
let child;
|
||||
let gaugeHolder;
|
||||
|
||||
beforeEach((done) => {
|
||||
gaugeHolder = document.createElement('div');
|
||||
gaugeHolder.style.display = 'block';
|
||||
gaugeHolder.style.width = '1920px';
|
||||
gaugeHolder.style.height = '1080px';
|
||||
|
||||
child = document.createElement('div');
|
||||
gaugeHolder.appendChild(child);
|
||||
|
||||
openmct = createOpenMct();
|
||||
openmct.on('start', done);
|
||||
|
||||
openmct.install(openmct.plugins.Gauge());
|
||||
|
||||
openmct.startHeadless();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
return resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
it('Plugin installed by default', () => {
|
||||
const gaugueType = openmct.types.get('gauge');
|
||||
|
||||
expect(gaugueType).not.toBeNull();
|
||||
expect(gaugueType.definition.name).toEqual('Gauge');
|
||||
});
|
||||
|
||||
it('Gaugue plugin is creatable', () => {
|
||||
const gaugueType = openmct.types.get('gauge');
|
||||
|
||||
expect(gaugueType.definition.creatable).toBeTrue();
|
||||
});
|
||||
|
||||
it('Gaugue plugin is creatable', () => {
|
||||
const gaugueType = openmct.types.get('gauge');
|
||||
|
||||
expect(gaugueType.definition.creatable).toBeTrue();
|
||||
});
|
||||
|
||||
it('Gaugue form controller', () => {
|
||||
const gaugeController = openmct.forms.getFormControl('gauge-controller');
|
||||
expect(gaugeController).toBeDefined();
|
||||
});
|
||||
|
||||
describe('Gaugue with Filled Dial', () => {
|
||||
let gaugeViewProvider;
|
||||
let gaugeView;
|
||||
let gaugeViewObject;
|
||||
let mutablegaugeObject;
|
||||
let randomValue;
|
||||
|
||||
const minValue = -1;
|
||||
const maxValue = 1;
|
||||
|
||||
beforeEach(() => {
|
||||
randomValue = Math.random();
|
||||
gaugeViewObject = {
|
||||
...gaugeDomainObject,
|
||||
configuration: {
|
||||
gaugeController: {
|
||||
gaugeType: 'dial-filled',
|
||||
isDisplayMinMax: true,
|
||||
isDisplayCurVal: true,
|
||||
isUseTelemetryLimits: false,
|
||||
limitLow: -0.9,
|
||||
limitHigh: 0.9,
|
||||
max: maxValue,
|
||||
min: minValue,
|
||||
precision: 2
|
||||
}
|
||||
},
|
||||
composition: [
|
||||
{
|
||||
namespace: 'test-namespace',
|
||||
key: 'test-object'
|
||||
}
|
||||
],
|
||||
id: 'test-object',
|
||||
name: 'gauge'
|
||||
};
|
||||
|
||||
const testObjectProvider = jasmine.createSpyObj('testObjectProvider', [
|
||||
'get',
|
||||
'create',
|
||||
'update',
|
||||
'observe'
|
||||
]);
|
||||
|
||||
openmct.editor = {};
|
||||
openmct.editor.isEditing = () => false;
|
||||
|
||||
const applicableViews = openmct.objectViews.get(gaugeViewObject, [gaugeViewObject]);
|
||||
gaugeViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'gauge');
|
||||
|
||||
testObjectProvider.get.and.returnValue(Promise.resolve(gaugeViewObject));
|
||||
testObjectProvider.create.and.returnValue(Promise.resolve(gaugeViewObject));
|
||||
openmct.objects.addProvider('test-namespace', testObjectProvider);
|
||||
testObjectProvider.observe.and.returnValue(() => {});
|
||||
testObjectProvider.create.and.returnValue(Promise.resolve(true));
|
||||
testObjectProvider.update.and.returnValue(Promise.resolve(true));
|
||||
|
||||
spyOn(openmct.telemetry, 'getMetadata').and.returnValue({
|
||||
valuesForHints: () => {
|
||||
return [
|
||||
{
|
||||
source: 'sin'
|
||||
}
|
||||
];
|
||||
},
|
||||
value: () => 1
|
||||
});
|
||||
spyOn(openmct.telemetry, 'getValueFormatter').and.returnValue({
|
||||
parse: () => {
|
||||
return 2000;
|
||||
}
|
||||
});
|
||||
spyOn(openmct.telemetry, 'getFormatMap').and.returnValue({
|
||||
sin: {
|
||||
format: (datum) => {
|
||||
return randomValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
spyOn(openmct.telemetry, 'getLimits').and.returnValue({ limits: () => Promise.resolve() });
|
||||
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([randomValue]));
|
||||
spyOn(openmct.time, 'bounds').and.returnValue({
|
||||
start: 1000,
|
||||
end: 5000
|
||||
});
|
||||
|
||||
return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => {
|
||||
mutablegaugeObject = mutableObject;
|
||||
gaugeView = gaugeViewProvider.view(mutablegaugeObject);
|
||||
gaugeView.show(child);
|
||||
|
||||
return Vue.nextTick();
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
gaugeView.destroy();
|
||||
|
||||
return resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
it('provides gauge view', () => {
|
||||
expect(gaugeViewProvider).toBeDefined();
|
||||
});
|
||||
|
||||
it('renders gauge element', () => {
|
||||
const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper');
|
||||
expect(gaugeElement.length).toBe(1);
|
||||
});
|
||||
|
||||
it('renders major elements', () => {
|
||||
const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper');
|
||||
const rangeElement = gaugeHolder.querySelector('.js-gauge-dial-range');
|
||||
const valueElement = gaugeHolder.querySelector('.js-dial-current-value');
|
||||
|
||||
const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement);
|
||||
|
||||
expect(hasMajorElements).toBe(true);
|
||||
});
|
||||
|
||||
it('renders correct min max values', () => {
|
||||
expect(gaugeHolder.querySelector('.js-gauge-dial-range').textContent).toEqual(`${minValue} ${maxValue}`);
|
||||
});
|
||||
|
||||
it('renders correct current value', (done) => {
|
||||
function WatchUpdateValue() {
|
||||
const textElement = gaugeHolder.querySelector('.js-dial-current-value');
|
||||
expect(Number(textElement.textContent).toFixed(gaugeViewObject.configuration.gaugeController.precision)).toBe(randomValue.toFixed(gaugeViewObject.configuration.gaugeController.precision));
|
||||
done();
|
||||
}
|
||||
|
||||
const debouncedWatchUpdate = debounce(WatchUpdateValue, 200);
|
||||
Vue.nextTick(debouncedWatchUpdate);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Gaugue with Needle Dial', () => {
|
||||
let gaugeViewProvider;
|
||||
let gaugeView;
|
||||
let gaugeViewObject;
|
||||
let mutablegaugeObject;
|
||||
let randomValue;
|
||||
|
||||
const minValue = -1;
|
||||
const maxValue = 1;
|
||||
beforeEach(() => {
|
||||
randomValue = Math.random();
|
||||
gaugeViewObject = {
|
||||
...gaugeDomainObject,
|
||||
configuration: {
|
||||
gaugeController: {
|
||||
gaugeType: 'dial-needle',
|
||||
isDisplayMinMax: true,
|
||||
isDisplayCurVal: true,
|
||||
isUseTelemetryLimits: false,
|
||||
limitLow: -0.9,
|
||||
limitHigh: 0.9,
|
||||
max: maxValue,
|
||||
min: minValue,
|
||||
precision: 2
|
||||
}
|
||||
},
|
||||
composition: [
|
||||
{
|
||||
namespace: 'test-namespace',
|
||||
key: 'test-object'
|
||||
}
|
||||
],
|
||||
id: 'test-object',
|
||||
name: 'gauge'
|
||||
};
|
||||
|
||||
const testObjectProvider = jasmine.createSpyObj('testObjectProvider', [
|
||||
'get',
|
||||
'create',
|
||||
'update',
|
||||
'observe'
|
||||
]);
|
||||
|
||||
openmct.editor = {};
|
||||
openmct.editor.isEditing = () => false;
|
||||
|
||||
const applicableViews = openmct.objectViews.get(gaugeViewObject, [gaugeViewObject]);
|
||||
gaugeViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'gauge');
|
||||
|
||||
testObjectProvider.get.and.returnValue(Promise.resolve(gaugeViewObject));
|
||||
testObjectProvider.create.and.returnValue(Promise.resolve(gaugeViewObject));
|
||||
openmct.objects.addProvider('test-namespace', testObjectProvider);
|
||||
testObjectProvider.observe.and.returnValue(() => {});
|
||||
testObjectProvider.create.and.returnValue(Promise.resolve(true));
|
||||
testObjectProvider.update.and.returnValue(Promise.resolve(true));
|
||||
|
||||
spyOn(openmct.telemetry, 'getMetadata').and.returnValue({
|
||||
valuesForHints: () => {
|
||||
return [
|
||||
{
|
||||
source: 'sin'
|
||||
}
|
||||
];
|
||||
},
|
||||
value: () => 1
|
||||
});
|
||||
spyOn(openmct.telemetry, 'getValueFormatter').and.returnValue({
|
||||
parse: () => {
|
||||
return 2000;
|
||||
}
|
||||
});
|
||||
spyOn(openmct.telemetry, 'getFormatMap').and.returnValue({
|
||||
sin: {
|
||||
format: (datum) => {
|
||||
return randomValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
spyOn(openmct.telemetry, 'getLimits').and.returnValue({ limits: () => Promise.resolve() });
|
||||
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([randomValue]));
|
||||
spyOn(openmct.time, 'bounds').and.returnValue({
|
||||
start: 1000,
|
||||
end: 5000
|
||||
});
|
||||
|
||||
return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => {
|
||||
mutablegaugeObject = mutableObject;
|
||||
gaugeView = gaugeViewProvider.view(mutablegaugeObject);
|
||||
gaugeView.show(child);
|
||||
|
||||
return Vue.nextTick();
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
gaugeView.destroy();
|
||||
|
||||
return resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
it('provides gauge view', () => {
|
||||
expect(gaugeViewProvider).toBeDefined();
|
||||
});
|
||||
|
||||
it('renders gauge element', () => {
|
||||
const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper');
|
||||
expect(gaugeElement.length).toBe(1);
|
||||
});
|
||||
|
||||
it('renders major elements', () => {
|
||||
const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper');
|
||||
const rangeElement = gaugeHolder.querySelector('.js-gauge-dial-range');
|
||||
const valueElement = gaugeHolder.querySelector('.js-dial-current-value');
|
||||
|
||||
const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement);
|
||||
|
||||
expect(hasMajorElements).toBe(true);
|
||||
});
|
||||
|
||||
it('renders correct min max values', () => {
|
||||
expect(gaugeHolder.querySelector('.js-gauge-dial-range').textContent).toEqual(`${minValue} ${maxValue}`);
|
||||
});
|
||||
|
||||
it('renders correct current value', (done) => {
|
||||
function WatchUpdateValue() {
|
||||
const textElement = gaugeHolder.querySelector('.js-dial-current-value');
|
||||
expect(Number(textElement.textContent).toFixed(gaugeViewObject.configuration.gaugeController.precision)).toBe(randomValue.toFixed(gaugeViewObject.configuration.gaugeController.precision));
|
||||
done();
|
||||
}
|
||||
|
||||
const debouncedWatchUpdate = debounce(WatchUpdateValue, 200);
|
||||
Vue.nextTick(debouncedWatchUpdate);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Gaugue with Vertical Meter', () => {
|
||||
let gaugeViewProvider;
|
||||
let gaugeView;
|
||||
let gaugeViewObject;
|
||||
let mutablegaugeObject;
|
||||
let randomValue;
|
||||
|
||||
const minValue = -1;
|
||||
const maxValue = 1;
|
||||
beforeEach(() => {
|
||||
randomValue = Math.random();
|
||||
gaugeViewObject = {
|
||||
...gaugeDomainObject,
|
||||
configuration: {
|
||||
gaugeController: {
|
||||
gaugeType: 'meter-vertical',
|
||||
isDisplayMinMax: true,
|
||||
isDisplayCurVal: true,
|
||||
isUseTelemetryLimits: false,
|
||||
limitLow: -0.9,
|
||||
limitHigh: 0.9,
|
||||
max: maxValue,
|
||||
min: minValue,
|
||||
precision: 2
|
||||
}
|
||||
},
|
||||
composition: [
|
||||
{
|
||||
namespace: 'test-namespace',
|
||||
key: 'test-object'
|
||||
}
|
||||
],
|
||||
id: 'test-object',
|
||||
name: 'gauge'
|
||||
};
|
||||
|
||||
const testObjectProvider = jasmine.createSpyObj('testObjectProvider', [
|
||||
'get',
|
||||
'create',
|
||||
'update',
|
||||
'observe'
|
||||
]);
|
||||
|
||||
openmct.editor = {};
|
||||
openmct.editor.isEditing = () => false;
|
||||
|
||||
const applicableViews = openmct.objectViews.get(gaugeViewObject, [gaugeViewObject]);
|
||||
gaugeViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'gauge');
|
||||
|
||||
testObjectProvider.get.and.returnValue(Promise.resolve(gaugeViewObject));
|
||||
testObjectProvider.create.and.returnValue(Promise.resolve(gaugeViewObject));
|
||||
openmct.objects.addProvider('test-namespace', testObjectProvider);
|
||||
testObjectProvider.observe.and.returnValue(() => {});
|
||||
testObjectProvider.create.and.returnValue(Promise.resolve(true));
|
||||
testObjectProvider.update.and.returnValue(Promise.resolve(true));
|
||||
|
||||
spyOn(openmct.telemetry, 'getMetadata').and.returnValue({
|
||||
valuesForHints: () => {
|
||||
return [
|
||||
{
|
||||
source: 'sin'
|
||||
}
|
||||
];
|
||||
},
|
||||
value: () => 1
|
||||
});
|
||||
spyOn(openmct.telemetry, 'getValueFormatter').and.returnValue({
|
||||
parse: () => {
|
||||
return 2000;
|
||||
}
|
||||
});
|
||||
spyOn(openmct.telemetry, 'getFormatMap').and.returnValue({
|
||||
sin: {
|
||||
format: (datum) => {
|
||||
return randomValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
spyOn(openmct.telemetry, 'getLimits').and.returnValue({ limits: () => Promise.resolve() });
|
||||
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([randomValue]));
|
||||
spyOn(openmct.time, 'bounds').and.returnValue({
|
||||
start: 1000,
|
||||
end: 5000
|
||||
});
|
||||
|
||||
return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => {
|
||||
mutablegaugeObject = mutableObject;
|
||||
gaugeView = gaugeViewProvider.view(mutablegaugeObject);
|
||||
gaugeView.show(child);
|
||||
|
||||
return Vue.nextTick();
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
gaugeView.destroy();
|
||||
|
||||
return resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
it('provides gauge view', () => {
|
||||
expect(gaugeViewProvider).toBeDefined();
|
||||
});
|
||||
|
||||
it('renders gauge element', () => {
|
||||
const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper');
|
||||
expect(gaugeElement.length).toBe(1);
|
||||
});
|
||||
|
||||
it('renders major elements', () => {
|
||||
const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper');
|
||||
const rangeElement = gaugeHolder.querySelector('.js-gauge-meter-range');
|
||||
const valueElement = gaugeHolder.querySelector('.js-meter-current-value');
|
||||
|
||||
const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement);
|
||||
|
||||
expect(hasMajorElements).toBe(true);
|
||||
});
|
||||
|
||||
it('renders correct min max values', () => {
|
||||
expect(gaugeHolder.querySelector('.js-gauge-meter-range').textContent).toEqual(`${maxValue} ${minValue}`);
|
||||
});
|
||||
|
||||
it('renders correct current value', (done) => {
|
||||
function WatchUpdateValue() {
|
||||
const textElement = gaugeHolder.querySelector('.js-meter-current-value');
|
||||
expect(Number(textElement.textContent).toFixed(gaugeViewObject.configuration.gaugeController.precision)).toBe(randomValue.toFixed(gaugeViewObject.configuration.gaugeController.precision));
|
||||
done();
|
||||
}
|
||||
|
||||
const debouncedWatchUpdate = debounce(WatchUpdateValue, 200);
|
||||
Vue.nextTick(debouncedWatchUpdate);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Gaugue with Vertical Meter Inverted', () => {
|
||||
let gaugeViewProvider;
|
||||
let gaugeView;
|
||||
let gaugeViewObject;
|
||||
let mutablegaugeObject;
|
||||
|
||||
beforeEach(() => {
|
||||
gaugeViewObject = {
|
||||
...gaugeDomainObject,
|
||||
configuration: {
|
||||
gaugeController: {
|
||||
gaugeType: 'meter-vertical',
|
||||
isDisplayMinMax: true,
|
||||
isDisplayCurVal: true,
|
||||
isUseTelemetryLimits: false,
|
||||
limitLow: -0.9,
|
||||
limitHigh: 0.9,
|
||||
max: 1,
|
||||
min: -1,
|
||||
precision: 2
|
||||
}
|
||||
},
|
||||
id: 'test-object',
|
||||
name: 'gauge'
|
||||
};
|
||||
|
||||
const testObjectProvider = jasmine.createSpyObj('testObjectProvider', [
|
||||
'get',
|
||||
'create',
|
||||
'update',
|
||||
'observe'
|
||||
]);
|
||||
|
||||
openmct.editor = {};
|
||||
openmct.editor.isEditing = () => false;
|
||||
|
||||
const applicableViews = openmct.objectViews.get(gaugeViewObject, [gaugeViewObject]);
|
||||
gaugeViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'gauge');
|
||||
|
||||
testObjectProvider.get.and.returnValue(Promise.resolve(gaugeViewObject));
|
||||
testObjectProvider.create.and.returnValue(Promise.resolve(gaugeViewObject));
|
||||
openmct.objects.addProvider('test-namespace', testObjectProvider);
|
||||
testObjectProvider.observe.and.returnValue(() => {});
|
||||
testObjectProvider.create.and.returnValue(Promise.resolve(true));
|
||||
testObjectProvider.update.and.returnValue(Promise.resolve(true));
|
||||
|
||||
return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => {
|
||||
mutablegaugeObject = mutableObject;
|
||||
|
||||
gaugeView = gaugeViewProvider.view(mutablegaugeObject);
|
||||
gaugeView.show(child);
|
||||
|
||||
return Vue.nextTick();
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
gaugeView.destroy();
|
||||
|
||||
return resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
it('provides gauge view', () => {
|
||||
expect(gaugeViewProvider).toBeDefined();
|
||||
});
|
||||
|
||||
it('renders gauge element', () => {
|
||||
const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper');
|
||||
expect(gaugeElement.length).toBe(1);
|
||||
});
|
||||
|
||||
it('renders major elements', () => {
|
||||
const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper');
|
||||
const rangeElement = gaugeHolder.querySelector('.js-gauge-meter-range');
|
||||
const valueElement = gaugeHolder.querySelector('.js-meter-current-value');
|
||||
|
||||
const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement);
|
||||
|
||||
expect(hasMajorElements).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Gaugue with Horizontal Meter', () => {
|
||||
let gaugeViewProvider;
|
||||
let gaugeView;
|
||||
let gaugeViewObject;
|
||||
let mutablegaugeObject;
|
||||
|
||||
beforeEach(() => {
|
||||
gaugeViewObject = {
|
||||
...gaugeDomainObject,
|
||||
configuration: {
|
||||
gaugeController: {
|
||||
gaugeType: 'meter-vertical',
|
||||
isDisplayMinMax: true,
|
||||
isDisplayCurVal: true,
|
||||
isUseTelemetryLimits: false,
|
||||
limitLow: -0.9,
|
||||
limitHigh: 0.9,
|
||||
max: 1,
|
||||
min: -1,
|
||||
precision: 2
|
||||
}
|
||||
},
|
||||
id: 'test-object',
|
||||
name: 'gauge'
|
||||
};
|
||||
|
||||
const testObjectProvider = jasmine.createSpyObj('testObjectProvider', [
|
||||
'get',
|
||||
'create',
|
||||
'update',
|
||||
'observe'
|
||||
]);
|
||||
|
||||
openmct.editor = {};
|
||||
openmct.editor.isEditing = () => false;
|
||||
|
||||
const applicableViews = openmct.objectViews.get(gaugeViewObject, [gaugeViewObject]);
|
||||
gaugeViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'gauge');
|
||||
|
||||
testObjectProvider.get.and.returnValue(Promise.resolve(gaugeViewObject));
|
||||
testObjectProvider.create.and.returnValue(Promise.resolve(gaugeViewObject));
|
||||
openmct.objects.addProvider('test-namespace', testObjectProvider);
|
||||
testObjectProvider.observe.and.returnValue(() => {});
|
||||
testObjectProvider.create.and.returnValue(Promise.resolve(true));
|
||||
testObjectProvider.update.and.returnValue(Promise.resolve(true));
|
||||
|
||||
return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => {
|
||||
mutablegaugeObject = mutableObject;
|
||||
|
||||
gaugeView = gaugeViewProvider.view(mutablegaugeObject);
|
||||
gaugeView.show(child);
|
||||
|
||||
return Vue.nextTick();
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
gaugeView.destroy();
|
||||
|
||||
return resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
it('provides gauge view', () => {
|
||||
expect(gaugeViewProvider).toBeDefined();
|
||||
});
|
||||
|
||||
it('renders gauge element', () => {
|
||||
const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper');
|
||||
expect(gaugeElement.length).toBe(1);
|
||||
});
|
||||
|
||||
it('renders major elements', () => {
|
||||
const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper');
|
||||
const rangeElement = gaugeHolder.querySelector('.c-gauge__range');
|
||||
const curveElement = gaugeHolder.querySelector('.c-meter');
|
||||
|
||||
const hasMajorElements = Boolean(wrapperElement && rangeElement && curveElement);
|
||||
|
||||
expect(hasMajorElements).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Gaugue with Filled Dial with Use Telemetry Limits', () => {
|
||||
let gaugeViewProvider;
|
||||
let gaugeView;
|
||||
let gaugeViewObject;
|
||||
let mutablegaugeObject;
|
||||
let randomValue;
|
||||
|
||||
beforeEach(() => {
|
||||
randomValue = Math.random();
|
||||
|
||||
gaugeViewObject = {
|
||||
...gaugeDomainObject,
|
||||
configuration: {
|
||||
gaugeController: {
|
||||
gaugeType: 'dial-filled',
|
||||
isDisplayMinMax: true,
|
||||
isDisplayCurVal: true,
|
||||
isUseTelemetryLimits: true,
|
||||
limitLow: 10,
|
||||
limitHigh: 90,
|
||||
max: 100,
|
||||
min: 0,
|
||||
precision: 2
|
||||
}
|
||||
},
|
||||
composition: [
|
||||
{
|
||||
namespace: 'test-namespace',
|
||||
key: 'test-object'
|
||||
}
|
||||
],
|
||||
id: 'test-object',
|
||||
name: 'gauge'
|
||||
};
|
||||
|
||||
const testObjectProvider = jasmine.createSpyObj('testObjectProvider', [
|
||||
'get',
|
||||
'create',
|
||||
'update',
|
||||
'observe'
|
||||
]);
|
||||
|
||||
openmct.editor = {};
|
||||
openmct.editor.isEditing = () => false;
|
||||
|
||||
const applicableViews = openmct.objectViews.get(gaugeViewObject, [gaugeViewObject]);
|
||||
gaugeViewProvider = applicableViews.find(viewProvider => viewProvider.key === 'gauge');
|
||||
|
||||
testObjectProvider.get.and.returnValue(Promise.resolve(gaugeViewObject));
|
||||
testObjectProvider.create.and.returnValue(Promise.resolve(gaugeViewObject));
|
||||
openmct.objects.addProvider('test-namespace', testObjectProvider);
|
||||
testObjectProvider.observe.and.returnValue(() => {});
|
||||
testObjectProvider.create.and.returnValue(Promise.resolve(true));
|
||||
testObjectProvider.update.and.returnValue(Promise.resolve(true));
|
||||
|
||||
spyOn(openmct.telemetry, 'getMetadata').and.returnValue({
|
||||
valuesForHints: () => {
|
||||
return [
|
||||
{
|
||||
source: 'sin'
|
||||
}
|
||||
];
|
||||
},
|
||||
value: () => 1
|
||||
});
|
||||
spyOn(openmct.telemetry, 'getValueFormatter').and.returnValue({
|
||||
parse: () => {
|
||||
return 2000;
|
||||
}
|
||||
});
|
||||
spyOn(openmct.telemetry, 'getFormatMap').and.returnValue({
|
||||
sin: {
|
||||
format: (datum) => {
|
||||
return randomValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
spyOn(openmct.telemetry, 'getLimits').and.returnValue(
|
||||
{
|
||||
limits: () => Promise.resolve({
|
||||
CRITICAL: {
|
||||
high: 0.99,
|
||||
low: -0.99
|
||||
}
|
||||
})
|
||||
}
|
||||
);
|
||||
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([randomValue]));
|
||||
spyOn(openmct.time, 'bounds').and.returnValue({
|
||||
start: 1000,
|
||||
end: 5000
|
||||
});
|
||||
|
||||
return openmct.objects.getMutable(gaugeViewObject.identifier).then((mutableObject) => {
|
||||
mutablegaugeObject = mutableObject;
|
||||
gaugeView = gaugeViewProvider.view(mutablegaugeObject);
|
||||
gaugeView.show(child);
|
||||
|
||||
return Vue.nextTick();
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
gaugeView.destroy();
|
||||
|
||||
return resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
it('provides gauge view', () => {
|
||||
expect(gaugeViewProvider).toBeDefined();
|
||||
});
|
||||
|
||||
it('renders gauge element', () => {
|
||||
const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper');
|
||||
expect(gaugeElement.length).toBe(1);
|
||||
});
|
||||
|
||||
it('renders major elements', () => {
|
||||
const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper');
|
||||
const rangeElement = gaugeHolder.querySelector('.js-gauge-dial-range');
|
||||
const valueElement = gaugeHolder.querySelector('.js-dial-current-value');
|
||||
|
||||
const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement);
|
||||
|
||||
expect(hasMajorElements).toBe(true);
|
||||
});
|
||||
|
||||
it('renders correct min max values', () => {
|
||||
expect(gaugeHolder.querySelector('.js-gauge-dial-range').textContent).toEqual(`${gaugeViewObject.configuration.gaugeController.min} ${gaugeViewObject.configuration.gaugeController.max}`);
|
||||
});
|
||||
|
||||
it('renders correct current value', (done) => {
|
||||
function WatchUpdateValue() {
|
||||
const textElement = gaugeHolder.querySelector('.js-dial-current-value');
|
||||
expect(Number(textElement.textContent).toFixed(gaugeViewObject.configuration.gaugeController.precision)).toBe(randomValue.toFixed(gaugeViewObject.configuration.gaugeController.precision));
|
||||
done();
|
||||
}
|
||||
|
||||
const debouncedWatchUpdate = debounce(WatchUpdateValue, 200);
|
||||
Vue.nextTick(debouncedWatchUpdate);
|
||||
});
|
||||
});
|
||||
});
|
67
src/plugins/gauge/GaugeViewProvider.js
Normal file
67
src/plugins/gauge/GaugeViewProvider.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 GaugeComponent from './components/Gauge.vue';
|
||||
import Vue from 'vue';
|
||||
|
||||
export default function GaugeViewProvider(openmct) {
|
||||
return {
|
||||
key: 'gauge',
|
||||
name: 'Gauge',
|
||||
cssClass: 'icon-gauge',
|
||||
canView: function (domainObject) {
|
||||
return domainObject.type === 'gauge';
|
||||
},
|
||||
canEdit: function (domainObject) {
|
||||
if (domainObject.type === 'gauge') {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
view: function (domainObject) {
|
||||
let component;
|
||||
|
||||
return {
|
||||
show: function (element) {
|
||||
component = new Vue({
|
||||
el: element,
|
||||
components: {
|
||||
GaugeComponent
|
||||
},
|
||||
provide: {
|
||||
openmct,
|
||||
domainObject,
|
||||
composition: openmct.composition.get(domainObject)
|
||||
},
|
||||
template: '<gauge-component></gauge-component>'
|
||||
});
|
||||
},
|
||||
destroy: function (element) {
|
||||
component.$destroy();
|
||||
component = undefined;
|
||||
}
|
||||
};
|
||||
},
|
||||
priority: function () {
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
}
|
566
src/plugins/gauge/components/Gauge.vue
Normal file
566
src/plugins/gauge/components/Gauge.vue
Normal file
@ -0,0 +1,566 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
<template>
|
||||
<div
|
||||
class="c-gauge__wrapper js-gauge-wrapper"
|
||||
:class="`c-gauge--${gaugeType}`"
|
||||
>
|
||||
<template v-if="typeDial">
|
||||
<svg
|
||||
width="0"
|
||||
height="0"
|
||||
class="c-dial__clip-paths"
|
||||
>
|
||||
<defs>
|
||||
<clipPath
|
||||
id="gaugeBgMask"
|
||||
clipPathUnits="objectBoundingBox"
|
||||
>
|
||||
<path d="M0.853553 0.853553C0.944036 0.763071 1 0.638071 1 0.5C1 0.223858 0.776142 0 0.5 0C0.223858 0 0 0.223858 0 0.5C0 0.638071 0.0559644 0.763071 0.146447 0.853553L0.285934 0.714066C0.23115 0.659281 0.197266 0.583598 0.197266 0.5C0.197266 0.332804 0.332804 0.197266 0.5 0.197266C0.667196 0.197266 0.802734 0.332804 0.802734 0.5C0.802734 0.583598 0.76885 0.659281 0.714066 0.714066L0.853553 0.853553Z" />
|
||||
</clipPath>
|
||||
<clipPath
|
||||
id="gaugeValueMask"
|
||||
clipPathUnits="objectBoundingBox"
|
||||
>
|
||||
<path d="M0.18926 0.81074C0.109735 0.731215 0.0605469 0.621351 0.0605469 0.5C0.0605469 0.257298 0.257298 0.0605469 0.5 0.0605469C0.742702 0.0605469 0.939453 0.257298 0.939453 0.5C0.939453 0.621351 0.890265 0.731215 0.81074 0.81074L0.714066 0.714066C0.76885 0.659281 0.802734 0.583599 0.802734 0.5C0.802734 0.332804 0.667196 0.197266 0.5 0.197266C0.332804 0.197266 0.197266 0.332804 0.197266 0.5C0.197266 0.583599 0.23115 0.659281 0.285934 0.714066L0.18926 0.81074Z" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<svg
|
||||
class="c-dial__range c-gauge__range js-gauge-dial-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
|
||||
class="c-dial__current-value-text-wrapper"
|
||||
viewBox="0 0 512 512"
|
||||
>
|
||||
<svg
|
||||
v-if="displayCurVal"
|
||||
class="c-dial__current-value-text-sizer"
|
||||
:viewBox="curValViewBox"
|
||||
>
|
||||
<text
|
||||
class="c-dial__current-value-text js-dial-current-value"
|
||||
lengthAdjust="spacing"
|
||||
text-anchor="middle"
|
||||
style="transform: translate(50%, 70%)"
|
||||
>{{ curVal }}</text>
|
||||
</svg>
|
||||
</svg>
|
||||
|
||||
<svg
|
||||
class="c-dial__bg"
|
||||
viewBox="0 0 10 10"
|
||||
>
|
||||
|
||||
<g
|
||||
v-if="limitLow !== null && dialLowLimitDeg < getLimitDegree('low', 'max')"
|
||||
class="c-dial__limit-low"
|
||||
:style="`transform: rotate(${dialLowLimitDeg}deg)`"
|
||||
>
|
||||
<rect
|
||||
v-if="dialLowLimitDeg >= getLimitDegree('low', 'q1')"
|
||||
class="c-dial__low-limit__low"
|
||||
x="5"
|
||||
y="5"
|
||||
width="5"
|
||||
height="5"
|
||||
/>
|
||||
<rect
|
||||
v-if="dialLowLimitDeg >= getLimitDegree('low', 'q2')"
|
||||
class="c-dial__low-limit__mid"
|
||||
x="5"
|
||||
y="0"
|
||||
width="5"
|
||||
height="5"
|
||||
/>
|
||||
<rect
|
||||
v-if="dialLowLimitDeg >= getLimitDegree('low', 'q3')"
|
||||
class="c-dial__low-limit__high"
|
||||
x="0"
|
||||
y="0"
|
||||
width="5"
|
||||
height="5"
|
||||
/>
|
||||
</g>
|
||||
|
||||
<g
|
||||
v-if="limitHigh !== null && dialHighLimitDeg < getLimitDegree('high', 'max')"
|
||||
class="c-dial__limit-high"
|
||||
:style="`transform: rotate(${dialHighLimitDeg}deg)`"
|
||||
>
|
||||
<rect
|
||||
v-if="dialHighLimitDeg <= getLimitDegree('high', 'max')"
|
||||
class="c-dial__high-limit__low"
|
||||
x="0"
|
||||
y="5"
|
||||
width="5"
|
||||
height="5"
|
||||
/>
|
||||
<rect
|
||||
v-if="dialHighLimitDeg <= getLimitDegree('high', 'q2')"
|
||||
class="c-dial__high-limit__mid"
|
||||
x="0"
|
||||
y="0"
|
||||
width="5"
|
||||
height="5"
|
||||
/>
|
||||
<rect
|
||||
v-if="dialHighLimitDeg <= getLimitDegree('high', 'q3')"
|
||||
class="c-dial__high-limit__high"
|
||||
x="5"
|
||||
y="0"
|
||||
width="5"
|
||||
height="5"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<svg
|
||||
v-if="typeFilledDial"
|
||||
class="c-dial__filled-value-wrapper"
|
||||
viewBox="0 0 10 10"
|
||||
>
|
||||
<g
|
||||
class="c-dial__filled-value"
|
||||
:style="`transform: rotate(${degValueFilledDial}deg)`"
|
||||
>
|
||||
<rect
|
||||
v-if="degValue >= getLimitDegree('low', 'q1')"
|
||||
class="c-dial__filled-value__low"
|
||||
x="5"
|
||||
y="5"
|
||||
width="5"
|
||||
height="5"
|
||||
/>
|
||||
<rect
|
||||
v-if="degValue >= getLimitDegree('low', 'q2')"
|
||||
class="c-dial__filled-value__mid"
|
||||
x="5"
|
||||
y="0"
|
||||
width="5"
|
||||
height="5"
|
||||
/>
|
||||
<rect
|
||||
v-if="degValue >= getLimitDegree('low', 'q3')"
|
||||
class="c-dial__filled-value__high"
|
||||
x="0"
|
||||
y="0"
|
||||
width="5"
|
||||
height="5"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<svg
|
||||
v-if="valueInBounds && typeNeedleDial"
|
||||
class="c-dial__needle-value-wrapper"
|
||||
viewBox="0 0 10 10"
|
||||
>
|
||||
<g
|
||||
class="c-dial__needle-value"
|
||||
:style="`transform: rotate(${degValue}deg)`"
|
||||
>
|
||||
<path d="M4.90234 9.39453L5.09766 9.39453L5.30146 8.20874C6.93993 8.05674 8.22265 6.67817 8.22266 5C8.22266 3.22018 6.77982 1.77734 5 1.77734C3.22018 1.77734 1.77734 3.22018 1.77734 5C1.77734 6.67817 3.06007 8.05674 4.69854 8.20874L4.90234 9.39453Z" />
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<template v-if="typeMeter">
|
||||
<div class="c-meter">
|
||||
<div
|
||||
v-if="displayMinMax"
|
||||
class="c-gauge__range c-meter__range js-gauge-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 !== null && meterHighLimitPerc > 0"
|
||||
class="c-meter__limit-high"
|
||||
:style="`height: ${meterHighLimitPerc}%`"
|
||||
></div>
|
||||
|
||||
<div
|
||||
v-if="limitLow !== null && 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 !== null && meterHighLimitPerc > 0"
|
||||
class="c-meter__limit-high"
|
||||
:style="`width: ${meterHighLimitPerc}%`"
|
||||
></div>
|
||||
|
||||
<div
|
||||
v-if="limitLow !== null && meterLowLimitPerc > 0"
|
||||
class="c-meter__limit-low"
|
||||
:style="`width: ${meterLowLimitPerc}%`"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<svg
|
||||
class="c-meter__current-value-text-wrapper"
|
||||
viewBox="0 0 512 512"
|
||||
>
|
||||
<svg
|
||||
v-if="displayCurVal"
|
||||
class="c-meter__current-value-text-sizer"
|
||||
:viewBox="curValViewBox"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
>
|
||||
<text
|
||||
class="c-dial__current-value-text js-meter-current-value"
|
||||
lengthAdjust="spacing"
|
||||
text-anchor="middle"
|
||||
style="transform: translate(50%, 70%)"
|
||||
>{{ curVal }}</text>
|
||||
</svg>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { DIAL_VALUE_DEG_OFFSET, getLimitDegree } from '../gauge-limit-util';
|
||||
|
||||
const LIMIT_PADDING_IN_PERCENT = 10;
|
||||
|
||||
export default {
|
||||
name: 'Gauge',
|
||||
inject: ['openmct', 'domainObject', 'composition'],
|
||||
data() {
|
||||
let gaugeController = this.domainObject.configuration.gaugeController;
|
||||
|
||||
return {
|
||||
curVal: 0,
|
||||
digits: 3,
|
||||
precision: gaugeController.precision,
|
||||
displayMinMax: gaugeController.isDisplayMinMax,
|
||||
displayCurVal: gaugeController.isDisplayCurVal,
|
||||
limitHigh: gaugeController.limitHigh,
|
||||
limitLow: gaugeController.limitLow,
|
||||
rangeHigh: gaugeController.max,
|
||||
rangeLow: gaugeController.min,
|
||||
gaugeType: gaugeController.gaugeType,
|
||||
activeTimeSystem: this.openmct.time.timeSystem()
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
degValue() {
|
||||
return this.percentToDegrees(this.valToPercent(this.curVal));
|
||||
},
|
||||
degValueFilledDial() {
|
||||
if (this.curVal > this.rangeHigh) {
|
||||
return this.percentToDegrees(100);
|
||||
}
|
||||
|
||||
return this.percentToDegrees(this.valToPercent(this.curVal));
|
||||
},
|
||||
dialHighLimitDeg() {
|
||||
return this.percentToDegrees(this.valToPercent(this.limitHigh));
|
||||
},
|
||||
dialLowLimitDeg() {
|
||||
return this.percentToDegrees(this.valToPercent(this.limitLow));
|
||||
},
|
||||
curValViewBox() {
|
||||
const DIGITS_RATIO = 10;
|
||||
const VIEWBOX_STR = '0 0 X 15';
|
||||
|
||||
return VIEWBOX_STR.replace('X', this.digits * DIGITS_RATIO);
|
||||
},
|
||||
typeDial() {
|
||||
return this.matchGaugeType('dial');
|
||||
},
|
||||
typeFilledDial() {
|
||||
return this.matchGaugeType('dial-filled');
|
||||
},
|
||||
typeNeedleDial() {
|
||||
return this.matchGaugeType('dial-needle');
|
||||
},
|
||||
typeMeter() {
|
||||
return this.matchGaugeType('meter');
|
||||
},
|
||||
typeMeterHorizontal() {
|
||||
return this.matchGaugeType('horizontal');
|
||||
},
|
||||
typeMeterVertical() {
|
||||
return this.matchGaugeType('vertical');
|
||||
},
|
||||
typeMeterInverted() {
|
||||
return this.matchGaugeType('inverted');
|
||||
},
|
||||
meterValueToPerc() {
|
||||
const meterDirection = (this.typeMeterInverted) ? -1 : 1;
|
||||
|
||||
if (this.curVal <= this.rangeLow) {
|
||||
return meterDirection * 100;
|
||||
}
|
||||
|
||||
if (this.curVal >= this.rangeHigh) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return this.valToPercentMeter(this.curVal) * meterDirection;
|
||||
},
|
||||
meterHighLimitPerc() {
|
||||
return this.valToPercentMeter(this.limitHigh);
|
||||
},
|
||||
meterLowLimitPerc() {
|
||||
return 100 - this.valToPercentMeter(this.limitLow);
|
||||
},
|
||||
valueInBounds() {
|
||||
return (this.curVal >= this.rangeLow && this.curVal <= this.rangeHigh);
|
||||
},
|
||||
timeFormatter() {
|
||||
const timeSystem = this.activeTimeSystem;
|
||||
const metadataValue = this.metadata.value(timeSystem.key) || { format: timeSystem.key };
|
||||
|
||||
return this.openmct.telemetry.getValueFormatter(metadataValue);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
curVal(newCurValue) {
|
||||
if (this.digits < newCurValue.toString().length) {
|
||||
this.digits = newCurValue.toString().length;
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.composition.on('add', this.addedToComposition);
|
||||
this.composition.on('remove', this.removeTelemetryObject);
|
||||
|
||||
this.composition.load();
|
||||
|
||||
this.openmct.time.on('bounds', this.refreshData);
|
||||
this.openmct.time.on('timeSystem', this.setTimeSystem);
|
||||
},
|
||||
destroyed() {
|
||||
this.composition.off('add', this.addedToComposition);
|
||||
this.composition.off('remove', this.removeTelemetryObject);
|
||||
|
||||
if (this.unsubscribe) {
|
||||
this.unsubscribe();
|
||||
}
|
||||
|
||||
this.openmct.time.off('bounds', this.refreshData);
|
||||
this.openmct.time.off('timeSystem', this.setTimeSystem);
|
||||
},
|
||||
methods: {
|
||||
getLimitDegree: getLimitDegree,
|
||||
addTelemetryObjectAndSubscribe(domainObject) {
|
||||
this.telemetryObject = domainObject;
|
||||
this.request();
|
||||
this.subscribe();
|
||||
},
|
||||
addedToComposition(domainObject) {
|
||||
if (this.telemetryObject) {
|
||||
this.confirmRemoval(domainObject);
|
||||
} else {
|
||||
this.addTelemetryObjectAndSubscribe(domainObject);
|
||||
}
|
||||
},
|
||||
confirmRemoval(domainObject) {
|
||||
const dialog = this.openmct.overlays.dialog({
|
||||
iconClass: 'alert',
|
||||
message: 'This action will replace the current telemetry source. Do you want to continue?',
|
||||
buttons: [
|
||||
{
|
||||
label: 'Ok',
|
||||
emphasis: true,
|
||||
callback: () => {
|
||||
this.removeFromComposition();
|
||||
this.removeTelemetryObject();
|
||||
this.addTelemetryObjectAndSubscribe(domainObject);
|
||||
dialog.dismiss();
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Cancel',
|
||||
callback: () => {
|
||||
this.removeFromComposition(domainObject);
|
||||
dialog.dismiss();
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
},
|
||||
matchGaugeType(str) {
|
||||
return this.gaugeType.indexOf(str) !== -1;
|
||||
},
|
||||
percentToDegrees(vPercent) {
|
||||
return this.round(((vPercent / 100) * 270) + DIAL_VALUE_DEG_OFFSET, 2);
|
||||
},
|
||||
removeFromComposition(telemetryObject = this.telemetryObject) {
|
||||
let composition = this.domainObject.composition.filter(id =>
|
||||
!this.openmct.objects.areIdsEqual(id, telemetryObject.identifier)
|
||||
);
|
||||
|
||||
this.openmct.objects.mutate(this.domainObject, 'composition', composition);
|
||||
},
|
||||
refreshData(bounds, isTick) {
|
||||
if (!isTick) {
|
||||
this.request();
|
||||
}
|
||||
},
|
||||
removeTelemetryObject() {
|
||||
if (this.unsubscribe) {
|
||||
this.unsubscribe();
|
||||
this.unsubscribe = null;
|
||||
}
|
||||
|
||||
this.metadata = null;
|
||||
this.formats = null;
|
||||
this.valueKey = null;
|
||||
this.limitHigh = null;
|
||||
this.limitLow = null;
|
||||
this.rangeHigh = null;
|
||||
this.rangeLow = null;
|
||||
},
|
||||
request(domainObject = this.telemetryObject) {
|
||||
this.metadata = this.openmct.telemetry.getMetadata(domainObject);
|
||||
this.formats = this.openmct.telemetry.getFormatMap(this.metadata);
|
||||
const LimitEvaluator = this.openmct.telemetry.getLimits(domainObject);
|
||||
LimitEvaluator.limits().then(this.updateLimits);
|
||||
|
||||
this.valueKey = this
|
||||
.metadata
|
||||
.valuesForHints(['range'])[0].source;
|
||||
|
||||
this.openmct
|
||||
.telemetry
|
||||
.request(domainObject, { strategy: 'latest' })
|
||||
.then(values => {
|
||||
const length = values.length;
|
||||
this.updateValue(values[length - 1]);
|
||||
});
|
||||
},
|
||||
round(val, decimals = this.precision) {
|
||||
let precision = Math.pow(10, decimals);
|
||||
|
||||
return Math.round(val * precision) / precision;
|
||||
},
|
||||
setTimeSystem(timeSystem) {
|
||||
this.activeTimeSystem = timeSystem;
|
||||
},
|
||||
subscribe(domainObject = this.telemetryObject) {
|
||||
this.unsubscribe = this.openmct
|
||||
.telemetry
|
||||
.subscribe(domainObject, this.updateValue.bind(this));
|
||||
},
|
||||
updateLimits(telemetryLimit) {
|
||||
if (!telemetryLimit || !this.domainObject.configuration.gaugeController.isUseTelemetryLimits) {
|
||||
return;
|
||||
}
|
||||
|
||||
let limits = {
|
||||
high: 0,
|
||||
low: 0
|
||||
};
|
||||
if (telemetryLimit.CRITICAL) {
|
||||
limits = telemetryLimit.CRITICAL;
|
||||
} else if (telemetryLimit.DISTRESS) {
|
||||
limits = telemetryLimit.DISTRESS;
|
||||
} else if (telemetryLimit.SEVERE) {
|
||||
limits = telemetryLimit.SEVERE;
|
||||
} else if (telemetryLimit.WARNING) {
|
||||
limits = telemetryLimit.WARNING;
|
||||
} else if (telemetryLimit.WATCH) {
|
||||
limits = telemetryLimit.WATCH;
|
||||
} else {
|
||||
this.openmct.notifications.error('No limits definition for given telemetry');
|
||||
}
|
||||
|
||||
this.limitHigh = this.round(limits.high[this.valueKey]);
|
||||
this.limitLow = this.round(limits.low[this.valueKey]);
|
||||
this.rangeHigh = this.round(this.limitHigh + this.limitHigh * LIMIT_PADDING_IN_PERCENT / 100);
|
||||
this.rangeLow = this.round(this.limitLow - Math.abs(this.limitLow * LIMIT_PADDING_IN_PERCENT / 100));
|
||||
},
|
||||
updateValue(datum) {
|
||||
this.datum = datum;
|
||||
|
||||
if (this.isRendering) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { start, end } = this.openmct.time.bounds();
|
||||
const parsedValue = this.timeFormatter.parse(this.datum);
|
||||
|
||||
const beforeStartOfBounds = parsedValue < start;
|
||||
const afterEndOfBounds = parsedValue > end;
|
||||
if (afterEndOfBounds || beforeStartOfBounds) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isRendering = true;
|
||||
requestAnimationFrame(() => {
|
||||
this.isRendering = false;
|
||||
|
||||
this.curVal = this.round(this.formats[this.valueKey].format(this.datum), this.precision);
|
||||
});
|
||||
},
|
||||
valToPercent(vValue) {
|
||||
// Used by dial
|
||||
if (vValue >= this.rangeHigh && this.typeFilledDial) {
|
||||
// For filled dial, clip values over the high range to prevent over-rotation
|
||||
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>
|
171
src/plugins/gauge/components/GaugeFormController.vue
Normal file
171
src/plugins/gauge/components/GaugeFormController.vue
Normal file
@ -0,0 +1,171 @@
|
||||
/*****************************************************************************
|
||||
* 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="'gaugeToggle'"
|
||||
:checked="isUseTelemetryLimits"
|
||||
label="Use telemetry limits for minimum and maximum ranges"
|
||||
@change="toggleUseTelemetryLimits"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="!isUseTelemetryLimits"
|
||||
class="c-form--sub-grid"
|
||||
>
|
||||
<div class="c-form__row">
|
||||
<span class="req-indicator req">
|
||||
</span>
|
||||
<label>Range minimum value</label>
|
||||
<input
|
||||
ref="min"
|
||||
v-model.number="min"
|
||||
data-field-name="min"
|
||||
type="number"
|
||||
@input="onChange"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="c-form__row">
|
||||
<span class="req-indicator">
|
||||
</span>
|
||||
<label>Range low limit</label>
|
||||
<input
|
||||
ref="limitLow"
|
||||
v-model.number="limitLow"
|
||||
data-field-name="limitLow"
|
||||
type="number"
|
||||
@input="onChange"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="c-form__row">
|
||||
<span class="req-indicator req">
|
||||
</span>
|
||||
<label>Range maximum value</label>
|
||||
<input
|
||||
ref="max"
|
||||
v-model.number="max"
|
||||
data-field-name="max"
|
||||
type="number"
|
||||
@input="onChange"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="c-form__row">
|
||||
<span class="req-indicator">
|
||||
</span>
|
||||
<label>Range high limit</label>
|
||||
<input
|
||||
ref="limitHigh"
|
||||
v-model.number="limitHigh"
|
||||
data-field-name="limitHigh"
|
||||
type="number"
|
||||
@input="onChange"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ToggleSwitch from '@/ui/components/ToggleSwitch.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ToggleSwitch
|
||||
},
|
||||
props: {
|
||||
model: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isUseTelemetryLimits: this.model.value.isUseTelemetryLimits,
|
||||
isDisplayMinMax: this.model.value.isDisplayMinMax,
|
||||
isDisplayCurVal: this.model.value.isDisplayCurVal,
|
||||
limitHigh: this.model.value.limitHigh,
|
||||
limitLow: this.model.value.limitLow,
|
||||
max: this.model.value.max,
|
||||
min: this.model.value.min
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onChange(event) {
|
||||
const data = {
|
||||
model: this.model,
|
||||
value: {
|
||||
gaugeType: this.model.value.gaugeType,
|
||||
isDisplayMinMax: this.isDisplayMinMax,
|
||||
isDisplayCurVal: this.isDisplayCurVal,
|
||||
isUseTelemetryLimits: this.isUseTelemetryLimits,
|
||||
limitLow: this.limitLow,
|
||||
limitHigh: this.limitHigh,
|
||||
max: this.max,
|
||||
min: this.min,
|
||||
precision: this.model.value.precision
|
||||
}
|
||||
};
|
||||
|
||||
if (event) {
|
||||
const target = event.target;
|
||||
const targetIndicator = target.parentElement.querySelector('.req-indicator');
|
||||
if (targetIndicator.classList.contains('req')) {
|
||||
targetIndicator.classList.add('visited');
|
||||
}
|
||||
|
||||
this.model.validate(data, (valid) => {
|
||||
Object.entries(valid).forEach(([key, isValid]) => {
|
||||
const element = this.$refs[key];
|
||||
const reqIndicatorElement = element.parentElement.querySelector('.req-indicator');
|
||||
reqIndicatorElement.classList.toggle('invalid', !isValid);
|
||||
|
||||
if (reqIndicatorElement.classList.contains('req') && (!isValid || reqIndicatorElement.classList.contains('visited'))) {
|
||||
reqIndicatorElement.classList.toggle('valid', isValid);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
this.$emit('onChange', data);
|
||||
},
|
||||
toggleUseTelemetryLimits() {
|
||||
this.isUseTelemetryLimits = !this.isUseTelemetryLimits;
|
||||
|
||||
this.onChange();
|
||||
},
|
||||
toggleMinMax() {
|
||||
this.isDisplayMinMax = !this.isDisplayMinMax;
|
||||
|
||||
this.onChange();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
39
src/plugins/gauge/gauge-limit-util.js
Normal file
39
src/plugins/gauge/gauge-limit-util.js
Normal file
@ -0,0 +1,39 @@
|
||||
const GAUGE_LIMITS = {
|
||||
q1: 0,
|
||||
q2: 90,
|
||||
q3: 180,
|
||||
q4: 270
|
||||
};
|
||||
|
||||
export const DIAL_VALUE_DEG_OFFSET = 45;
|
||||
|
||||
// type: low, high
|
||||
// quadrant: low, mid, high, max
|
||||
export function getLimitDegree(type, quadrant) {
|
||||
if (quadrant === 'max') {
|
||||
return GAUGE_LIMITS.q4 + DIAL_VALUE_DEG_OFFSET;
|
||||
}
|
||||
|
||||
return type === 'low'
|
||||
? getLowLimitDegree(quadrant)
|
||||
: getHighLimitDegree(quadrant)
|
||||
;
|
||||
}
|
||||
|
||||
function getLowLimitDegree(quadrant) {
|
||||
return GAUGE_LIMITS[quadrant] + DIAL_VALUE_DEG_OFFSET;
|
||||
}
|
||||
|
||||
function getHighLimitDegree(quadrant) {
|
||||
if (quadrant === 'q1') {
|
||||
return GAUGE_LIMITS.q4 + DIAL_VALUE_DEG_OFFSET;
|
||||
}
|
||||
|
||||
if (quadrant === 'q2') {
|
||||
return GAUGE_LIMITS.q3 + DIAL_VALUE_DEG_OFFSET;
|
||||
}
|
||||
|
||||
if (quadrant === 'q3') {
|
||||
return GAUGE_LIMITS.q2 + DIAL_VALUE_DEG_OFFSET;
|
||||
}
|
||||
}
|
223
src/plugins/gauge/gauge.scss
Normal file
223
src/plugins/gauge/gauge.scss
Normal file
@ -0,0 +1,223 @@
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
/********************************************** DIAL GAUGE */
|
||||
svg[class*='c-dial'] {
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
position: absolute;
|
||||
|
||||
g {
|
||||
transform-origin: center;
|
||||
}
|
||||
}
|
||||
|
||||
.c-dial {
|
||||
&__bg {
|
||||
background: $colorGaugeBg;
|
||||
clip-path: url(#gaugeBgMask);
|
||||
}
|
||||
|
||||
&__limit-high rect { fill: $colorGaugeLimitHigh; }
|
||||
&__limit-low rect { fill: $colorGaugeLimitLow; }
|
||||
|
||||
&__filled-value-wrapper {
|
||||
clip-path: url(#gaugeValueMask);
|
||||
}
|
||||
|
||||
&__needle-value-wrapper {
|
||||
clip-path: url(#gaugeValueMask);
|
||||
}
|
||||
|
||||
&__filled-value { fill: $colorGaugeValue; }
|
||||
|
||||
&__needle-value {
|
||||
fill: $colorGaugeValue;
|
||||
transition: transform $transitionTimeGauge;
|
||||
}
|
||||
|
||||
&__current-value-text {
|
||||
fill: $colorGaugeTextValue;
|
||||
font-family: $heroFont;
|
||||
}
|
||||
}
|
||||
|
||||
/********************************************** METER GAUGE */
|
||||
.c-meter {
|
||||
// Common styles for c-meter
|
||||
@include abs();
|
||||
display: flex;
|
||||
|
||||
svg {
|
||||
// current-value-text
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__range {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__bg {
|
||||
background: $colorGaugeBg;
|
||||
border-radius: $basicCr;
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__value {
|
||||
// Filled area
|
||||
position: absolute;
|
||||
background: $colorGaugeValue;
|
||||
transition: transform $transitionTimeGauge;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.c-gauge__curval {
|
||||
fill: $colorGaugeMeterTextValue !important;
|
||||
}
|
||||
|
||||
[class*='limit'] {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
&__limit-high {
|
||||
background: $colorGaugeLimitHigh;
|
||||
}
|
||||
|
||||
&__limit-low {
|
||||
background: $colorGaugeLimitLow;
|
||||
}
|
||||
}
|
||||
|
||||
.c-meter {
|
||||
.c-gauge--meter-vertical &,
|
||||
.c-gauge--meter-vertical-inverted & {
|
||||
&__range {
|
||||
flex-direction: column;
|
||||
min-width: min-content;
|
||||
margin-right: $interiorMarginSm;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
&__value {
|
||||
// Filled area
|
||||
$lrM: $marginGaugeMeterValue;
|
||||
left: $lrM;
|
||||
right: $lrM;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
[class*='limit'] {
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.c-gauge--meter-vertical & {
|
||||
&__limit-low {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
&__limit-high {
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.c-gauge--meter-vertical-inverted & {
|
||||
&__limit-low {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
&__limit-high {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
&__range__low {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
&__range__high {
|
||||
order: 2;
|
||||
}
|
||||
}
|
||||
|
||||
.c-gauge--meter-horizontal & {
|
||||
flex-direction: column;
|
||||
|
||||
&__range {
|
||||
flex-direction: row;
|
||||
min-height: min-content;
|
||||
margin-top: $interiorMarginSm;
|
||||
order: 2;
|
||||
|
||||
&__high {
|
||||
order: 2;
|
||||
}
|
||||
|
||||
&__low {
|
||||
order: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&__bg {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
&__value {
|
||||
// Filled area
|
||||
$m: $marginGaugeMeterValue;
|
||||
top: $m;
|
||||
bottom: $m;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
[class*='limit'] {
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
&__limit-low {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&__limit-high {
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
@ -139,13 +143,13 @@
|
||||
<!-- spacecraft position fresh -->
|
||||
<div
|
||||
v-if="relatedTelemetry.hasRelatedTelemetry && isSpacecraftPositionFresh"
|
||||
class="c-imagery__age icon-check c-imagery--new"
|
||||
class="c-imagery__age icon-check c-imagery--new no-animation"
|
||||
>POS</div>
|
||||
|
||||
<!-- camera position fresh -->
|
||||
<div
|
||||
v-if="relatedTelemetry.hasRelatedTelemetry && isCameraPositionFresh"
|
||||
class="c-imagery__age icon-check c-imagery--new"
|
||||
class="c-imagery__age icon-check c-imagery--new no-animation"
|
||||
>CAM</div>
|
||||
</div>
|
||||
<div class="h-local-controls">
|
||||
@ -159,10 +163,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="displayThumbnails"
|
||||
class="c-imagery__thumbs-wrapper"
|
||||
:class="[
|
||||
{ 'is-paused': isPaused && !isFixed },
|
||||
{ 'is-autoscroll-off': !resizingWindow && !autoScroll && !isPaused }
|
||||
{ 'is-autoscroll-off': !resizingWindow && !autoScroll && !isPaused },
|
||||
{ 'is-small-thumbs': displayThumbnailsSmall },
|
||||
{ 'hide': !displayThumbnails }
|
||||
]"
|
||||
>
|
||||
<div
|
||||
@ -175,6 +182,7 @@
|
||||
:key="image.url + image.time"
|
||||
class="c-imagery__thumb c-thumb"
|
||||
:class="{ selected: focusedImageIndex === index && isPaused }"
|
||||
:title="image.formattedTime"
|
||||
@click="thumbnailClicked(index)"
|
||||
>
|
||||
<a
|
||||
@ -228,6 +236,8 @@ const ARROW_LEFT = 37;
|
||||
const SCROLL_LATENCY = 250;
|
||||
|
||||
const ZOOM_SCALE_DEFAULT = 1;
|
||||
const SHOW_THUMBS_THRESHOLD_HEIGHT = 200;
|
||||
const SHOW_THUMBS_FULLSIZE_THRESHOLD_HEIGHT = 600;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -235,7 +245,7 @@ export default {
|
||||
ImageControls
|
||||
},
|
||||
mixins: [imageryData],
|
||||
inject: ['openmct', 'domainObject', 'objectPath', 'currentView'],
|
||||
inject: ['openmct', 'domainObject', 'objectPath', 'currentView', 'imageFreshnessOptions'],
|
||||
props: {
|
||||
focusedImageTimestamp: {
|
||||
type: Number,
|
||||
@ -268,6 +278,7 @@ export default {
|
||||
imageContainerHeight: undefined,
|
||||
sizedImageWidth: 0,
|
||||
sizedImageHeight: 0,
|
||||
viewHeight: 0,
|
||||
lockCompass: true,
|
||||
resizingWindow: false,
|
||||
timeContext: undefined,
|
||||
@ -286,7 +297,8 @@ export default {
|
||||
imageTranslateY: 0,
|
||||
pan: undefined,
|
||||
animateZoom: true,
|
||||
imagePanned: false
|
||||
imagePanned: false,
|
||||
forceShowThumbnails: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@ -302,6 +314,15 @@ export default {
|
||||
|
||||
return compassRoseSizingClasses;
|
||||
},
|
||||
displayThumbnails() {
|
||||
return (
|
||||
this.forceShowThumbnails
|
||||
|| this.viewHeight >= SHOW_THUMBS_THRESHOLD_HEIGHT
|
||||
);
|
||||
},
|
||||
displayThumbnailsSmall() {
|
||||
return this.viewHeight > SHOW_THUMBS_THRESHOLD_HEIGHT && this.viewHeight <= SHOW_THUMBS_FULLSIZE_THRESHOLD_HEIGHT;
|
||||
},
|
||||
time() {
|
||||
return this.formatTime(this.focusedImage);
|
||||
},
|
||||
@ -310,6 +331,16 @@ export default {
|
||||
},
|
||||
isImageNew() {
|
||||
let cutoff = FIVE_MINUTES;
|
||||
if (this.imageFreshnessOptions) {
|
||||
const { fadeOutDelayTime, fadeOutDurationTime} = this.imageFreshnessOptions;
|
||||
// convert css duration to IS8601 format for parsing
|
||||
const isoFormattedDuration = 'PT' + fadeOutDurationTime.toUpperCase();
|
||||
const isoFormattedDelay = 'PT' + fadeOutDelayTime.toUpperCase();
|
||||
const parsedDuration = moment.duration(isoFormattedDuration).asMilliseconds();
|
||||
const parsedDelay = moment.duration(isoFormattedDelay).asMilliseconds();
|
||||
cutoff = parsedDuration + parsedDelay;
|
||||
}
|
||||
|
||||
let age = this.numericDuration;
|
||||
|
||||
return age < cutoff && !this.refreshCSS;
|
||||
@ -493,6 +524,8 @@ export default {
|
||||
if (!this.isPaused) {
|
||||
this.setFocusedImage(imageIndex);
|
||||
this.scrollToRight();
|
||||
} else {
|
||||
this.scrollToFocused();
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
@ -532,7 +565,7 @@ export default {
|
||||
this.updateRelatedTelemetryForFocusedImage = _.debounce(this.updateRelatedTelemetryForFocusedImage, 400);
|
||||
|
||||
// for resizing the object view
|
||||
this.resizeImageContainer = _.debounce(this.resizeImageContainer, 400);
|
||||
this.resizeImageContainer = _.debounce(this.resizeImageContainer, 400, { leading: true });
|
||||
|
||||
if (this.$refs.imageBG) {
|
||||
this.imageContainerResizeObserver = new ResizeObserver(this.resizeImageContainer);
|
||||
@ -579,6 +612,9 @@ export default {
|
||||
|
||||
},
|
||||
methods: {
|
||||
calculateViewHeight() {
|
||||
this.viewHeight = this.$el.clientHeight;
|
||||
},
|
||||
setTimeContext() {
|
||||
this.stopFollowingTimeContext();
|
||||
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
|
||||
@ -952,6 +988,8 @@ export default {
|
||||
}
|
||||
|
||||
this.setSizedImageDimensions();
|
||||
this.calculateViewHeight();
|
||||
this.scrollToFocused();
|
||||
},
|
||||
setSizedImageDimensions() {
|
||||
this.focusedImageNaturalAspectRatio = this.$refs.focusedImage.naturalWidth / this.$refs.focusedImage.naturalHeight;
|
||||
@ -980,6 +1018,8 @@ export default {
|
||||
this.scrollToRight('reset');
|
||||
}
|
||||
|
||||
this.calculateViewHeight();
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.resizingWindow = false;
|
||||
});
|
||||
|
@ -1,3 +1,15 @@
|
||||
@use 'sass:math';
|
||||
|
||||
@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 +135,20 @@
|
||||
// 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;
|
||||
&.no-animation {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&__thumbs-wrapper {
|
||||
display: flex; // Uses row layout
|
||||
justify-content: flex-end;
|
||||
|
||||
&.is-autoscroll-off {
|
||||
background: $colorInteriorBorder;
|
||||
@ -146,17 +166,11 @@
|
||||
flex: 0 1 auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 135px;
|
||||
height: 145px;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
margin-bottom: 1px;
|
||||
padding-bottom: $interiorMarginSm;
|
||||
|
||||
.c-thumb:last-child {
|
||||
// Hilite the lastest thumb
|
||||
background: $colorBodyFg;
|
||||
color: $colorBodyBg;
|
||||
}
|
||||
}
|
||||
|
||||
&__auto-scroll-resume-button {
|
||||
@ -169,10 +183,12 @@
|
||||
|
||||
/*************************************** THUMBS */
|
||||
.c-thumb {
|
||||
$w: $imageThumbsD;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 4px;
|
||||
width: $imageThumbsD;
|
||||
min-width: $w;
|
||||
width: $w;
|
||||
|
||||
&:hover {
|
||||
background: $colorThumbHoverBg;
|
||||
@ -194,11 +210,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
.l-layout,
|
||||
.c-fl {
|
||||
.is-small-thumbs {
|
||||
.c-imagery__thumbs-scroll-area {
|
||||
// When Imagery is in a layout, hide the thumbs area
|
||||
display: none;
|
||||
height: 60px; // Allow room for scrollbar
|
||||
}
|
||||
|
||||
.c-thumb {
|
||||
$w: math.div($imageThumbsD, 2);
|
||||
min-width: $w;
|
||||
width: $w;
|
||||
|
||||
&__timestamp {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -207,7 +231,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));
|
||||
};
|
||||
}
|
||||
|
@ -344,6 +344,8 @@ describe("The Imagery View Layouts", () => {
|
||||
);
|
||||
openmct.install(clearDataPlugin);
|
||||
clearDataAction = openmct.actions.getAction('clear-data-action');
|
||||
// force show the thumbnails
|
||||
imageryView._getInstance().$children[0].forceShowThumbnails = true;
|
||||
|
||||
return Vue.nextTick();
|
||||
});
|
||||
@ -523,7 +525,10 @@ describe("The Imagery View Layouts", () => {
|
||||
expect(clearDataAction).toBeDefined();
|
||||
});
|
||||
|
||||
it('on clearData action should clear data for object is selected', (done) => {
|
||||
it('on clearData action should clear data for object is selected', async (done) => {
|
||||
// force show the thumbnails
|
||||
imageryView._getInstance().$children[0].forceShowThumbnails = true;
|
||||
await Vue.nextTick();
|
||||
expect(parent.querySelectorAll('.c-imagery__thumb').length).not.toBe(0);
|
||||
openmct.objectViews.on('clearData', async (_domainObject) => {
|
||||
await Vue.nextTick();
|
||||
|
@ -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);
|
||||
},
|
||||
|
@ -20,10 +20,7 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import PerformancePlugin from './plugin.js';
|
||||
import {
|
||||
createOpenMct,
|
||||
resetApplicationState
|
||||
} from 'utils/testing';
|
||||
import { createOpenMct, resetApplicationState } from 'utils/testing';
|
||||
|
||||
describe('the plugin', () => {
|
||||
let openmct;
|
||||
@ -31,9 +28,8 @@ describe('the plugin', () => {
|
||||
let child;
|
||||
|
||||
let performanceIndicator;
|
||||
let countFramesPromise;
|
||||
|
||||
beforeEach((done) => {
|
||||
beforeEach(done => {
|
||||
openmct = createOpenMct();
|
||||
|
||||
element = document.createElement('div');
|
||||
@ -42,11 +38,9 @@ describe('the plugin', () => {
|
||||
|
||||
openmct.install(new PerformancePlugin());
|
||||
|
||||
countFramesPromise = countFrames();
|
||||
|
||||
openmct.on('start', done);
|
||||
|
||||
performanceIndicator = openmct.indicators.indicatorObjects.find((indicator) => {
|
||||
performanceIndicator = openmct.indicators.indicatorObjects.find(indicator => {
|
||||
return indicator.text && indicator.text() === '~ fps';
|
||||
});
|
||||
|
||||
@ -61,25 +55,21 @@ describe('the plugin', () => {
|
||||
expect(performanceIndicator).toBeDefined();
|
||||
});
|
||||
|
||||
it('correctly calculates fps', () => {
|
||||
return countFramesPromise.then((frames) => {
|
||||
expect(performanceIndicator.text()).toEqual(`${frames} fps`);
|
||||
});
|
||||
it('calculates an fps value', async () => {
|
||||
await loopForABit();
|
||||
// eslint-disable-next-line
|
||||
expect(parseInt(performanceIndicator.text().split(' fps')[0])).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
function countFrames() {
|
||||
let startTime = performance.now();
|
||||
function loopForABit() {
|
||||
let frames = 0;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
requestAnimationFrame(function incrementCount() {
|
||||
let now = performance.now();
|
||||
|
||||
if ((now - startTime) < 1000) {
|
||||
frames++;
|
||||
requestAnimationFrame(incrementCount);
|
||||
return new Promise(resolve => {
|
||||
requestAnimationFrame(function loop() {
|
||||
if (++frames === 240) {
|
||||
resolve();
|
||||
} else {
|
||||
resolve(frames);
|
||||
requestAnimationFrame(loop);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -37,6 +37,7 @@
|
||||
v-if="seriesModels.length > 0"
|
||||
:tick-width="tickWidth"
|
||||
:single-series="seriesModels.length === 1"
|
||||
:has-same-range-value="hasSameRangeValue"
|
||||
:series-model="seriesModels[0]"
|
||||
:style="{
|
||||
left: (plotWidth - tickWidth) + 'px'
|
||||
@ -250,7 +251,8 @@ export default {
|
||||
loaded: false,
|
||||
isTimeOutOfSync: false,
|
||||
showLimitLineLabels: undefined,
|
||||
isFrozenOnMouseDown: false
|
||||
isFrozenOnMouseDown: false,
|
||||
hasSameRangeValue: true
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@ -362,6 +364,7 @@ export default {
|
||||
this.setDisplayRange(series, xKey);
|
||||
}, this);
|
||||
this.listenTo(series, 'change:yKey', () => {
|
||||
this.checkSameRangeValue();
|
||||
this.loadSeriesData(series);
|
||||
}, this);
|
||||
|
||||
@ -369,10 +372,18 @@ export default {
|
||||
this.loadSeriesData(series);
|
||||
}, this);
|
||||
|
||||
this.checkSameRangeValue();
|
||||
this.loadSeriesData(series);
|
||||
},
|
||||
|
||||
checkSameRangeValue() {
|
||||
this.hasSameRangeValue = this.seriesModels.every((model) => {
|
||||
return model.get('yKey') === this.seriesModels[0].get('yKey');
|
||||
});
|
||||
},
|
||||
|
||||
removeSeries(plotSeries) {
|
||||
this.checkSameRangeValue();
|
||||
this.stopListening(plotSeries);
|
||||
},
|
||||
|
||||
@ -488,7 +499,7 @@ export default {
|
||||
},
|
||||
|
||||
setDisplayRange(series, xKey) {
|
||||
if (this.config.series.length !== 1) {
|
||||
if (this.config.series.models.length !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -30,8 +30,8 @@
|
||||
class="gl-plot-tick-wrapper"
|
||||
>
|
||||
<div
|
||||
v-for="tick in ticks"
|
||||
:key="tick.value"
|
||||
v-for="(tick, i) in ticks"
|
||||
:key="'tick-left' + i"
|
||||
class="gl-plot-tick gl-plot-x-tick-label"
|
||||
:style="{
|
||||
left: (100 * (tick.value - min) / interval) + '%'
|
||||
@ -46,8 +46,8 @@
|
||||
class="gl-plot-tick-wrapper"
|
||||
>
|
||||
<div
|
||||
v-for="tick in ticks"
|
||||
:key="tick.value"
|
||||
v-for="(tick, i) in ticks"
|
||||
:key="'tick-top' + i"
|
||||
class="gl-plot-tick gl-plot-y-tick-label"
|
||||
:style="{ top: (100 * (max - tick.value) / interval) + '%' }"
|
||||
:title="tick.fullText || tick.text"
|
||||
@ -59,8 +59,8 @@
|
||||
<!-- grid lines follow -->
|
||||
<template v-if="position === 'right'">
|
||||
<div
|
||||
v-for="tick in ticks"
|
||||
:key="tick.value"
|
||||
v-for="(tick, i) in ticks"
|
||||
:key="'tick-right' + i"
|
||||
class="gl-plot-hash hash-v"
|
||||
:style="{
|
||||
right: (100 * (max - tick.value) / interval) + '%',
|
||||
@ -71,8 +71,8 @@
|
||||
</template>
|
||||
<template v-if="position === 'bottom'">
|
||||
<div
|
||||
v-for="tick in ticks"
|
||||
:key="tick.value"
|
||||
v-for="(tick, i) in ticks"
|
||||
:key="'tick-bottom' + i"
|
||||
class="gl-plot-hash hash-h"
|
||||
:style="{ bottom: (100 * (tick.value - min) / interval) + '%', width: '100%' }"
|
||||
>
|
||||
@ -83,7 +83,7 @@
|
||||
|
||||
<script>
|
||||
import eventHelpers from "./lib/eventHelpers";
|
||||
import { ticks, getFormattedTicks } from "./tickUtils";
|
||||
import { ticks, getLogTicks, getFormattedTicks } from "./tickUtils";
|
||||
import configStore from "./configuration/ConfigStore";
|
||||
|
||||
export default {
|
||||
@ -96,6 +96,13 @@ export default {
|
||||
},
|
||||
required: true
|
||||
},
|
||||
// Make it a prop, then later we can allow user to change it via UI input
|
||||
tickCount: {
|
||||
type: Number,
|
||||
default() {
|
||||
return 6;
|
||||
}
|
||||
},
|
||||
position: {
|
||||
required: true,
|
||||
type: String,
|
||||
@ -118,7 +125,6 @@ export default {
|
||||
|
||||
this.axis = this.getAxisFromConfig();
|
||||
|
||||
this.tickCount = 4;
|
||||
this.tickUpdate = false;
|
||||
this.listenTo(this.axis, 'change:displayRange', this.updateTicks, this);
|
||||
this.listenTo(this.axis, 'change:format', this.updateTicks, this);
|
||||
@ -184,7 +190,12 @@ export default {
|
||||
}, this);
|
||||
}
|
||||
|
||||
return ticks(range.min, range.max, number);
|
||||
if (this.axisType === 'yAxis' && this.axis.get('logMode')) {
|
||||
return getLogTicks(range.min, range.max, number, 4);
|
||||
// return getLogTicks2(range.min, range.max, number);
|
||||
} else {
|
||||
return ticks(range.min, range.max, number);
|
||||
}
|
||||
},
|
||||
|
||||
updateTicksForceRegeneration() {
|
||||
@ -193,6 +204,8 @@ export default {
|
||||
|
||||
updateTicks(forceRegeneration = false) {
|
||||
const range = this.axis.get('displayRange');
|
||||
const logMode = this.axis.get('logMode');
|
||||
|
||||
if (!range) {
|
||||
delete this.min;
|
||||
delete this.max;
|
||||
@ -220,7 +233,7 @@ export default {
|
||||
step: newTicks[1] - newTicks[0]
|
||||
};
|
||||
|
||||
newTicks = getFormattedTicks(newTicks, format);
|
||||
newTicks = getFormattedTicks(newTicks, format, logMode);
|
||||
|
||||
this.ticks = newTicks;
|
||||
this.shouldCheckWidth = true;
|
||||
|
@ -29,9 +29,9 @@
|
||||
>
|
||||
|
||||
<div
|
||||
v-if="singleSeries"
|
||||
v-if="canShowYAxisLabel"
|
||||
class="gl-plot-label gl-plot-y-label"
|
||||
:class="{'icon-gear': (yKeyOptions.length > 1)}"
|
||||
:class="{'icon-gear': (yKeyOptions.length > 1 && singleSeries)}"
|
||||
>{{ yAxisLabel }}
|
||||
</div>
|
||||
|
||||
@ -76,6 +76,12 @@ export default {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
hasSameRangeValue: {
|
||||
type: Boolean,
|
||||
default() {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
seriesModel: {
|
||||
type: Object,
|
||||
default() {
|
||||
@ -95,6 +101,11 @@ export default {
|
||||
loaded: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
canShowYAxisLabel() {
|
||||
return this.singleSeries === true || this.hasSameRangeValue === true;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.yAxis = this.getYAxisFromConfig();
|
||||
this.loaded = true;
|
||||
|
@ -23,7 +23,7 @@
|
||||
import MCTChartSeriesElement from './MCTChartSeriesElement';
|
||||
|
||||
export default class MCTChartLineStepAfter extends MCTChartSeriesElement {
|
||||
removePoint(point, index, count) {
|
||||
removePoint(index) {
|
||||
if (index > 0 && index / 2 < this.count) {
|
||||
this.buffer[index + 1] = this.buffer[index - 1];
|
||||
}
|
||||
|
@ -85,11 +85,10 @@ export default class MCTChartSeriesElement {
|
||||
|
||||
this.removeSegments(removalPoint, vertexCount);
|
||||
|
||||
this.removePoint(
|
||||
this.makePoint(point, series),
|
||||
removalPoint,
|
||||
vertexCount
|
||||
);
|
||||
// TODO useless makePoint call?
|
||||
this.makePoint(point, series);
|
||||
this.removePoint(removalPoint);
|
||||
|
||||
this.count -= (vertexCount / 2);
|
||||
}
|
||||
|
||||
@ -109,11 +108,7 @@ export default class MCTChartSeriesElement {
|
||||
const insertionPoint = this.startIndexForPointAtIndex(index);
|
||||
this.growIfNeeded(pointsRequired);
|
||||
this.makeInsertionPoint(insertionPoint, pointsRequired);
|
||||
this.addPoint(
|
||||
this.makePoint(point, series),
|
||||
insertionPoint,
|
||||
pointsRequired
|
||||
);
|
||||
this.addPoint(this.makePoint(point, series), insertionPoint);
|
||||
this.count += (pointsRequired / 2);
|
||||
}
|
||||
|
||||
|
@ -71,6 +71,7 @@ export default class Model extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* @abstract
|
||||
* @param {ModelOptions<T, O>} options
|
||||
*/
|
||||
initialize(options) {
|
||||
|
@ -157,7 +157,8 @@ export default class PlotConfigurationModel extends Model {
|
||||
@typedef {{
|
||||
configuration: {
|
||||
series: import('./PlotSeries').PlotSeriesModelType[]
|
||||
}
|
||||
yAxis: import('./YAxisModel').YAxisModelType
|
||||
},
|
||||
}} SomeDomainObject_NeedsName
|
||||
*/
|
||||
|
||||
|
@ -23,6 +23,7 @@ import _ from 'lodash';
|
||||
import Model from "./Model";
|
||||
import { MARKER_SHAPES } from '../draw/MarkerShapes';
|
||||
import configStore from "../configuration/ConfigStore";
|
||||
import { symlog } from '../mathUtils';
|
||||
|
||||
/**
|
||||
* Plot series handle interpreting telemetry metadata for a single telemetry
|
||||
@ -63,6 +64,8 @@ import configStore from "../configuration/ConfigStore";
|
||||
* @extends {Model<PlotSeriesModelType, PlotSeriesModelOptions>}
|
||||
*/
|
||||
export default class PlotSeries extends Model {
|
||||
logMode = false;
|
||||
|
||||
/**
|
||||
@param {import('./Model').ModelOptions<PlotSeriesModelType, PlotSeriesModelOptions>} options
|
||||
*/
|
||||
@ -70,6 +73,8 @@ export default class PlotSeries extends Model {
|
||||
|
||||
super(options);
|
||||
|
||||
this.logMode = options.collection.plot.model.yAxis.logMode;
|
||||
|
||||
this.listenTo(this, 'change:xKey', this.onXKeyChange, this);
|
||||
this.listenTo(this, 'change:yKey', this.onYKeyChange, this);
|
||||
this.persistedConfig = options.persistedConfig;
|
||||
@ -229,6 +234,7 @@ export default class PlotSeries extends Model {
|
||||
this.getXVal = format.parse.bind(format);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update y formatter on change, default to stepAfter interpolation if
|
||||
* y range is an enumeration.
|
||||
@ -250,8 +256,13 @@ export default class PlotSeries extends Model {
|
||||
this.evaluate = function (datum) {
|
||||
return this.limitEvaluator.evaluate(datum, valueMetadata);
|
||||
}.bind(this);
|
||||
this.set('unit', valueMetadata.unit);
|
||||
const format = this.formats[newKey];
|
||||
this.getYVal = format.parse.bind(format);
|
||||
this.getYVal = (value) => {
|
||||
const y = format.parse(value);
|
||||
|
||||
return this.logMode ? symlog(y, 10) : y;
|
||||
};
|
||||
}
|
||||
|
||||
formatX(point) {
|
||||
@ -519,7 +530,8 @@ export default class PlotSeries extends Model {
|
||||
|
||||
/**
|
||||
* Update the series data with the given value.
|
||||
* @returns {Array<{
|
||||
* This return type definition is totally wrong, only covers sinwave generator. It needs to be generic.
|
||||
* @return-example {Array<{
|
||||
cos: number
|
||||
sin: number
|
||||
mctLimitState: {
|
||||
|
@ -19,7 +19,7 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import Model from "./Model";
|
||||
import Model from './Model';
|
||||
|
||||
/**
|
||||
* @extends {Model<XAxisModelType, XAxisModelOptions>}
|
||||
@ -49,11 +49,11 @@ export default class XAxisModel extends Model {
|
||||
}
|
||||
});
|
||||
|
||||
this.on('change:frozen', ((frozen) => {
|
||||
this.on('change:frozen', (frozen) => {
|
||||
if (!frozen) {
|
||||
this.set('range', this.get('range'));
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
if (this.get('range')) {
|
||||
this.set('range', this.get('range'));
|
||||
@ -126,7 +126,7 @@ export default class XAxisModel extends Model {
|
||||
|
||||
/**
|
||||
@typedef {import("./Model").ModelType<{
|
||||
range: NumberRange
|
||||
range?: NumberRange
|
||||
displayRange: NumberRange
|
||||
frozen: boolean
|
||||
label: string
|
||||
|
@ -19,7 +19,7 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import _ from 'lodash';
|
||||
import { antisymlog, symlog } from '../mathUtils';
|
||||
import Model from './Model';
|
||||
|
||||
/**
|
||||
@ -31,7 +31,7 @@ import Model from './Model';
|
||||
*
|
||||
* `autoscale`: boolean, whether or not to autoscale.
|
||||
* `autoscalePadding`: float, percent of padding to display in plots.
|
||||
* `displayRange`: the current display range for the x Axis.
|
||||
* `displayRange`: the current display range for the axis.
|
||||
* `format`: the formatter for the axis.
|
||||
* `frozen`: boolean, if true, displayRange will not be updated automatically.
|
||||
* Used to temporarily disable automatic updates during user interaction.
|
||||
@ -54,6 +54,7 @@ export default class YAxisModel extends Model {
|
||||
this.listenTo(this, 'change:stats', this.calculateAutoscaleExtents, this);
|
||||
this.listenTo(this, 'change:autoscale', this.toggleAutoscale, this);
|
||||
this.listenTo(this, 'change:autoscalePadding', this.updatePadding, this);
|
||||
this.listenTo(this, 'change:logMode', this.onLogModeChange, this);
|
||||
this.listenTo(this, 'change:frozen', this.toggleFreeze, this);
|
||||
this.listenTo(this, 'change:range', this.updateDisplayRange, this);
|
||||
this.updateDisplayRange(this.get('range'));
|
||||
@ -63,22 +64,17 @@ export default class YAxisModel extends Model {
|
||||
*/
|
||||
listenToSeriesCollection(seriesCollection) {
|
||||
this.seriesCollection = seriesCollection;
|
||||
this.listenTo(this.seriesCollection, 'add', (series => {
|
||||
this.listenTo(this.seriesCollection, 'add', series => {
|
||||
this.trackSeries(series);
|
||||
this.updateFromSeries(this.seriesCollection);
|
||||
}), this);
|
||||
this.listenTo(this.seriesCollection, 'remove', (series => {
|
||||
}, this);
|
||||
this.listenTo(this.seriesCollection, 'remove', series => {
|
||||
this.untrackSeries(series);
|
||||
this.updateFromSeries(this.seriesCollection);
|
||||
}), this);
|
||||
}, this);
|
||||
this.seriesCollection.forEach(this.trackSeries, this);
|
||||
this.updateFromSeries(this.seriesCollection);
|
||||
}
|
||||
updateDisplayRange(range) {
|
||||
if (!this.get('autoscale')) {
|
||||
this.set('displayRange', range);
|
||||
}
|
||||
}
|
||||
toggleFreeze(frozen) {
|
||||
if (!frozen) {
|
||||
this.toggleAutoscale(this.get('autoscale'));
|
||||
@ -140,11 +136,11 @@ export default class YAxisModel extends Model {
|
||||
}
|
||||
resetStats() {
|
||||
this.unset('stats');
|
||||
this.seriesCollection.forEach(function (series) {
|
||||
this.seriesCollection.forEach(series => {
|
||||
if (series.has('stats')) {
|
||||
this.updateStats(series.get('stats'));
|
||||
}
|
||||
}, this);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @param {import('./PlotSeries').default} series
|
||||
@ -166,12 +162,95 @@ export default class YAxisModel extends Model {
|
||||
this.resetStats();
|
||||
this.updateFromSeries(this.seriesCollection);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is called in order to map the user-provided `range` to the
|
||||
* `displayRange` that we actually use for plot display.
|
||||
*
|
||||
* @param {import('./XAxisModel').NumberRange} range
|
||||
*/
|
||||
updateDisplayRange(range) {
|
||||
if (this.get('autoscale')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const _range = { ...range };
|
||||
|
||||
if (this.get('logMode')) {
|
||||
_range.min = symlog(range.min, 10);
|
||||
_range.max = symlog(range.max, 10);
|
||||
}
|
||||
|
||||
this.set('displayRange', _range);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} autoscale
|
||||
*/
|
||||
toggleAutoscale(autoscale) {
|
||||
if (autoscale && this.has('stats')) {
|
||||
this.set('displayRange', this.applyPadding(this.get('stats')));
|
||||
} else {
|
||||
this.set('displayRange', this.get('range'));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const range = this.get('range');
|
||||
|
||||
if (range) {
|
||||
// If we already have a user-defined range, make sure it maps to the
|
||||
// range we'll actually use for the ticks.
|
||||
|
||||
const _range = { ...range };
|
||||
|
||||
if (this.get('logMode')) {
|
||||
_range.min = symlog(range.min, 10);
|
||||
_range.max = symlog(range.max, 10);
|
||||
}
|
||||
|
||||
this.set('displayRange', _range);
|
||||
} else {
|
||||
// Otherwise use the last known displayRange as the initial
|
||||
// values for the user-defined range, so that we don't end up
|
||||
// with any error from an undefined user range.
|
||||
|
||||
const _range = this.get('displayRange');
|
||||
|
||||
if (!_range) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.get('logMode')) {
|
||||
_range.min = antisymlog(_range.min, 10);
|
||||
_range.max = antisymlog(_range.max, 10);
|
||||
}
|
||||
|
||||
this.set('range', _range);
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {boolean} logMode */
|
||||
onLogModeChange(logMode) {
|
||||
const range = this.get('displayRange');
|
||||
|
||||
if (logMode) {
|
||||
range.min = symlog(range.min, 10);
|
||||
range.max = symlog(range.max, 10);
|
||||
} else {
|
||||
range.min = antisymlog(range.min, 10);
|
||||
range.max = antisymlog(range.max, 10);
|
||||
}
|
||||
|
||||
this.set('displayRange', range);
|
||||
|
||||
this.resetSeries();
|
||||
}
|
||||
resetSeries() {
|
||||
this.plot.series.forEach((plotSeries) => {
|
||||
plotSeries.logMode = this.get('logMode');
|
||||
plotSeries.reset(plotSeries.getSeriesData());
|
||||
});
|
||||
// Update the series collection labels and formatting
|
||||
this.updateFromSeries(this.seriesCollection);
|
||||
}
|
||||
/**
|
||||
* Update yAxis format, values, and label from known series.
|
||||
@ -179,7 +258,7 @@ export default class YAxisModel extends Model {
|
||||
*/
|
||||
updateFromSeries(seriesCollection) {
|
||||
const plotModel = this.plot.get('domainObject');
|
||||
const label = _.get(plotModel, 'configuration.yAxis.label');
|
||||
const label = plotModel.configuration?.yAxis?.label;
|
||||
const sampleSeries = seriesCollection.first();
|
||||
if (!sampleSeries) {
|
||||
if (!label) {
|
||||
@ -192,22 +271,28 @@ export default class YAxisModel extends Model {
|
||||
const yKey = sampleSeries.get('yKey');
|
||||
const yMetadata = sampleSeries.metadata.value(yKey);
|
||||
const yFormat = sampleSeries.formats[yKey];
|
||||
this.set('format', yFormat.format.bind(yFormat));
|
||||
|
||||
if (this.get('logMode')) {
|
||||
this.set('format', (n) => yFormat.format(antisymlog(n, 10)));
|
||||
} else {
|
||||
this.set('format', (n) => yFormat.format(n));
|
||||
}
|
||||
|
||||
this.set('values', yMetadata.values);
|
||||
if (!label) {
|
||||
const labelName = seriesCollection.map(function (s) {
|
||||
return s.metadata ? s.metadata.value(s.get('yKey')).name : '';
|
||||
}).reduce(function (a, b) {
|
||||
if (a === undefined) {
|
||||
return b;
|
||||
}
|
||||
const labelName = seriesCollection
|
||||
.map(s => (s.metadata ? s.metadata.value(s.get('yKey')).name : ''))
|
||||
.reduce((a, b) => {
|
||||
if (a === undefined) {
|
||||
return b;
|
||||
}
|
||||
|
||||
if (a === b) {
|
||||
return a;
|
||||
}
|
||||
if (a === b) {
|
||||
return a;
|
||||
}
|
||||
|
||||
return '';
|
||||
}, undefined);
|
||||
return '';
|
||||
}, undefined);
|
||||
|
||||
if (labelName) {
|
||||
this.set('label', labelName);
|
||||
@ -215,19 +300,19 @@ export default class YAxisModel extends Model {
|
||||
return;
|
||||
}
|
||||
|
||||
const labelUnits = seriesCollection.map(function (s) {
|
||||
return s.metadata ? s.metadata.value(s.get('yKey')).units : '';
|
||||
}).reduce(function (a, b) {
|
||||
if (a === undefined) {
|
||||
return b;
|
||||
}
|
||||
const labelUnits = seriesCollection
|
||||
.map(s => (s.metadata ? s.metadata.value(s.get('yKey')).units : ''))
|
||||
.reduce((a, b) => {
|
||||
if (a === undefined) {
|
||||
return b;
|
||||
}
|
||||
|
||||
if (a === b) {
|
||||
return a;
|
||||
}
|
||||
if (a === b) {
|
||||
return a;
|
||||
}
|
||||
|
||||
return '';
|
||||
}, undefined);
|
||||
return '';
|
||||
}, undefined);
|
||||
|
||||
if (labelUnits) {
|
||||
this.set('label', labelUnits);
|
||||
@ -239,14 +324,18 @@ export default class YAxisModel extends Model {
|
||||
/**
|
||||
* @override
|
||||
* @param {import('./Model').ModelOptions<YAxisModelType, YAxisModelOptions>} options
|
||||
* @returns {YAxisModelType}
|
||||
* @returns {Partial<YAxisModelType>}
|
||||
*/
|
||||
defaultModel(options) {
|
||||
// @ts-ignore incomplete YAxisModelType object used for default value.
|
||||
return {
|
||||
frozen: false,
|
||||
autoscale: true,
|
||||
logMode: options.model?.logMode ?? false,
|
||||
autoscalePadding: 0.1
|
||||
|
||||
// 'range' is not specified here, it is undefined at first. When the
|
||||
// user turns off autoscale, the current 'displayRange' is used for
|
||||
// the initial value of 'range'.
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -256,8 +345,9 @@ export default class YAxisModel extends Model {
|
||||
/**
|
||||
@typedef {import('./XAxisModel').AxisModelType & {
|
||||
autoscale: boolean
|
||||
logMode: boolean
|
||||
autoscalePadding: number
|
||||
stats: import('./XAxisModel').NumberRange
|
||||
stats?: import('./XAxisModel').NumberRange
|
||||
values: Array<TODO>
|
||||
}} YAxisModelType
|
||||
*/
|
||||
|
@ -48,11 +48,19 @@
|
||||
<li class="grid-row">
|
||||
<div
|
||||
class="grid-cell label"
|
||||
title="Automatically scale the Y axis to keep all values in view."
|
||||
>Autoscale</div>
|
||||
title="Enable log mode."
|
||||
>Log mode</div>
|
||||
<div class="grid-cell value">
|
||||
{{ autoscale ? "Enabled: " : "Disabled" }}
|
||||
{{ autoscale ? autoscalePadding : "" }}
|
||||
{{ logMode ? "Enabled" : "Disabled" }}
|
||||
</div>
|
||||
</li>
|
||||
<li class="grid-row">
|
||||
<div
|
||||
class="grid-cell label"
|
||||
title="Automatically scale the Y axis to keep all values in view."
|
||||
>Auto scale</div>
|
||||
<div class="grid-cell value">
|
||||
{{ autoscale ? "Enabled: " + autoscalePadding : "Disabled" }}
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
@ -142,6 +150,7 @@ export default {
|
||||
config: {},
|
||||
label: '',
|
||||
autoscale: '',
|
||||
logMode: false,
|
||||
autoscalePadding: '',
|
||||
rangeMin: '',
|
||||
rangeMax: '',
|
||||
@ -172,6 +181,7 @@ export default {
|
||||
initConfiguration() {
|
||||
this.label = this.config.yAxis.get('label');
|
||||
this.autoscale = this.config.yAxis.get('autoscale');
|
||||
this.logMode = this.config.yAxis.get('logMode');
|
||||
this.autoscalePadding = this.config.yAxis.get('autoscalePadding');
|
||||
const range = this.config.yAxis.get('range');
|
||||
if (range) {
|
||||
|
@ -14,9 +14,22 @@
|
||||
@change="updateForm('label')"
|
||||
></div>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="l-inspector-part">
|
||||
<h2>Y Axis Scaling</h2>
|
||||
<li class="grid-row">
|
||||
<div
|
||||
class="grid-cell label"
|
||||
title="Enable log mode."
|
||||
>
|
||||
Log mode
|
||||
</div>
|
||||
<div class="grid-cell value">
|
||||
<!-- eslint-disable-next-line vue/html-self-closing -->
|
||||
<input
|
||||
v-model="logMode"
|
||||
type="checkbox"
|
||||
@change="updateForm('logMode')"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
<li class="grid-row">
|
||||
<div
|
||||
class="grid-cell label"
|
||||
@ -52,10 +65,10 @@
|
||||
class="l-inspector-part"
|
||||
>
|
||||
<div
|
||||
v-show="!autoscale && validation.range"
|
||||
v-show="!autoscale && validationErrors.range"
|
||||
class="grid-span-all form-error"
|
||||
>
|
||||
{{ validation.range }}
|
||||
{{ validationErrors.range }}
|
||||
</div>
|
||||
<li class="grid-row force-border">
|
||||
<div
|
||||
@ -88,7 +101,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { objectPath, validate, coerce } from "./formUtil";
|
||||
import { objectPath } from "./formUtil";
|
||||
import _ from "lodash";
|
||||
|
||||
export default {
|
||||
@ -105,10 +118,11 @@ export default {
|
||||
return {
|
||||
label: '',
|
||||
autoscale: '',
|
||||
logMode: false,
|
||||
autoscalePadding: '',
|
||||
rangeMin: '',
|
||||
rangeMax: '',
|
||||
validation: {}
|
||||
validationErrors: {}
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
@ -117,38 +131,35 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
initialize: function () {
|
||||
this.fields = [
|
||||
{
|
||||
modelProp: 'label',
|
||||
this.fields = {
|
||||
label: {
|
||||
objectPath: 'configuration.yAxis.label'
|
||||
},
|
||||
{
|
||||
modelProp: 'autoscale',
|
||||
autoscale: {
|
||||
coerce: Boolean,
|
||||
objectPath: 'configuration.yAxis.autoscale'
|
||||
},
|
||||
{
|
||||
modelProp: 'autoscalePadding',
|
||||
autoscalePadding: {
|
||||
coerce: Number,
|
||||
objectPath: 'configuration.yAxis.autoscalePadding'
|
||||
},
|
||||
{
|
||||
modelProp: 'range',
|
||||
logMode: {
|
||||
coerce: Boolean,
|
||||
objectPath: 'configuration.yAxis.logMode'
|
||||
},
|
||||
range: {
|
||||
objectPath: 'configuration.yAxis.range',
|
||||
coerce: function coerceRange(range) {
|
||||
if (!range) {
|
||||
return {
|
||||
min: 0,
|
||||
max: 0
|
||||
};
|
||||
}
|
||||
const newRange = {
|
||||
min: -1,
|
||||
max: 1
|
||||
};
|
||||
|
||||
const newRange = {};
|
||||
if (typeof range.min !== 'undefined' && range.min !== null) {
|
||||
if (range && typeof range.min !== 'undefined' && range.min !== null) {
|
||||
newRange.min = Number(range.min);
|
||||
}
|
||||
|
||||
if (typeof range.max !== 'undefined' && range.max !== null) {
|
||||
if (range && typeof range.max !== 'undefined' && range.max !== null) {
|
||||
newRange.max = Number(range.max);
|
||||
}
|
||||
|
||||
@ -178,28 +189,18 @@ export default {
|
||||
if (Number(range.min) > Number(range.max)) {
|
||||
return 'Minimum must be less than Maximum.';
|
||||
}
|
||||
|
||||
if (model.get('autoscale')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
];
|
||||
};
|
||||
},
|
||||
initFormValues() {
|
||||
this.label = this.yAxis.get('label');
|
||||
this.autoscale = this.yAxis.get('autoscale');
|
||||
this.logMode = this.yAxis.get('logMode');
|
||||
this.autoscalePadding = this.yAxis.get('autoscalePadding');
|
||||
const range = this.yAxis.get('range');
|
||||
if (!range) {
|
||||
this.rangeMin = undefined;
|
||||
this.rangeMax = undefined;
|
||||
} else {
|
||||
this.rangeMin = range.min;
|
||||
this.rangeMax = range.max;
|
||||
}
|
||||
const range = this.yAxis.get('range') ?? this.yAxis.get('displayRange');
|
||||
this.rangeMin = range?.min;
|
||||
this.rangeMax = range?.max;
|
||||
},
|
||||
updateForm(formKey) {
|
||||
let newVal;
|
||||
@ -212,26 +213,27 @@ export default {
|
||||
newVal = this[formKey];
|
||||
}
|
||||
|
||||
const oldVal = this.yAxis.get(formKey);
|
||||
const formField = this.fields.find((field) => field.modelProp === formKey);
|
||||
|
||||
const path = objectPath(formField.objectPath);
|
||||
const validationResult = validate(newVal, this.yAxis, formField.validate);
|
||||
if (validationResult === true) {
|
||||
delete this.validation[formKey];
|
||||
} else {
|
||||
this.validation[formKey] = validationResult;
|
||||
let oldVal = this.yAxis.get(formKey);
|
||||
const formField = this.fields[formKey];
|
||||
|
||||
const validationError = formField.validate?.(newVal, this.yAxis);
|
||||
this.validationErrors[formKey] = validationError;
|
||||
if (validationError) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_.isEqual(coerce(newVal, formField.coerce), coerce(oldVal, formField.coerce))) {
|
||||
this.yAxis.set(formKey, coerce(newVal, formField.coerce));
|
||||
newVal = formField.coerce?.(newVal) ?? newVal;
|
||||
oldVal = formField.coerce?.(oldVal) ?? oldVal;
|
||||
|
||||
const path = objectPath(formField.objectPath);
|
||||
if (!_.isEqual(newVal, oldVal)) {
|
||||
// TODO: Why do we mutate yAxis twice, once directly, once via objects.mutate? Or are they different objects?
|
||||
this.yAxis.set(formKey, newVal);
|
||||
if (path) {
|
||||
this.openmct.objects.mutate(
|
||||
this.domainObject,
|
||||
path(this.domainObject, this.yAxis),
|
||||
coerce(newVal, formField.coerce)
|
||||
newVal
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -15,15 +15,5 @@ export function validate(value, model, validateFunc) {
|
||||
}
|
||||
|
||||
export function objectPath(path) {
|
||||
if (path) {
|
||||
if (typeof path !== "function") {
|
||||
const staticObjectPath = path;
|
||||
|
||||
return function (object, model) {
|
||||
return staticObjectPath;
|
||||
};
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
return path && typeof path !== 'function' ? () => path : path;
|
||||
}
|
||||
|
44
src/plugins/plot/mathUtils.js
Normal file
44
src/plugins/plot/mathUtils.js
Normal file
@ -0,0 +1,44 @@
|
||||
/** The natural number `e`. */
|
||||
export const e = Math.exp(1);
|
||||
|
||||
/**
|
||||
Returns the logarithm of a number, using the given base or the natural number
|
||||
`e` as base if not specified.
|
||||
@param {number} n
|
||||
@param {number=} base log base, defaults to e
|
||||
*/
|
||||
export function log(n, base = e) {
|
||||
if (base === e) {
|
||||
return Math.log(n);
|
||||
}
|
||||
|
||||
return Math.log(n) / Math.log(base);
|
||||
}
|
||||
|
||||
/**
|
||||
Returns the inverse of the logarithm of a number, using the given base or the
|
||||
natural number `e` as base if not specified.
|
||||
@param {number} n
|
||||
@param {number=} base log base, defaults to e
|
||||
*/
|
||||
export function antilog(n, base = e) {
|
||||
return Math.pow(base, n);
|
||||
}
|
||||
|
||||
/**
|
||||
A symmetric logarithm function. See https://github.com/nasa/openmct/issues/2297#issuecomment-1032914258
|
||||
@param {number} n
|
||||
@param {number=} base log base, defaults to e
|
||||
*/
|
||||
export function symlog(n, base = e) {
|
||||
return Math.sign(n) * log(Math.abs(n) + 1, base);
|
||||
}
|
||||
|
||||
/**
|
||||
An inverse symmetric logarithm function. See https://github.com/nasa/openmct/issues/2297#issuecomment-1032914258
|
||||
@param {number} n
|
||||
@param {number=} base log base, defaults to e
|
||||
*/
|
||||
export function antisymlog(n, base = e) {
|
||||
return Math.sign(n) * (antilog(Math.abs(n), base) - 1);
|
||||
}
|
@ -389,7 +389,7 @@ describe("the plugin", function () {
|
||||
expect(xAxisElement.length).toBe(1);
|
||||
|
||||
let ticks = xAxisElement[0].querySelectorAll(".gl-plot-tick");
|
||||
expect(ticks.length).toBe(5);
|
||||
expect(ticks.length).toBe(9);
|
||||
|
||||
done();
|
||||
});
|
||||
@ -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: ''
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -666,7 +694,7 @@ describe("the plugin", function () {
|
||||
|
||||
Vue.nextTick(() => {
|
||||
let ticks = xAxisElement[0].querySelectorAll(".gl-plot-tick");
|
||||
expect(ticks.length).toBe(5);
|
||||
expect(ticks.length).toBe(9);
|
||||
|
||||
done();
|
||||
});
|
||||
@ -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', () => {
|
||||
@ -1044,7 +1086,9 @@ describe("the plugin", function () {
|
||||
expandControl.dispatchEvent(clickEvent);
|
||||
|
||||
const yAxisProperties = editOptionsEl.querySelectorAll("div.grid-properties:first-of-type .l-inspector-part");
|
||||
expect(yAxisProperties.length).toEqual(3);
|
||||
|
||||
// TODO better test
|
||||
expect(yAxisProperties.length).toEqual(2);
|
||||
});
|
||||
|
||||
it('renders color palette options', () => {
|
||||
|
@ -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: {
|
||||
|
137
src/plugins/plot/stackedPlot/mixins/objectStyles-mixin.js
Normal file
137
src/plugins/plot/stackedPlot/mixins/objectStyles-mixin.js
Normal file
@ -0,0 +1,137 @@
|
||||
/*****************************************************************************
|
||||
* 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];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
@ -1,3 +1,5 @@
|
||||
import { antisymlog, symlog } from "./mathUtils";
|
||||
|
||||
const e10 = Math.sqrt(50);
|
||||
const e5 = Math.sqrt(10);
|
||||
const e2 = Math.sqrt(2);
|
||||
@ -40,6 +42,47 @@ function getPrecision(step) {
|
||||
return precision;
|
||||
}
|
||||
|
||||
export function getLogTicks(start, stop, mainTickCount = 8, secondaryTickCount = 6) {
|
||||
// log()'ed values
|
||||
const mainLogTicks = ticks(start, stop, mainTickCount);
|
||||
|
||||
// original values
|
||||
const mainTicks = mainLogTicks.map(n => antisymlog(n, 10));
|
||||
|
||||
const result = [];
|
||||
|
||||
let i = 0;
|
||||
for (const logTick of mainLogTicks) {
|
||||
result.push(logTick);
|
||||
|
||||
if (i === mainLogTicks.length - 1) {
|
||||
break;
|
||||
}
|
||||
|
||||
const tick = mainTicks[i];
|
||||
const nextTick = mainTicks[i + 1];
|
||||
const rangeBetweenMainTicks = nextTick - tick;
|
||||
|
||||
const secondaryLogTicks = ticks(
|
||||
tick + rangeBetweenMainTicks / (secondaryTickCount + 1),
|
||||
nextTick - rangeBetweenMainTicks / (secondaryTickCount + 1),
|
||||
secondaryTickCount - 2
|
||||
)
|
||||
.map(n => symlog(n, 10));
|
||||
|
||||
result.push(...secondaryLogTicks);
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function getLogTicks2(start, stop, count = 8) {
|
||||
return ticks(antisymlog(start, 10), antisymlog(stop, 10), count)
|
||||
.map(n => symlog(n, 10));
|
||||
}
|
||||
|
||||
/**
|
||||
* Linear tick generation from d3-array.
|
||||
*/
|
||||
@ -88,12 +131,17 @@ export function commonSuffix(a, b) {
|
||||
return a.slice(a.length - breakpoint);
|
||||
}
|
||||
|
||||
export function getFormattedTicks(newTicks, format) {
|
||||
export function getFormattedTicks(newTicks, format, formatFloat) {
|
||||
newTicks = newTicks
|
||||
.map(function (tickValue) {
|
||||
let formattedValue = format(tickValue);
|
||||
if (formatFloat === true && typeof formattedValue === 'number' && !Number.isInteger(formattedValue)) {
|
||||
formattedValue = parseFloat(formattedValue).toFixed(2);
|
||||
}
|
||||
|
||||
return {
|
||||
value: tickValue,
|
||||
text: format(tickValue)
|
||||
text: formattedValue
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -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;
|
||||
});
|
||||
|
@ -9,7 +9,11 @@ export default class TelemetryTableView {
|
||||
this.objectPath = objectPath;
|
||||
this.component = undefined;
|
||||
|
||||
this.table = new TelemetryTable(domainObject, openmct);
|
||||
Object.defineProperty(this, 'table', {
|
||||
value: new TelemetryTable(domainObject, openmct),
|
||||
enumerable: false,
|
||||
configurable: false
|
||||
});
|
||||
}
|
||||
|
||||
getViewContext() {
|
||||
|
@ -29,10 +29,6 @@ define(
|
||||
_,
|
||||
EventEmitter
|
||||
) {
|
||||
const LESS_THAN = -1;
|
||||
const EQUAL = 0;
|
||||
const GREATER_THAN = 1;
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
@ -80,10 +76,7 @@ define(
|
||||
this.rows = [];
|
||||
}
|
||||
|
||||
for (let row of rowsToAdd) {
|
||||
let index = this.sortedIndex(this.rows, row);
|
||||
this.rows.splice(index, 0, row);
|
||||
}
|
||||
this.sortAndMergeRows(rowsToAdd);
|
||||
|
||||
// we emit filter no matter what to trigger
|
||||
// an update of visible rows
|
||||
@ -92,58 +85,85 @@ define(
|
||||
}
|
||||
}
|
||||
|
||||
sortedLastIndex(rows, testRow) {
|
||||
return this.sortedIndex(rows, testRow, _.sortedLastIndex);
|
||||
}
|
||||
sortAndMergeRows(rows) {
|
||||
const sortedRowsToAdd = this.sortCollection(rows);
|
||||
|
||||
/**
|
||||
* Finds the correct insertion point for the given row.
|
||||
* Leverages lodash's `sortedIndex` function which implements a binary search.
|
||||
* @private
|
||||
*/
|
||||
sortedIndex(rows, testRow, lodashFunction = _.sortedIndexBy) {
|
||||
if (this.rows.length === 0) {
|
||||
return 0;
|
||||
this.rows = sortedRowsToAdd;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const testRowValue = this.getValueForSortColumn(testRow);
|
||||
const firstValue = this.getValueForSortColumn(this.rows[0]);
|
||||
const lastValue = this.getValueForSortColumn(this.rows[this.rows.length - 1]);
|
||||
const firstIncomingRow = sortedRowsToAdd[0];
|
||||
const lastIncomingRow = sortedRowsToAdd[sortedRowsToAdd.length - 1];
|
||||
const firstExistingRow = this.rows[0];
|
||||
const lastExistingRow = this.rows[this.rows.length - 1];
|
||||
|
||||
if (this.firstRowInSortOrder(lastIncomingRow, firstExistingRow)
|
||||
=== lastIncomingRow
|
||||
) {
|
||||
this.rows = [...sortedRowsToAdd, ...this.rows];
|
||||
} else if (this.firstRowInSortOrder(lastExistingRow, firstIncomingRow)
|
||||
=== lastExistingRow
|
||||
) {
|
||||
this.rows = [...this.rows, ...sortedRowsToAdd];
|
||||
} else {
|
||||
this.mergeSortedRows(sortedRowsToAdd);
|
||||
}
|
||||
}
|
||||
|
||||
sortCollection(rows) {
|
||||
const sortedRows = _.orderBy(
|
||||
rows,
|
||||
row => row.getParsedValue(this.sortOptions.key), this.sortOptions.direction
|
||||
);
|
||||
|
||||
return sortedRows;
|
||||
}
|
||||
|
||||
mergeSortedRows(rows) {
|
||||
const mergedRows = [];
|
||||
let i = 0;
|
||||
let j = 0;
|
||||
|
||||
while (i < this.rows.length && j < rows.length) {
|
||||
const existingRow = this.rows[i];
|
||||
const incomingRow = rows[j];
|
||||
|
||||
if (this.firstRowInSortOrder(existingRow, incomingRow) === existingRow) {
|
||||
mergedRows.push(existingRow);
|
||||
i++;
|
||||
} else {
|
||||
mergedRows.push(incomingRow);
|
||||
j++;
|
||||
}
|
||||
}
|
||||
|
||||
// tail of existing rows is all that is left to merge
|
||||
if (i < this.rows.length) {
|
||||
for (i; i < this.rows.length; i++) {
|
||||
mergedRows.push(this.rows[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// tail of incoming rows is all that is left to merge
|
||||
if (j < rows.length) {
|
||||
for (j; j < rows.length; j++) {
|
||||
mergedRows.push(rows[j]);
|
||||
}
|
||||
}
|
||||
|
||||
this.rows = mergedRows;
|
||||
}
|
||||
|
||||
firstRowInSortOrder(row1, row2) {
|
||||
const val1 = this.getValueForSortColumn(row1);
|
||||
const val2 = this.getValueForSortColumn(row2);
|
||||
|
||||
if (this.sortOptions.direction === 'asc') {
|
||||
if (testRowValue > lastValue) {
|
||||
return this.rows.length;
|
||||
} else if (testRowValue === lastValue) {
|
||||
// Maintain stable sort
|
||||
return this.rows.length;
|
||||
} else if (testRowValue <= firstValue) {
|
||||
return 0;
|
||||
} else {
|
||||
return lodashFunction(rows, testRow, (thisRow) => {
|
||||
return this.getValueForSortColumn(thisRow);
|
||||
});
|
||||
}
|
||||
return val1 <= val2 ? row1 : row2;
|
||||
} else {
|
||||
if (testRowValue >= firstValue) {
|
||||
return 0;
|
||||
} else if (testRowValue < lastValue) {
|
||||
return this.rows.length;
|
||||
} else if (testRowValue === lastValue) {
|
||||
// Maintain stable sort
|
||||
return this.rows.length;
|
||||
} else {
|
||||
// Use a custom comparison function to support descending sort.
|
||||
return lodashFunction(rows, testRow, (thisRow) => {
|
||||
const thisRowValue = this.getValueForSortColumn(thisRow);
|
||||
if (testRowValue === thisRowValue) {
|
||||
return EQUAL;
|
||||
} else if (testRowValue < thisRowValue) {
|
||||
return LESS_THAN;
|
||||
} else {
|
||||
return GREATER_THAN;
|
||||
}
|
||||
});
|
||||
}
|
||||
return val1 >= val2 ? row1 : row2;
|
||||
}
|
||||
}
|
||||
|
||||
@ -205,8 +225,9 @@ define(
|
||||
sortBy(sortOptions) {
|
||||
if (arguments.length > 0) {
|
||||
this.sortOptions = sortOptions;
|
||||
performance.mark('table:row:sort:start');
|
||||
this.rows = _.orderBy(this.rows, (row) => row.getParsedValue(sortOptions.key), sortOptions.direction);
|
||||
|
||||
performance.mark('table:row:sort:stop');
|
||||
this.emit('sort');
|
||||
}
|
||||
|
||||
|
@ -613,6 +613,7 @@ export default {
|
||||
this.calculateScrollbarWidth();
|
||||
},
|
||||
sortBy(columnKey) {
|
||||
performance.mark('table:sort');
|
||||
// If sorting by the same column, flip the sort direction.
|
||||
if (this.sortOptions.key === columnKey) {
|
||||
if (this.sortOptions.direction === 'asc') {
|
||||
@ -669,6 +670,7 @@ export default {
|
||||
this.setHeight();
|
||||
},
|
||||
rowsAdded(rows) {
|
||||
performance.mark('row:added');
|
||||
this.setHeight();
|
||||
|
||||
let sizingRow;
|
||||
@ -690,6 +692,7 @@ export default {
|
||||
this.updateVisibleRows();
|
||||
},
|
||||
rowsRemoved(rows) {
|
||||
performance.mark('row:removed');
|
||||
this.setHeight();
|
||||
this.updateVisibleRows();
|
||||
},
|
||||
|
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 * 60 * 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); }
|
||||
|
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>
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user