mirror of
https://github.com/nasa/openmct.git
synced 2025-01-25 05:47:09 +00:00
Merge branch 'master' into test-heuristics
This commit is contained in:
commit
b39ff87197
@ -2,7 +2,7 @@ version: 2.1
|
||||
executors:
|
||||
pw-focal-development:
|
||||
docker:
|
||||
- image: mcr.microsoft.com/playwright:v1.19.2-focal
|
||||
- image: mcr.microsoft.com/playwright:v1.21.1-focal
|
||||
environment:
|
||||
NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed
|
||||
parameters:
|
||||
@ -64,7 +64,7 @@ commands:
|
||||
- run: curl -Os https://uploader.codecov.io/latest/linux/codecov;chmod +x codecov;./codecov
|
||||
orbs:
|
||||
node: circleci/node@4.9.0
|
||||
browser-tools: circleci/browser-tools@1.2.3
|
||||
browser-tools: circleci/browser-tools@1.3.0
|
||||
jobs:
|
||||
npm-audit:
|
||||
parameters:
|
||||
@ -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:
|
||||
|
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
@ -32,12 +32,12 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: javascript
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
uses: github/codeql-action/analyze@v2
|
||||
|
4
.github/workflows/e2e-pr.yml
vendored
4
.github/workflows/e2e-pr.yml
vendored
@ -30,11 +30,11 @@ jobs:
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
- run: npx playwright@1.19.2 install
|
||||
- run: npx playwright@1.21.1 install
|
||||
- run: npm 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
|
||||
|
2
.github/workflows/e2e-visual.yml
vendored
2
.github/workflows/e2e-visual.yml
vendored
@ -17,7 +17,7 @@ jobs:
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
- run: npx playwright@1.19.2 install
|
||||
- run: npx playwright@1.21.1 install
|
||||
- run: npm install
|
||||
- name: Run the e2e visual tests
|
||||
run: npm run test:e2e:visual
|
||||
|
1
.github/workflows/pr-platform.yml
vendored
1
.github/workflows/pr-platform.yml
vendored
@ -18,6 +18,7 @@ jobs:
|
||||
node_version:
|
||||
- 14
|
||||
- 16
|
||||
- 18
|
||||
architecture:
|
||||
- x64
|
||||
name: Node ${{ matrix.node_version }} - ${{ matrix.architecture }} on ${{ matrix.os }}
|
||||
|
@ -6,9 +6,9 @@ const { devices } = require('@playwright/test');
|
||||
|
||||
/** @type {import('@playwright/test').PlaywrightTestConfig} */
|
||||
const config = {
|
||||
retries: 2,
|
||||
retries: 1,
|
||||
testDir: 'tests',
|
||||
timeout: 90 * 1000,
|
||||
timeout: 60 * 1000,
|
||||
webServer: {
|
||||
command: 'npm run start',
|
||||
port: 8080,
|
||||
@ -28,12 +28,12 @@ const config = {
|
||||
{
|
||||
name: 'chrome',
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
...devices['Desktop Chrome']
|
||||
browserName: 'chromium'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'MMOC',
|
||||
grepInvert: /@snapshot/,
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
viewport: {
|
||||
|
@ -29,12 +29,12 @@ const config = {
|
||||
{
|
||||
name: 'chrome',
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
...devices['Desktop Chrome']
|
||||
browserName: 'chromium'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'MMOC',
|
||||
grepInvert: /@snapshot/,
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
viewport: {
|
||||
|
@ -27,16 +27,115 @@ This test suite is dedicated to tests which verify the basic operations surround
|
||||
const { test, expect } = require('@playwright/test');
|
||||
|
||||
test.describe('Move item tests', () => {
|
||||
test.fixme('Create a basic object and verify that it can be moved to another Folder', async ({ page }) => {
|
||||
//Create and save Folder
|
||||
//Create and save Domain Object
|
||||
//Verify that the newly created domain object can be moved to Folder from Step 1.
|
||||
//Verify that newly moved object appears in the correct point in Tree
|
||||
//Verify that newly moved object appears correctly in Inspector panel
|
||||
test('Create a basic object and verify that it can be moved to another folder', async ({ page }) => {
|
||||
// Go to Open MCT
|
||||
await page.goto('/');
|
||||
|
||||
// Create a new folder in the root my items folder
|
||||
let folder1 = "Folder1";
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
await page.locator('li.icon-folder').click();
|
||||
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"]').click();
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder1);
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=OK').click(),
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
//Wait until Save Banner is gone
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
|
||||
// Create another folder with a new name at default location, which is currently inside Folder 1
|
||||
let folder2 = "Folder2";
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
await page.locator('li.icon-folder').click();
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"]').click();
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder2);
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=OK').click(),
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
//Wait until Save Banner is gone
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
|
||||
// Move Folder 2 from Folder 1 to My Items
|
||||
await page.locator('text=Open MCT My Items >> span').nth(3).click();
|
||||
await page.locator('.c-tree__scrollable div div:nth-child(2) .c-tree__item .c-tree__item__view-control').click();
|
||||
|
||||
await page.locator(`a:has-text("${folder2}")`).click({
|
||||
button: 'right'
|
||||
});
|
||||
await page.locator('li.icon-move').click();
|
||||
await page.locator('form[name="mctForm"] >> text=My Items').click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=OK').click()
|
||||
]);
|
||||
|
||||
// Expect that Folder 2 is in My Items, the root folder
|
||||
expect(page.locator(`text=My Items >> nth=0:has(text=${folder2})`)).toBeTruthy();
|
||||
});
|
||||
test.fixme('Create a basic object and verify that it cannot be moved to object without Composition Provider', async ({ page }) => {
|
||||
//Create and save Telemetry Object
|
||||
//Create and save Domain Object
|
||||
//Verify that the newly created domain object cannot be moved to Telemetry Object from step 1.
|
||||
test('Create a basic object and verify that it cannot be moved to telemetry object without Composition Provider', async ({ page }) => {
|
||||
// Go to Open MCT
|
||||
await page.goto('/');
|
||||
|
||||
// Create Telemetry Table
|
||||
let telemetryTable = 'Test Telemetry Table';
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
await page.locator('li:has-text("Telemetry Table")').click();
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"]').click();
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(telemetryTable);
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=OK').click()
|
||||
]);
|
||||
|
||||
// Finish editing and save Telemetry Table
|
||||
await page.locator('.c-button--menu.c-button--major.icon-save').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
// Create New Folder Basic Domain Object
|
||||
let folder = 'Test Folder';
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
await page.locator('li:has-text("Folder")').click();
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"]').click();
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder);
|
||||
|
||||
// See if it's possible to put the folder in the Telemetry object during creation (Soft Assert)
|
||||
await page.locator(`form[name="mctForm"] >> text=${telemetryTable}`).click();
|
||||
let okButton = await page.locator('button.c-button.c-button--major:has-text("OK")');
|
||||
let okButtonStateDisabled = await okButton.isDisabled();
|
||||
expect.soft(okButtonStateDisabled).toBeTruthy();
|
||||
|
||||
// Continue test regardless of assertion and create it in My Items
|
||||
await page.locator('form[name="mctForm"] >> text=My Items').click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=OK').click()
|
||||
]);
|
||||
|
||||
// Open My Items
|
||||
await page.locator('text=Open MCT My Items >> span').nth(3).click();
|
||||
|
||||
// Select Folder Object and select Move from context menu
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator(`a:has-text("${folder}")`).click()
|
||||
]);
|
||||
await page.locator('.c-tree__item.is-navigated-object .c-tree__item__label .c-tree__item__type-icon').click({
|
||||
button: 'right'
|
||||
});
|
||||
await page.locator('li.icon-move').click();
|
||||
|
||||
// See if it's possible to put the folder in the Telemetry object after creation
|
||||
await page.locator('text=Location Open MCT My Items >> span').nth(3).click();
|
||||
await page.locator(`form[name="mctForm"] >> text=${telemetryTable}`).click();
|
||||
let okButton2 = await page.locator('button.c-button.c-button--major:has-text("OK")');
|
||||
let okButtonStateDisabled2 = await okButton2.isDisabled();
|
||||
expect(okButtonStateDisabled2).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
@ -21,45 +21,163 @@
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
This test suite is dedicated to tests which verify the basic operations surrounding conditionSets.
|
||||
This test suite is dedicated to tests which verify the basic operations surrounding conditionSets. Note: this
|
||||
suite is sharing state between tests which is considered an anti-pattern. Implimenting in this way to
|
||||
demonstrate some playwright for test developers. This pattern should not be re-used in other CRUD suites.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('@playwright/test');
|
||||
|
||||
test.describe('Condition Set Operations', () => {
|
||||
test('Create new button `condition set` creates new condition object', async ({ page }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
let conditionSetUrl;
|
||||
let getConditionSetIdentifierFromUrl;
|
||||
|
||||
//Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
test('Create new Condition Set object and store @localStorage', async ({ page, context }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
// Click text=Condition Set
|
||||
await page.click('text=Condition Set');
|
||||
//Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
// Click text=OK
|
||||
// Click text=Condition Set
|
||||
await page.click('text=Condition Set');
|
||||
|
||||
// Click text=OK
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.click('text=OK')
|
||||
]);
|
||||
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
|
||||
//Save localStorage for future test execution
|
||||
await context.storageState({ path: './e2e/tests/recycled_storage.json' });
|
||||
|
||||
//Set object identifier from url
|
||||
conditionSetUrl = await page.url();
|
||||
console.log('conditionSetUrl ' + conditionSetUrl);
|
||||
|
||||
getConditionSetIdentifierFromUrl = await conditionSetUrl.split('/').pop().split('?')[0];
|
||||
console.log('getConditionSetIdentifierFromUrl ' + getConditionSetIdentifierFromUrl);
|
||||
|
||||
});
|
||||
|
||||
test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
|
||||
//Load localStorage for subsequent tests
|
||||
test.use({ storageState: './e2e/tests/recycled_storage.json' });
|
||||
|
||||
//Begin suite of tests again localStorage
|
||||
test('Condition set object properties persist in main view and inspector', async ({ page }) => {
|
||||
//Navigate to baseURL with injected localStorage
|
||||
await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
|
||||
|
||||
//Assertions on loaded Condition Set in main view
|
||||
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
|
||||
|
||||
//Assertions on loaded Condition Set in Inspector
|
||||
await expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy;
|
||||
|
||||
//Reload Page
|
||||
await Promise.all([
|
||||
page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/dab945d4-5a84-480e-8180-222b4aa730fa?tc.mode=fixed&tc.startBound=1639696164435&tc.endBound=1639697964435&tc.timeSystem=utc&view=conditionSet.view' }*/),
|
||||
page.click('text=OK')
|
||||
page.reload(),
|
||||
page.waitForLoadState('networkidle')
|
||||
]);
|
||||
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
|
||||
//Re-verify after reload
|
||||
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
|
||||
//Assertions on loaded Condition Set in Inspector
|
||||
await expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy;
|
||||
|
||||
});
|
||||
test.fixme('condition set object properties exist', async ({ page }) => {
|
||||
//Go to object created in step one
|
||||
//Verify the Condition Set properties persist on Save
|
||||
//Verify the Condition Set properties persist on page.reload()
|
||||
});
|
||||
test.fixme('condition set object can be modified', async ({ page }) => {
|
||||
//Go to object created in step one
|
||||
test('condition set object can be modified on @localStorage', async ({ page }) => {
|
||||
await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
|
||||
|
||||
//Assertions on loaded Condition Set in main view
|
||||
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
|
||||
|
||||
//Update the Condition Set properties
|
||||
//Verify the Condition Set properties persist on Save
|
||||
//Verify the Condition Set properties persist on page.reload()
|
||||
// Click Edit Button
|
||||
await page.locator('text=Conditions View Snapshot >> button').nth(3).click();
|
||||
|
||||
//Edit Condition Set Name from main view
|
||||
await page.locator('text=Unnamed Condition Set').first().fill('Renamed Condition Set');
|
||||
await page.locator('text=Renamed Condition Set').first().press('Enter');
|
||||
// Click Save Button
|
||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
||||
// Click Save and Finish Editing Option
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
//Verify Main section reflects updated Name Property
|
||||
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Renamed Condition Set');
|
||||
|
||||
// Verify Inspector properties
|
||||
// Verify Inspector has updated Name property
|
||||
await expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy();
|
||||
// Verify Inspector Details has updated Name property
|
||||
await expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy();
|
||||
|
||||
// Verify Tree reflects updated Name proprety
|
||||
// Expand Tree
|
||||
await page.locator('text=Open MCT My Items >> span >> nth=3').click();
|
||||
// Verify Condition Set Object is renamed in Tree
|
||||
await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
|
||||
// Verify Search Tree reflects renamed Name property
|
||||
await page.locator('input[type="search"]').fill('Renamed');
|
||||
await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
|
||||
|
||||
//Reload Page
|
||||
await Promise.all([
|
||||
page.reload(),
|
||||
page.waitForLoadState('networkidle')
|
||||
]);
|
||||
|
||||
//Verify Main section reflects updated Name Property
|
||||
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Renamed Condition Set');
|
||||
|
||||
// Verify Inspector properties
|
||||
// Verify Inspector has updated Name property
|
||||
await expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy();
|
||||
// Verify Inspector Details has updated Name property
|
||||
await expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy();
|
||||
|
||||
// Verify Tree reflects updated Name proprety
|
||||
// Expand Tree
|
||||
await page.locator('text=Open MCT My Items >> span >> nth=3').click();
|
||||
// Verify Condition Set Object is renamed in Tree
|
||||
await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
|
||||
// Verify Search Tree reflects renamed Name property
|
||||
await page.locator('input[type="search"]').fill('Renamed');
|
||||
await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
|
||||
});
|
||||
test.fixme('condition set object can be deleted', async ({ page }) => {
|
||||
//Go to object created in step one
|
||||
//Verify that Condition Set object can be deleted
|
||||
//Verify the Condition Set object does not exist in Tree
|
||||
//Verify the Condition Set object does not exist with direct navigation to object's URL
|
||||
test('condition set object can be deleted by Search Tree Actions menu on @localStorage', async ({ page }) => {
|
||||
//Navigate to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
//Expect Unnamed Condition Set to be visible in Main View
|
||||
await expect(page.locator('a:has-text("Unnamed Condition Set Condition Set")')).toBeVisible();
|
||||
|
||||
// Search for Unnamed Condition Set
|
||||
await page.locator('input[type="search"]').fill('Unnamed Condition Set');
|
||||
// Right Click to Open Actions Menu
|
||||
await page.locator('a:has-text("Unnamed Condition Set")').click({
|
||||
button: 'right'
|
||||
});
|
||||
// Click Remove Action
|
||||
await page.locator('text=Remove').click();
|
||||
|
||||
await page.locator('text=OK').click();
|
||||
|
||||
//Expect Unnamed Condition Set to be removed in Main View
|
||||
await expect(page.locator('a:has-text("Unnamed Condition Set Condition Set")')).not.toBeVisible();
|
||||
|
||||
await page.locator('.c-search__clear-input').click();
|
||||
// Search for Unnamed Condition Set
|
||||
await page.locator('input[type="search"]').fill('Unnamed Condition Set');
|
||||
// Expect Unnamed Condition Set to be removed
|
||||
await expect(page.locator('a:has-text("Unnamed Condition Set")')).not.toBeVisible();
|
||||
|
||||
//Feature?
|
||||
//Domain Object is still available by direct URL after delete
|
||||
await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
|
||||
|
||||
});
|
||||
});
|
||||
|
@ -24,13 +24,14 @@
|
||||
This test suite is dedicated to tests which verify the basic operations surrounding imagery,
|
||||
but only assume that example imagery is present.
|
||||
*/
|
||||
/* globals process */
|
||||
|
||||
const { test, expect } = require('@playwright/test');
|
||||
|
||||
test.describe('Example Imagery', () => {
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
page.on('console', msg => console.log(msg.text()))
|
||||
page.on('console', msg => console.log(msg.text()));
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
@ -42,10 +43,13 @@ test.describe('Example Imagery', () => {
|
||||
|
||||
// Click text=OK
|
||||
await Promise.all([
|
||||
page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/dab945d4-5a84-480e-8180-222b4aa730fa?tc.mode=fixed&tc.startBound=1639696164435&tc.endBound=1639697964435&tc.timeSystem=utc&view=conditionSet.view' }*/),
|
||||
page.click('text=OK')
|
||||
page.waitForNavigation({waitUntil: 'networkidle'}),
|
||||
page.click('text=OK'),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
|
||||
//Wait until Save Banner is gone
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
|
||||
});
|
||||
|
||||
@ -77,9 +81,11 @@ test.describe('Example Imagery', () => {
|
||||
|
||||
test('Can use alt+drag to move around image once zoomed in', async ({ page }) => {
|
||||
const deltaYStep = 100; //equivalent to 1x zoom
|
||||
const panHotkey = process.platform === 'linux' ? ['Control', 'Alt'] : ['Alt'];
|
||||
|
||||
const bgImageLocator = await page.locator(backgroundImageSelector);
|
||||
await bgImageLocator.hover();
|
||||
|
||||
// zoom in
|
||||
await page.mouse.wheel(0, deltaYStep * 2);
|
||||
await bgImageLocator.hover();
|
||||
@ -91,40 +97,49 @@ test.describe('Example Imagery', () => {
|
||||
// center the mouse pointer
|
||||
await page.mouse.move(imageCenterX, imageCenterY);
|
||||
|
||||
//Get Diagnostic info about process environment
|
||||
console.log('process.platform is ' + process.platform);
|
||||
const getUA = await page.evaluate(() => navigator.userAgent);
|
||||
console.log('navigator.userAgent ' + getUA);
|
||||
// Pan Imagery Hints
|
||||
const expectedAltText = process.platform === 'linux' ? 'Ctrl+Alt drag to pan' : 'Alt drag to pan';
|
||||
const imageryHintsText = await page.locator('.c-imagery__hints').innerText();
|
||||
expect(expectedAltText).toEqual(imageryHintsText);
|
||||
|
||||
// pan right
|
||||
await page.keyboard.down('Alt');
|
||||
await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(imageCenterX - 200, imageCenterY, 10);
|
||||
await page.mouse.up();
|
||||
await page.keyboard.up('Alt');
|
||||
await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
|
||||
const afterRightPanBoundingBox = await bgImageLocator.boundingBox();
|
||||
expect(zoomedBoundingBox.x).toBeGreaterThan(afterRightPanBoundingBox.x);
|
||||
|
||||
// pan left
|
||||
await page.keyboard.down('Alt');
|
||||
await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(imageCenterX, imageCenterY, 10);
|
||||
await page.mouse.up();
|
||||
await page.keyboard.up('Alt');
|
||||
await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
|
||||
const afterLeftPanBoundingBox = await bgImageLocator.boundingBox();
|
||||
expect(afterRightPanBoundingBox.x).toBeLessThan(afterLeftPanBoundingBox.x);
|
||||
|
||||
// pan up
|
||||
await page.mouse.move(imageCenterX, imageCenterY);
|
||||
await page.keyboard.down('Alt');
|
||||
await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(imageCenterX, imageCenterY + 200, 10);
|
||||
await page.mouse.up();
|
||||
await page.keyboard.up('Alt');
|
||||
await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
|
||||
const afterUpPanBoundingBox = await bgImageLocator.boundingBox();
|
||||
expect(afterUpPanBoundingBox.y).toBeGreaterThan(afterLeftPanBoundingBox.y);
|
||||
|
||||
// pan down
|
||||
await page.keyboard.down('Alt');
|
||||
await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(imageCenterX, imageCenterY - 200, 10);
|
||||
await page.mouse.up();
|
||||
await page.keyboard.up('Alt');
|
||||
await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
|
||||
const afterDownPanBoundingBox = await bgImageLocator.boundingBox();
|
||||
expect(afterDownPanBoundingBox.y).toBeLessThan(afterUpPanBoundingBox.y);
|
||||
|
||||
@ -156,20 +171,26 @@ test.describe('Example Imagery', () => {
|
||||
|
||||
test('Can use the reset button to reset the image', async ({ page }) => {
|
||||
const bgImageLocator = await page.locator(backgroundImageSelector);
|
||||
// wait for zoom animation to finish
|
||||
await bgImageLocator.hover();
|
||||
|
||||
const zoomInBtn = await page.locator('.t-btn-zoom-in');
|
||||
const zoomResetBtn = await page.locator('.t-btn-zoom-reset');
|
||||
const initialBoundingBox = await bgImageLocator.boundingBox();
|
||||
|
||||
await zoomInBtn.click();
|
||||
// wait for zoom animation to finish
|
||||
await bgImageLocator.hover();
|
||||
await zoomInBtn.click();
|
||||
// wait for zoom animation to finish
|
||||
await bgImageLocator.hover();
|
||||
|
||||
const zoomedInBoundingBox = await bgImageLocator.boundingBox();
|
||||
expect.soft(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
|
||||
expect.soft(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
|
||||
|
||||
await zoomResetBtn.click();
|
||||
// wait for zoom animation to finish
|
||||
await bgImageLocator.hover();
|
||||
|
||||
const resetBoundingBox = await bgImageLocator.boundingBox();
|
||||
@ -180,38 +201,38 @@ test.describe('Example Imagery', () => {
|
||||
expect(resetBoundingBox.width).toEqual(initialBoundingBox.width);
|
||||
});
|
||||
|
||||
//test('Can use Mouse Wheel to zoom in and out of previous image');
|
||||
//test('Can zoom into the latest image and the real-time/fixed-time imagery will pause');
|
||||
//test.skip('Can zoom into a previous image from thumbstrip in real-time or fixed-time');
|
||||
//test.skip('Clicking on the left arrow should pause the imagery and go to previous image');
|
||||
//test.skip('If the imagery view is in pause mode, it should not be updated when new images come in');
|
||||
//test.skip('If the imagery view is not in pause mode, it should be updated when new images come in');
|
||||
//test.fixme('Can use Mouse Wheel to zoom in and out of previous image');
|
||||
//test.fixme('Can zoom into the latest image and the real-time/fixed-time imagery will pause');
|
||||
//test.fixme('Can zoom into a previous image from thumbstrip in real-time or fixed-time');
|
||||
//test.fixme('Clicking on the left arrow should pause the imagery and go to previous image');
|
||||
//test.fixme('If the imagery view is in pause mode, it should not be updated when new images come in');
|
||||
//test.fixme('If the imagery view is not in pause mode, it should be updated when new images come in');
|
||||
});
|
||||
|
||||
test.describe('Example Imagery in Display layout', () => {
|
||||
test.skip('Can use Mouse Wheel to zoom in and out of previous image');
|
||||
test.skip('Can use alt+drag to move around image once zoomed in');
|
||||
test.skip('Can zoom into the latest image and the real-time/fixed-time imagery will pause');
|
||||
test.skip('Clicking on the left arrow should pause the imagery and go to previous image');
|
||||
test.skip('If the imagery view is in pause mode, it should not be updated when new images come in');
|
||||
test.skip('If the imagery view is not in pause mode, it should be updated when new images come in');
|
||||
test.fixme('Can use Mouse Wheel to zoom in and out of previous image');
|
||||
test.fixme('Can use alt+drag to move around image once zoomed in');
|
||||
test.fixme('Can zoom into the latest image and the real-time/fixed-time imagery will pause');
|
||||
test.fixme('Clicking on the left arrow should pause the imagery and go to previous image');
|
||||
test.fixme('If the imagery view is in pause mode, it should not be updated when new images come in');
|
||||
test.fixme('If the imagery view is not in pause mode, it should be updated when new images come in');
|
||||
});
|
||||
|
||||
test.describe('Example Imagery in Flexible layout', () => {
|
||||
test.skip('Can use Mouse Wheel to zoom in and out of previous image');
|
||||
test.skip('Can use alt+drag to move around image once zoomed in');
|
||||
test.skip('Can zoom into the latest image and the real-time/fixed-time imagery will pause');
|
||||
test.skip('Clicking on the left arrow should pause the imagery and go to previous image');
|
||||
test.skip('If the imagery view is in pause mode, it should not be updated when new images come in');
|
||||
test.skip('If the imagery view is not in pause mode, it should be updated when new images come in');
|
||||
test.fixme('Can use Mouse Wheel to zoom in and out of previous image');
|
||||
test.fixme('Can use alt+drag to move around image once zoomed in');
|
||||
test.fixme('Can zoom into the latest image and the real-time/fixed-time imagery will pause');
|
||||
test.fixme('Clicking on the left arrow should pause the imagery and go to previous image');
|
||||
test.fixme('If the imagery view is in pause mode, it should not be updated when new images come in');
|
||||
test.fixme('If the imagery view is not in pause mode, it should be updated when new images come in');
|
||||
});
|
||||
|
||||
test.describe('Example Imagery in Tabs view', () => {
|
||||
test.skip('Can use Mouse Wheel to zoom in and out of previous image');
|
||||
test.skip('Can use alt+drag to move around image once zoomed in');
|
||||
test.skip('Can zoom into the latest image and the real-time/fixed-time imagery will pause');
|
||||
test.skip('Can zoom into a previous image from thumbstrip in real-time or fixed-time');
|
||||
test.skip('Clicking on the left arrow should pause the imagery and go to previous image');
|
||||
test.skip('If the imagery view is in pause mode, it should not be updated when new images come in');
|
||||
test.skip('If the imagery view is not in pause mode, it should be updated when new images come in');
|
||||
test.fixme('Can use Mouse Wheel to zoom in and out of previous image');
|
||||
test.fixme('Can use alt+drag to move around image once zoomed in');
|
||||
test.fixme('Can zoom into the latest image and the real-time/fixed-time imagery will pause');
|
||||
test.fixme('Can zoom into a previous image from thumbstrip in real-time or fixed-time');
|
||||
test.fixme('Clicking on the left arrow should pause the imagery and go to previous image');
|
||||
test.fixme('If the imagery view is in pause mode, it should not be updated when new images come in');
|
||||
test.fixme('If the imagery view is not in pause mode, it should be updated when new images come in');
|
||||
});
|
||||
|
@ -21,7 +21,7 @@
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
Test for plot autoscale.
|
||||
Testsuite for plot autoscale.
|
||||
*/
|
||||
|
||||
const { test: _test, expect } = require('@playwright/test');
|
||||
@ -47,7 +47,7 @@ test.use({
|
||||
});
|
||||
|
||||
test.describe('ExportAsJSON', () => {
|
||||
test.only('autoscale off causes no error from undefined user range', async ({ page }) => {
|
||||
test.slow('User can set autoscale with a valid range @snapshot', async ({ page }) => {
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
await setTimeRange(page);
|
||||
@ -68,14 +68,6 @@ test.describe('ExportAsJSON', () => {
|
||||
.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, {
|
||||
@ -91,18 +83,12 @@ test.describe('ExportAsJSON', () => {
|
||||
|
||||
await page.keyboard.up('Alt');
|
||||
|
||||
page.off('pageerror', onError);
|
||||
|
||||
// There would have been an error at this point. So if there isn't, then
|
||||
// we fixed it.
|
||||
expect(errorCount).toBe(0);
|
||||
|
||||
// Ensure the drag worked.
|
||||
await Promise.all([
|
||||
testYTicks(page, ['0.00', '0.50', '1.00', '1.50', '2.00']),
|
||||
new Promise(r => setTimeout(r, 100))
|
||||
.then(() => canvas.screenshot())
|
||||
.then(shot => expect(shot).toMatchSnapshot('autoscale-canvas-panned.png', { maxDiffPixels: 20 }))
|
||||
.then(shot => expect(shot).toMatchSnapshot('autoscale-canvas-panned.png', { maxDiffPixels: 40 }))
|
||||
]);
|
||||
});
|
||||
});
|
||||
@ -134,9 +120,14 @@ async function createSinewaveOverlayPlot(page) {
|
||||
// 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()
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=OK').click(),
|
||||
//Wait for Save Banner to appear1
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
//Wait until Save Banner is gone
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
|
||||
// save (exit edit mode)
|
||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
||||
@ -148,14 +139,19 @@ async function createSinewaveOverlayPlot(page) {
|
||||
// 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()
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=OK').click(),
|
||||
//Wait for Save Banner to appear1
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
//Wait until Save Banner is gone
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
|
||||
// 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.waitForNavigation(),
|
||||
page.locator('text=Unnamed Overlay Plot').first().click()
|
||||
]);
|
||||
}
|
||||
@ -168,11 +164,18 @@ async function turnOffAutoscale(page) {
|
||||
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();
|
||||
await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"] >> nth=1').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();
|
||||
await Promise.all([
|
||||
page.locator('text=Save and Finish Editing').click(),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
//Wait until Save Banner is gone
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -180,6 +183,7 @@ async function turnOffAutoscale(page) {
|
||||
*/
|
||||
async function testYTicks(page, values) {
|
||||
const yTicks = page.locator('.gl-plot-y-tick-label');
|
||||
await page.locator('canvas >> nth=1').hover();
|
||||
let promises = [yTicks.count().then(c => expect(c).toBe(values.length))];
|
||||
|
||||
for (let i = 0, l = values.length; i < l; i += 1) {
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 15 KiB |
Binary file not shown.
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 18 KiB |
301
e2e/tests/plugins/plot/logPlot.e2e.spec.js
Normal file
301
e2e/tests/plugins/plot/logPlot.e2e.spec.js
Normal file
@ -0,0 +1,301 @@
|
||||
/*****************************************************************************
|
||||
* 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. Note this test suite if very much under active development and should not
|
||||
necessarily be used for reference when writing new tests in this area.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('@playwright/test');
|
||||
|
||||
test.describe('Log plot tests', () => {
|
||||
test.slow('Log Plot ticks are functionally correct in regular and log mode and after refresh', 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 and wait for charts and ticks to load
|
||||
await page.waitForTimeout(1 * 1000);
|
||||
await page.reload({ waitUntil: 'networkidle'});
|
||||
await page.waitForSelector('.gl-plot-chart-area');
|
||||
await page.waitForSelector('.gl-plot-y-tick-label');
|
||||
|
||||
// test log ticks hold up after refresh
|
||||
await testLogTicks(page);
|
||||
//await testLogPlotPixels(page);
|
||||
});
|
||||
|
||||
test.skip('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({ waitUntil: 'networkidle'}),
|
||||
page.locator('text=OK').click(),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
//Wait until Save Banner is gone
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
|
||||
// save the overlay plot
|
||||
|
||||
await saveOverlayPlot(page);
|
||||
|
||||
// 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({ waitUntil: 'networkidle'}),
|
||||
page.locator('text=OK').click(),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
//Wait until Save Banner is gone
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
|
||||
// click on overlay plot
|
||||
|
||||
await page.locator('text=Open MCT My Items >> span').nth(3).click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=Unnamed Overlay Plot').first().click()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function testRegularTicks(page) {
|
||||
const yTicks = await 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 = await 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();
|
||||
await expect(await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1)).toBeVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 Promise.all([
|
||||
page.locator('text=Save and Finish Editing').click(),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
//Wait until Save Banner is gone
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached' });
|
||||
}
|
||||
|
||||
/**
|
||||
* @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, 5 * 1000));
|
||||
|
||||
// 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);
|
||||
}
|
@ -67,3 +67,46 @@ test.describe('Time counductor operations', () => {
|
||||
expect(endDateValidityStatus).not.toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// Testing instructions:
|
||||
// Try to change the realtime offsets when in realtime (local clock) mode.
|
||||
test.describe('Time conductor input fields real-time mode', () => {
|
||||
test('validate input fields in real-time mode', async ({ page }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
// Set realtime "local clock" mode offsets
|
||||
const timeInputs = page.locator('input.c-input--datetime');
|
||||
|
||||
// Click fixed timespan button
|
||||
await page.locator('.c-button__label >> text=Fixed Timespan').click();
|
||||
|
||||
// Click local clock
|
||||
await page.locator('.icon-clock >> text=Local Clock').click();
|
||||
|
||||
// Click time offset button
|
||||
await page.locator('.c-conductor__delta-button >> text=00:30:00').click();
|
||||
|
||||
// Input start time offset
|
||||
await page.fill('.pr-time-controls__secs', '23');
|
||||
|
||||
// Click the check button
|
||||
await page.locator('.icon-check').click();
|
||||
|
||||
// Verify time was updated on time offset button
|
||||
await expect(page.locator('.c-conductor__delta-button').first()).toContainText('00:30:23');
|
||||
|
||||
// Click time offset set preceding now button
|
||||
await page.locator('.c-conductor__delta-button >> text=00:00:30').click();
|
||||
|
||||
// Input preceding time offset
|
||||
await page.fill('.pr-time-controls__secs', '31')
|
||||
|
||||
// Click the check buttons
|
||||
await page.locator('.icon-check').click();
|
||||
|
||||
// Verify time was updated on preceding time offset button
|
||||
await expect(page.locator('.c-conductor__delta-button').nth(1)).toContainText('00:00:31');
|
||||
});
|
||||
});
|
||||
|
22
e2e/tests/recycled_storage.json
Normal file
22
e2e/tests/recycled_storage.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"cookies": [],
|
||||
"origins": [
|
||||
{
|
||||
"origin": "http://localhost:8080",
|
||||
"localStorage": [
|
||||
{
|
||||
"name": "tcHistory",
|
||||
"value": "{\"utc\":[{\"start\":1651513945533,\"end\":1651515745533}]}"
|
||||
},
|
||||
{
|
||||
"name": "mct",
|
||||
"value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"c0f99e39-85e7-4ef7-99b1-ef52d4ed69b2\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1651515746374,\"modified\":1651515746374},\"c0f99e39-85e7-4ef7-99b1-ef52d4ed69b2\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"c0f99e39-85e7-4ef7-99b1-ef52d4ed69b2\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"e35a066b-eb0e-4b05-a4c9-cc31dc202572\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1651515746373,\"location\":\"mine\",\"persisted\":1651515746373}}"
|
||||
},
|
||||
{
|
||||
"name": "mct-tree-expanded",
|
||||
"value": "[]"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@ -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());
|
||||
|
25
package.json
25
package.json
@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "openmct",
|
||||
"version": "2.0.2",
|
||||
"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.4",
|
||||
"@percy/playwright": "1.0.2",
|
||||
"@playwright/test": "1.19.2",
|
||||
"@playwright/test": "1.21.1",
|
||||
"@types/eventemitter3": "^1.0.0",
|
||||
"@types/jasmine": "^4.0.1",
|
||||
"@types/karma": "^6.3.2",
|
||||
@ -20,12 +20,12 @@
|
||||
"copy-webpack-plugin": "10.2.0",
|
||||
"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",
|
||||
"d3-axis": "3.0.0",
|
||||
"d3-scale": "3.3.0",
|
||||
"d3-selection": "3.0.0",
|
||||
"eslint": "8.13.0",
|
||||
"eslint-plugin-compat": "4.0.2",
|
||||
"eslint-plugin-playwright": "0.8.0",
|
||||
"eslint-plugin-playwright": "0.9.0",
|
||||
"eslint-plugin-vue": "8.5.0",
|
||||
"eslint-plugin-you-dont-need-lodash-underscore": "6.12.0",
|
||||
"eventemitter3": "1.2.0",
|
||||
@ -37,7 +37,7 @@
|
||||
"imports-loader": "0.8.0",
|
||||
"jasmine-core": "4.0.1",
|
||||
"jsdoc": "3.5.5",
|
||||
"karma": "6.3.17",
|
||||
"karma": "6.3.18",
|
||||
"karma-chrome-launcher": "3.1.1",
|
||||
"karma-cli": "2.0.0",
|
||||
"karma-coverage": "2.1.1",
|
||||
@ -46,14 +46,14 @@
|
||||
"karma-jasmine": "4.0.1",
|
||||
"karma-junit-reporter": "2.0.1",
|
||||
"karma-sourcemap-loader": "0.3.8",
|
||||
"karma-spec-reporter": "0.0.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": "2.29.3",
|
||||
"moment-duration-format": "2.3.2",
|
||||
"moment-timezone": "0.5.34",
|
||||
"node-bourbon": "4.2.3",
|
||||
"painterro": "1.2.56",
|
||||
@ -61,7 +61,7 @@
|
||||
"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",
|
||||
@ -92,8 +92,9 @@
|
||||
"test": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run",
|
||||
"test:firefox": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run --browsers=FirefoxHeadless",
|
||||
"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:ci": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome smoke default condition timeConductor branding clock",
|
||||
"test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome",
|
||||
"test:e2e:updatesnapshots": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome --grep @snapshot --update-snapshots",
|
||||
"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",
|
||||
|
@ -241,7 +241,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());
|
||||
|
@ -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);
|
||||
|
@ -172,6 +172,7 @@ export class TelemetryCollection extends EventEmitter {
|
||||
* @private
|
||||
*/
|
||||
_processNewTelemetry(telemetryData) {
|
||||
performance.mark('tlm:process:start');
|
||||
if (telemetryData === undefined) {
|
||||
return;
|
||||
}
|
||||
@ -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,41 @@ 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);
|
||||
|
||||
this.telemetryCollection.destroy();
|
||||
},
|
||||
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 +207,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", () => {
|
||||
|
@ -27,7 +27,7 @@
|
||||
:href="url"
|
||||
>
|
||||
<div class="c-condition-widget__label">
|
||||
{{ internalDomainObject.conditionalLabel || internalDomainObject.label }}
|
||||
{{ label }}
|
||||
</div>
|
||||
</component>
|
||||
</template>
|
||||
@ -39,28 +39,112 @@ export default {
|
||||
inject: ['openmct', 'domainObject'],
|
||||
data: function () {
|
||||
return {
|
||||
internalDomainObject: this.domainObject
|
||||
conditionalLabel: '',
|
||||
conditionSetIdentifier: null,
|
||||
domainObjectLabel: '',
|
||||
url: null,
|
||||
urlDefined: false,
|
||||
useConditionSetOutputAsLabel: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
urlDefined() {
|
||||
return this.internalDomainObject.url && this.internalDomainObject.url.length > 0;
|
||||
},
|
||||
url() {
|
||||
return this.urlDefined ? sanitizeUrl(this.internalDomainObject.url) : null;
|
||||
label() {
|
||||
return this.useConditionSetOutputAsLabel
|
||||
? this.conditionalLabel
|
||||
: this.domainObjectLabel
|
||||
;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
conditionSetIdentifier: {
|
||||
handler(newValue, oldValue) {
|
||||
if (!oldValue || !newValue || !this.openmct.objects.areIdsEqual(newValue, oldValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.listenToConditionSetChanges();
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.unlisten = this.openmct.objects.observe(this.internalDomainObject, '*', this.updateInternalDomainObject);
|
||||
this.unlisten = this.openmct.objects.observe(this.domainObject, '*', this.updateDomainObject);
|
||||
|
||||
if (this.domainObject) {
|
||||
this.updateDomainObject(this.domainObject);
|
||||
this.listenToConditionSetChanges();
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.conditionSetIdentifier = null;
|
||||
|
||||
if (this.unlisten) {
|
||||
this.unlisten();
|
||||
}
|
||||
|
||||
this.stopListeningToConditionSetChanges();
|
||||
},
|
||||
methods: {
|
||||
updateInternalDomainObject(domainObject) {
|
||||
this.internalDomainObject = domainObject;
|
||||
async listenToConditionSetChanges() {
|
||||
if (!this.conditionSetIdentifier) {
|
||||
return;
|
||||
}
|
||||
|
||||
const conditionSetDomainObject = await this.openmct.objects.get(this.conditionSetIdentifier);
|
||||
this.stopListeningToConditionSetChanges();
|
||||
|
||||
if (!conditionSetDomainObject) {
|
||||
this.openmct.notifications.alert('Unable to find condition set');
|
||||
}
|
||||
|
||||
this.telemetryCollection = this.openmct.telemetry.requestCollection(conditionSetDomainObject, {
|
||||
size: 1,
|
||||
strategy: 'latest'
|
||||
});
|
||||
|
||||
this.telemetryCollection.on('add', this.updateConditionLabel, this);
|
||||
this.telemetryCollection.load();
|
||||
},
|
||||
stopListeningToConditionSetChanges() {
|
||||
if (this.telemetryCollection) {
|
||||
this.telemetryCollection.off('add', this.updateConditionLabel, this);
|
||||
this.telemetryCollection.destroy();
|
||||
this.telemetryCollection = null;
|
||||
}
|
||||
},
|
||||
updateConditionLabel([latestDatum]) {
|
||||
if (!this.conditionSetIdentifier) {
|
||||
this.stopListeningToConditionSetChanges();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.conditionalLabel = latestDatum.output || '';
|
||||
},
|
||||
updateDomainObject(domainObject) {
|
||||
if (this.domainObjectLabel !== domainObject.label) {
|
||||
this.domainObjectLabel = domainObject.label;
|
||||
}
|
||||
|
||||
const urlDefined = domainObject.url && domainObject.url.length > 0;
|
||||
if (this.urlDefined !== urlDefined) {
|
||||
this.urlDefined = urlDefined;
|
||||
}
|
||||
|
||||
const url = this.urlDefined ? sanitizeUrl(domainObject.url) : null;
|
||||
if (this.url !== url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
const conditionSetIdentifier = domainObject.configuration.objectStyles.conditionSetIdentifier;
|
||||
if (this.conditionSetIdentifier !== conditionSetIdentifier) {
|
||||
this.conditionSetIdentifier = conditionSetIdentifier;
|
||||
}
|
||||
|
||||
const useConditionSetOutputAsLabel = this.conditionSetIdentifier && domainObject.configuration.useConditionSetOutputAsLabel;
|
||||
if (this.useConditionSetOutputAsLabel !== useConditionSetOutputAsLabel) {
|
||||
this.useConditionSetOutputAsLabel = useConditionSetOutputAsLabel;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -222,20 +222,20 @@ 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);
|
||||
|
||||
this.telemetryCollection.destroy();
|
||||
|
||||
if (this.mutablePromise) {
|
||||
this.mutablePromise.then(() => {
|
||||
@ -253,34 +253,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 +266,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 +283,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"
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -14,7 +14,7 @@ $elemBg: rgba(black, 0.7);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
z-index: 2;
|
||||
@include userSelectNone;
|
||||
}
|
||||
|
||||
|
@ -55,7 +55,7 @@
|
||||
<div
|
||||
v-if="zoomFactor > 1"
|
||||
class="c-imagery__hints"
|
||||
>Alt-drag to pan</div>
|
||||
>{{formatImageAltText}}</div>
|
||||
<div
|
||||
ref="focusedImageWrapper"
|
||||
class="image-wrapper"
|
||||
@ -143,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">
|
||||
@ -163,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
|
||||
@ -179,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
|
||||
@ -232,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: {
|
||||
@ -272,6 +278,7 @@ export default {
|
||||
imageContainerHeight: undefined,
|
||||
sizedImageWidth: 0,
|
||||
sizedImageHeight: 0,
|
||||
viewHeight: 0,
|
||||
lockCompass: true,
|
||||
resizingWindow: false,
|
||||
timeContext: undefined,
|
||||
@ -290,7 +297,8 @@ export default {
|
||||
imageTranslateY: 0,
|
||||
pan: undefined,
|
||||
animateZoom: true,
|
||||
imagePanned: false
|
||||
imagePanned: false,
|
||||
forceShowThumbnails: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@ -306,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);
|
||||
},
|
||||
@ -314,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;
|
||||
@ -461,6 +488,16 @@ export default {
|
||||
width: this.sizedImageWidth,
|
||||
height: this.sizedImageHeight
|
||||
};
|
||||
},
|
||||
formatImageAltText() {
|
||||
const regexLinux = /Linux/;
|
||||
const navigator = window.navigator.userAgent;
|
||||
|
||||
if (regexLinux.test(navigator)) {
|
||||
return 'Ctrl+Alt drag to pan';
|
||||
}
|
||||
|
||||
return 'Alt drag to pan';
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@ -497,6 +534,8 @@ export default {
|
||||
if (!this.isPaused) {
|
||||
this.setFocusedImage(imageIndex);
|
||||
this.scrollToRight();
|
||||
} else {
|
||||
this.scrollToFocused();
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
@ -536,7 +575,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);
|
||||
@ -583,6 +622,9 @@ export default {
|
||||
|
||||
},
|
||||
methods: {
|
||||
calculateViewHeight() {
|
||||
this.viewHeight = this.$el.clientHeight;
|
||||
},
|
||||
setTimeContext() {
|
||||
this.stopFollowingTimeContext();
|
||||
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
|
||||
@ -956,6 +998,8 @@ export default {
|
||||
}
|
||||
|
||||
this.setSizedImageDimensions();
|
||||
this.calculateViewHeight();
|
||||
this.scrollToFocused();
|
||||
},
|
||||
setSizedImageDimensions() {
|
||||
this.focusedImageNaturalAspectRatio = this.$refs.focusedImage.naturalWidth / this.$refs.focusedImage.naturalHeight;
|
||||
@ -984,6 +1028,8 @@ export default {
|
||||
this.scrollToRight('reset');
|
||||
}
|
||||
|
||||
this.calculateViewHeight();
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.resizingWindow = false;
|
||||
});
|
||||
|
@ -1,3 +1,5 @@
|
||||
@use 'sass:math';
|
||||
|
||||
@keyframes fade-out {
|
||||
from {
|
||||
background-color: rgba($colorOk, 0.5);
|
||||
@ -137,9 +139,12 @@
|
||||
animation-name: fade-out;
|
||||
animation-timing-function: ease-in;
|
||||
animation-iteration-count: 1;
|
||||
animation-fill-mode: forwards;
|
||||
animation-fill-mode: forwards;
|
||||
&.no-animation {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__thumbs-wrapper {
|
||||
display: flex; // Uses row layout
|
||||
@ -161,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 {
|
||||
@ -184,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;
|
||||
@ -209,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -222,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;
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -32,7 +32,7 @@ Add a line to install the CouchDB plugin for OpenMCT:
|
||||
```
|
||||
openmct.install(openmct.plugins.CouchDB("http://localhost:5984/openmct"));
|
||||
```
|
||||
6. Enable cors in CouchDB by editing `/usr/local/etc/local.ini` and add: `
|
||||
6. Enable cors in CouchDB by editing `~/homebrew/etc/local.ini` and add: `
|
||||
```
|
||||
[chttpd]
|
||||
enable_cors = true
|
||||
@ -45,4 +45,4 @@ origins = http://localhost:8080
|
||||
9. Navigate to http://localhost:8080/ and create a random object in OpenMCT (e.g., a `Clock`) and save. You may get an error saying that the objects failed to persist. This is a known error that you can ignore, and will only happen the first time you save.
|
||||
10. Navigate to: http://127.0.0.1:5984/_utils/#database/openmct/_all_docs
|
||||
11. Look at the `JSON` tab and ensure you can see the `Clock` object you created above.
|
||||
12. All done! 🏆
|
||||
12. All done! 🏆
|
||||
|
@ -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,11 @@ 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);
|
||||
} else {
|
||||
return ticks(range.min, range.max, number);
|
||||
}
|
||||
},
|
||||
|
||||
updateTicksForceRegeneration() {
|
||||
@ -193,6 +203,7 @@ export default {
|
||||
|
||||
updateTicks(forceRegeneration = false) {
|
||||
const range = this.axis.get('displayRange');
|
||||
|
||||
if (!range) {
|
||||
delete this.min;
|
||||
delete this.max;
|
||||
|
@ -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) {
|
||||
|
@ -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,6 +19,7 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import { antisymlog, symlog } from '../mathUtils';
|
||||
import Model from './Model';
|
||||
|
||||
/**
|
||||
@ -30,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.
|
||||
@ -53,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'));
|
||||
@ -73,11 +75,6 @@ export default class YAxisModel extends Model {
|
||||
this.seriesCollection.forEach(this.trackSeries, this);
|
||||
this.updateFromSeries(this.seriesCollection);
|
||||
}
|
||||
updateDisplayRange(range) {
|
||||
if (!this.get('autoscale')) {
|
||||
this.set('displayRange', range);
|
||||
}
|
||||
}
|
||||
toggleFreeze(frozen) {
|
||||
if (!frozen) {
|
||||
this.toggleAutoscale(this.get('autoscale'));
|
||||
@ -165,23 +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 {
|
||||
const range = this.get('range');
|
||||
|
||||
if (range) {
|
||||
// If we already have a user-defined range, make sure it maps to the
|
||||
// range we'll actually use for the ticks.
|
||||
this.set('displayRange', range);
|
||||
} else {
|
||||
// Otherwise use the last known displayRange as the initial
|
||||
// values for the user-defined range, so that we don't end up
|
||||
// with any error from an undefined user range.
|
||||
this.set('range', this.get('displayRange'));
|
||||
}
|
||||
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.
|
||||
@ -189,7 +258,7 @@ export default class YAxisModel extends Model {
|
||||
*/
|
||||
updateFromSeries(seriesCollection) {
|
||||
const plotModel = this.plot.get('domainObject');
|
||||
const label = plotModel?.configuration?.yAxis?.label;
|
||||
const label = plotModel.configuration?.yAxis?.label;
|
||||
const sampleSeries = seriesCollection.first();
|
||||
if (!sampleSeries) {
|
||||
if (!label) {
|
||||
@ -202,7 +271,13 @@ 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
|
||||
@ -255,6 +330,7 @@ export default class YAxisModel extends Model {
|
||||
return {
|
||||
frozen: false,
|
||||
autoscale: true,
|
||||
logMode: options.model?.logMode ?? false,
|
||||
autoscalePadding: 0.1
|
||||
|
||||
// 'range' is not specified here, it is undefined at first. When the
|
||||
@ -269,6 +345,7 @@ export default class YAxisModel extends Model {
|
||||
/**
|
||||
@typedef {import('./XAxisModel').AxisModelType & {
|
||||
autoscale: boolean
|
||||
logMode: boolean
|
||||
autoscalePadding: number
|
||||
stats?: import('./XAxisModel').NumberRange
|
||||
values: Array<TODO>
|
||||
|
@ -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"
|
||||
@ -105,6 +118,7 @@ export default {
|
||||
return {
|
||||
label: '',
|
||||
autoscale: '',
|
||||
logMode: false,
|
||||
autoscalePadding: '',
|
||||
rangeMin: '',
|
||||
rangeMax: '',
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -180,11 +191,12 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
};
|
||||
},
|
||||
initFormValues() {
|
||||
this.label = this.yAxis.get('label');
|
||||
this.autoscale = this.yAxis.get('autoscale');
|
||||
this.logMode = this.yAxis.get('logMode');
|
||||
this.autoscalePadding = this.yAxis.get('autoscalePadding');
|
||||
const range = this.yAxis.get('range') ?? this.yAxis.get('displayRange');
|
||||
this.rangeMin = range?.min;
|
||||
@ -202,7 +214,7 @@ export default {
|
||||
}
|
||||
|
||||
let oldVal = this.yAxis.get(formKey);
|
||||
const formField = this.fields.find((field) => field.modelProp === formKey);
|
||||
const formField = this.fields[formKey];
|
||||
|
||||
const validationError = formField.validate?.(newVal, this.yAxis);
|
||||
this.validationErrors[formKey] = validationError;
|
||||
|
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();
|
||||
});
|
||||
@ -694,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();
|
||||
});
|
||||
@ -1086,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', () => {
|
||||
|
@ -132,12 +132,6 @@ export default {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (this.object && this.object.type === 'conditionWidget' && keys.includes('output')) {
|
||||
this.openmct.objects.mutate(this.object, 'conditionalLabel', styleObj.output);
|
||||
} else {
|
||||
this.openmct.objects.mutate(this.object, 'conditionalLabel', '');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -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,42 @@ 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Linear tick generation from d3-array.
|
||||
*/
|
||||
|
@ -77,6 +77,7 @@ define([
|
||||
'./userIndicator/plugin',
|
||||
'../../example/exampleUser/plugin',
|
||||
'./localStorage/plugin',
|
||||
'./gauge/GaugePlugin',
|
||||
'./timelist/plugin'
|
||||
], function (
|
||||
_,
|
||||
@ -135,6 +136,7 @@ define([
|
||||
UserIndicator,
|
||||
ExampleUser,
|
||||
LocalStorage,
|
||||
GaugePlugin,
|
||||
TimeList
|
||||
) {
|
||||
const plugins = {};
|
||||
@ -212,6 +214,7 @@ define([
|
||||
plugins.DeviceClassifier = DeviceClassifier.default;
|
||||
plugins.UserIndicator = UserIndicator.default;
|
||||
plugins.LocalStorage = LocalStorage.default;
|
||||
plugins.Gauge = GaugePlugin.default;
|
||||
plugins.Timelist = TimeList.default;
|
||||
|
||||
return plugins;
|
||||
|
@ -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();
|
||||
},
|
||||
|
@ -366,6 +366,16 @@ $legendHoverValueBg: rgba($colorBodyFg, 0.2);
|
||||
$legendTableHeadBg: $colorTabHeaderBg;
|
||||
$colorPlotLimitLineBg: rgba($colorBodyBg, 0.2);
|
||||
|
||||
// Gauges
|
||||
$colorGaugeBg: pullForward($colorBodyBg, 5%); // Gauge radial area background, meter background
|
||||
$colorGaugeValue: rgba(#fff, 0.3); // Gauge value graphic (radial sweep, bar) color
|
||||
$colorGaugeTextValue: #fff; // Radial gauge text value
|
||||
$colorGaugeMeterTextValue: $colorGaugeTextValue; // Meter text value, overlaid on value bar
|
||||
$colorGaugeRange: $colorBodyFg; // Range text color
|
||||
$colorGaugeLimitHigh: rgba($colorLimitRedBg, 0.4);
|
||||
$colorGaugeLimitLow: $colorGaugeLimitHigh;
|
||||
$transitionTimeGauge: 150ms; // CSS transition time used to smooth needle gauge and meter value transitions
|
||||
$marginGaugeMeterValue: 10%; // Margin between meter value bar and bounds edges
|
||||
// Time Strip and Lists
|
||||
$colorCurrentBg: rgba($colorStatusAlert, 0.3);
|
||||
$colorCurrentFg: pullForward($colorBodyFg, 20%);
|
||||
|
@ -370,6 +370,16 @@ $legendHoverValueBg: rgba($colorBodyFg, 0.2);
|
||||
$legendTableHeadBg: rgba($colorBodyFg, 0.15);
|
||||
$colorPlotLimitLineBg: rgba($colorBodyBg, 0.2);
|
||||
|
||||
// Gauges
|
||||
$colorGaugeBg: pullForward($colorBodyBg, 5%); // Gauge radial area background, meter background
|
||||
$colorGaugeValue: rgba(#fff, 0.3); // Gauge value graphic (radial sweep, bar) color
|
||||
$colorGaugeTextValue: #fff; // Radial gauge text value
|
||||
$colorGaugeMeterTextValue: $colorGaugeTextValue; // Meter text value, overlaid on value bar
|
||||
$colorGaugeRange: $colorBodyFg; // Range text color
|
||||
$colorGaugeLimitHigh: rgba($colorLimitRedBg, 0.4);
|
||||
$colorGaugeLimitLow: $colorGaugeLimitHigh;
|
||||
$transitionTimeGauge: 150ms; // CSS transition time used to smooth needle gauge and meter value transitions
|
||||
$marginGaugeMeterValue: 10%; // Margin between meter value bar and bounds edges
|
||||
// Time Strip and Lists
|
||||
$colorCurrentBg: rgba($colorStatusAlert, 0.3);
|
||||
$colorCurrentFg: pullForward($colorBodyFg, 20%);
|
||||
|
@ -366,6 +366,16 @@ $legendHoverValueBg: rgba($colorBodyFg, 0.2);
|
||||
$legendTableHeadBg: rgba($colorBodyFg, 0.15);
|
||||
$colorPlotLimitLineBg: rgba($colorBodyBg, 0.4);
|
||||
|
||||
// Gauges
|
||||
$colorGaugeBg: pullForward($colorBodyBg, 20%); // Gauge radial area background, meter background
|
||||
$colorGaugeValue: rgba(#000, 0.3); // Gauge value graphic (radial sweep, bar) color
|
||||
$colorGaugeTextValue: pullForward($colorBodyFg, 20%); // Radial gauge text value
|
||||
$colorGaugeMeterTextValue: $colorGaugeTextValue; // Meter text value, overlaid on value bar
|
||||
$colorGaugeRange: $colorBodyFg; // Range text color
|
||||
$colorGaugeLimitHigh: rgba($colorLimitRedBg, 0.2);
|
||||
$colorGaugeLimitLow: $colorGaugeLimitHigh;
|
||||
$transitionTimeGauge: 150ms; // CSS transition time used to smooth needle gauge and meter value transitions
|
||||
$marginGaugeMeterValue: 10%; // Margin between meter value bar and bounds edges
|
||||
// Time Strip and Lists
|
||||
$colorCurrentBg: rgba($colorStatusAlert, 0.3);
|
||||
$colorCurrentFg: pullForward($colorBodyFg, 20%);
|
||||
|
@ -70,8 +70,24 @@
|
||||
padding: $formTBPad $formLRPad;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
&--sub-grid {
|
||||
// 3 columns: <req> <label> <input/control>
|
||||
display: grid;
|
||||
grid-column-gap: $interiorMargin;
|
||||
grid-template-columns: 20px max-content 1fr;
|
||||
grid-row-gap: $interiorMargin;
|
||||
margin-top: $interiorMarginLg;
|
||||
width: max-content;
|
||||
|
||||
.c-form__row {
|
||||
display: contents;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.c-form-row {
|
||||
align-items: start;
|
||||
|
||||
|
@ -53,6 +53,7 @@
|
||||
@import "../ui/toolbar/components/toolbar-checkbox.scss";
|
||||
@import "./notebook.scss";
|
||||
@import "../plugins/notebook/components/sidebar.scss";
|
||||
@import "../plugins/gauge/gauge.scss";
|
||||
|
||||
#splash-screen {
|
||||
display: none;
|
||||
|
@ -213,12 +213,6 @@ export default {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (this.domainObject && this.domainObject.type === 'conditionWidget' && keys.includes('output')) {
|
||||
this.openmct.objects.mutate(this.domainObject, 'conditionalLabel', styleObj.output);
|
||||
} else {
|
||||
this.openmct.objects.mutate(this.domainObject, 'conditionalLabel', '');
|
||||
}
|
||||
},
|
||||
updateView(immediatelySelect) {
|
||||
this.clear();
|
||||
@ -450,4 +444,3 @@ export default {
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -123,7 +123,7 @@ export default {
|
||||
}
|
||||
},
|
||||
drawAxis(bounds, timeSystem) {
|
||||
let viewBounds = Object.assign({}, bounds);
|
||||
let viewBounds = Object.create(bounds);
|
||||
|
||||
this.setScale(viewBounds, timeSystem);
|
||||
this.setAxis(viewBounds);
|
||||
|
@ -166,7 +166,8 @@ export default {
|
||||
}
|
||||
|
||||
return definition.form
|
||||
.map((field) => {
|
||||
.filter(field => !field.hideFromInspector)
|
||||
.map(field => {
|
||||
let path = field.property;
|
||||
if (typeof path === 'string') {
|
||||
path = [path];
|
||||
|
@ -128,7 +128,7 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
const isExistingView = viewProvider.key === this.existingView.key;
|
||||
const isExistingView = viewProvider.key === this.existingView?.key;
|
||||
|
||||
this.clear();
|
||||
|
||||
|
@ -44,7 +44,7 @@ const config = {
|
||||
"bourbon": "bourbon.scss",
|
||||
"plotly-basic": "plotly.js-basic-dist",
|
||||
"plotly-gl2d": "plotly.js-gl2d-dist",
|
||||
"d3-scale": path.join(__dirname, "node_modules/d3-scale/build/d3-scale.min.js"),
|
||||
"d3-scale": path.join(__dirname, "node_modules/d3-scale/dist/d3-scale.min.js"),
|
||||
"printj": path.join(__dirname, "node_modules/printj/dist/printj.min.js"),
|
||||
"styles": path.join(__dirname, "src/styles"),
|
||||
"MCT": path.join(__dirname, "src/MCT"),
|
||||
|
Loading…
x
Reference in New Issue
Block a user