Compare commits

..

9 Commits

Author SHA1 Message Date
John Hill
b02d063b8b Merge branch 'master' into playwright-karma 2022-03-23 09:02:32 -07:00
unlikelyzero
079956d7ae Merge branch 'master' of https://github.com/nasa/openmct into playwright-karma 2022-01-11 08:30:33 -08:00
unlikelyzero
c81c5f7c25 Add Safari Support 2022-01-11 08:29:04 -08:00
John Hill
986083f3c7 Updated config.yml 2022-01-10 19:12:44 -08:00
John Hill
cb7c9fd4f9 Updated config.yml 2022-01-10 18:52:06 -08:00
unlikelyzero
8b8f5c4df4 Move to playwright browsers 2022-01-10 18:41:02 -08:00
unlikelyzero
80fc393b54 Merge branch 'master' of https://github.com/nasa/openmct 2022-01-10 16:12:41 -08:00
unlikelyzero
58138e8554 Merge branch 'master' of https://github.com/nasa/openmct 2021-12-29 17:08:50 -08:00
unlikelyzero
54bed23267 No need for allure reporting in visual tests 2021-12-23 11:11:51 -08:00
60 changed files with 546 additions and 1905 deletions

View File

@@ -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.19.1-focal
environment:
NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed
parameters:
@@ -21,7 +21,7 @@ commands:
- restore_cache_cmd:
node-version: << parameters.node-version >>
- node/install:
install-npm: true
install-npm: false
node-version: << parameters.node-version >>
- run: npm install
restore_cache_cmd:
@@ -47,6 +47,7 @@ commands:
paths:
- ~/.npm
- node_modules
- ~/.cache/ms-playwright
generate_and_store_version_and_filesystem_artifacts:
description: "Track important packages and files"
steps:
@@ -64,7 +65,6 @@ 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
jobs:
npm-audit:
parameters:
@@ -96,26 +96,9 @@ jobs:
steps:
- build_and_install:
node-version: <<parameters.node-version>>
- when:
condition:
equal: [ "FirefoxESR", <<parameters.browser>> ]
steps:
- browser-tools/install-firefox:
version: "91.7.1esr" #https://archive.mozilla.org/pub/firefox/releases/
- when:
condition:
equal: [ "FirefoxHeadless", <<parameters.browser>> ]
steps:
- browser-tools/install-firefox
- when:
condition:
equal: [ "ChromeHeadless", <<parameters.browser>> ]
steps:
- browser-tools/install-chrome:
replace-existing: false
- run: npm run test -- --browsers=<<parameters.browser>>
- save_cache_cmd:
node-version: <<parameters.node-version>>
- run: npm run test:coverage -- --browsers=<<parameters.browser>>
- store_test_results:
path: dist/reports/tests/
- store_artifacts:
@@ -142,12 +125,15 @@ workflows:
overall-circleci-commit-status: #These jobs run on every commit
jobs:
- lint:
name: node16-lint
node-version: lts/gallium
- unit-test:
name: node12-chrome
node-version: lts/erbium
browser: PlaywrightChrome
- unit-test:
name: node14-chrome
node-version: lts/fermium
browser: ChromeHeadless
browser: PlaywrightChrome
post-steps:
- upload_code_covio
- unit-test:
@@ -158,20 +144,24 @@ workflows:
name: e2e-ci
node-version: lts/gallium
suite: ci
the-nightly: #These jobs do not run on PRs, but against master at night
the-nightly: #These jobs do not run on PRs, but against master each night
jobs:
- unit-test:
name: node16-firefoxESR-nightly
node-version: lts/gallium
browser: FirefoxESR
name: node12-chrome-nightly
node-version: lts/erbium
browser: PlaywrightChrome
- unit-test:
name: node14-firefox-nightly
node-version: lts/fermium
browser: FirefoxHeadless
browser: PlaywrightFirefox
- unit-test:
name: node14-chrome-nightly
name: node14-chrome-canary-nightly
node-version: lts/fermium
browser: ChromeHeadless
browser: PlaywrightChromeCanary
- unit-test:
name: node14-safari-nightly
node-version: lts/fermium
browser: PlaywrightSafari
- unit-test:
name: node16-chrome-nightly
node-version: lts/gallium

View File

@@ -8,7 +8,7 @@ on:
jobs:
e2e-full:
if: ${{ github.event_name == 'pull_request' && github.event.action == 'labeled' && github.event.label.name == 'pr:e2e' }} || ${{ github.event_name == 'pull_request' && github.event.action == 'opened' }}
if: ${{ github.event.label.name == 'pr:e2e' }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
@@ -30,18 +30,8 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: '16'
check-latest: true
- name: Cache node modules
uses: actions/cache@v3
env:
cache-name: cache-node-modules
with:
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
- run: npx playwright@1.19.2 install
- run: npm install
- run: npx playwright install
- run: npm run test:e2e:full
- name: Archive test results
uses: actions/upload-artifact@v2

View File

@@ -10,25 +10,15 @@ on:
jobs:
e2e-visual:
if: ${{ github.event.label.name == 'pr:visual' }} || ${{ github.event.workflow_dispatch }} || ${{ github.event.schedule }} || ${{ github.event_name == 'pull_request' && github.event.action == 'opened' }}
if: ${{ github.event.label.name == 'pr:visual' }} || ${{ github.event.workflow_dispatch }} || ${{ github.event.schedule }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '16'
check-latest: true
- name: Cache node modules
uses: actions/cache@v3
env:
cache-name: cache-node-modules
with:
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
- run: npx playwright@1.19.2 install
- run: npm install
- run: npx playwright install
- name: Run the e2e visual tests
run: npm run test:e2e:visual
env:

21
.github/workflows/e2e.yml vendored Normal file
View File

@@ -0,0 +1,21 @@
name: "e2e"
on:
workflow_dispatch:
inputs:
version:
description: 'Which branch do you want to test?' # Limited to branch for now
required: false
default: 'master'
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
ref: ${{ github.event.inputs.version }}
- uses: actions/setup-node@v3
with:
node-version: '16'
- run: npm install
- name: Run the e2e tests
run: npm run test:e2e:ci

View File

@@ -6,6 +6,11 @@ on:
description: 'Which branch do you want to test?' # Limited to branch for now
required: false
default: 'master'
pull_request:
types:
- labeled
schedule:
- cron: '28 21 * * 1-5'
jobs:
lighthouse-pr:
if: ${{ github.event.label.name == 'pr:lighthouse' }}
@@ -15,10 +20,10 @@ jobs:
uses: actions/checkout@v3
with:
ref: master #explicitly checkout master for baseline
- name: Install Node 16
- name: Install Node 14
uses: actions/setup-node@v3
with:
node-version: '16'
node-version: '14'
- name: Cache node modules
uses: actions/cache@v2
env:
@@ -49,11 +54,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Node 16
- name: Install Node 14
uses: actions/setup-node@v3
with:
node-version: '16'
check-latest: true
node-version: '14'
- name: Cache node modules
uses: actions/cache@v2
env:
@@ -79,10 +83,9 @@ jobs:
- name: Install Node 14
uses: actions/setup-node@v3
with:
node-version: '16'
check-latest: true
node-version: '14'
- name: Cache node modules
uses: actions/cache@v3
uses: actions/cache@v2
env:
cache-name: cache-node-modules
with:

View File

@@ -27,7 +27,6 @@ jobs:
with:
node-version: 16
registry-url: https://registry.npmjs.org/
check-latest: true
- run: npm install
- run: npm publish --access public --tag unstable
env:

View File

@@ -12,14 +12,13 @@ jobs:
fail-fast: false
matrix:
os:
- ubuntu-20.04 #MMOC Ubuntu version
- ubuntu-latest
- macos-latest
- macos-10.15
- windows-latest
node_version:
- 12
- 14
- 16
- 17
architecture:
- x64
name: Node ${{ matrix.node_version }} - ${{ matrix.architecture }} on ${{ matrix.os }}
@@ -30,16 +29,6 @@ jobs:
with:
node-version: ${{ matrix.node_version }}
architecture: ${{ matrix.architecture }}
check-latest: true
- name: Cache node modules
uses: actions/cache@v3
env:
cache-name: cache-node-modules
with:
path: ~/.npm
key: ${{ runner.os }}-${{ matrix.node_version }}--build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
restore-keys: |
${{ runner.os }}-${{ matrix.node_version }}--build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
- run: npm install
- run: npm test
- run: npm run lint -- --quiet

View File

@@ -1,27 +1,44 @@
# Ignore everything first (will not ignore special files like LICENSE.md,
# README.md, and package.json)...
/**/*
*.scssc
*.zip
*.gzip
*.tgz
*.DS_Store
# ...but include these folders...
!/dist/**/*
!/src/**/*
*.sass-cache
*COMPILE.css
# We might be able to remove this if it is not imported by any project directly.
# https://github.com/nasa/openmct/issues/4992
!/example/**/*
# Intellij project configuration files
*.idea
*.iml
# We will remove this in https://github.com/nasa/openmct/issues/4922
!/app.js
# External dependencies
# ...except for these files in the above folders.
/src/**/*Spec.js
/src/**/test/
# TODO move test utils into test/ folders
/src/utils/testing.js
# Build output
target
# Also include these special top-level files.
!copyright-notice.js
!copyright-notice.html
!index.html
!openmct.js
!SECURITY.md
# Mac OS X Finder
.DS_Store
# Closed source libraries
closed-lib
# Node, Bower dependencies
node_modules
bower_components
Procfile
# Protractor logs
protractor/logs
# npm-debug log
npm-debug.log
# Infra and tests
.circleci
.github
e2e
codecov.yml
lighthouserc.yml
*.Spec.js
karma.conf.js

2
app.js
View File

@@ -64,7 +64,7 @@ app.use(require('webpack-dev-middleware')(
compiler,
{
publicPath: '/dist',
stats: 'errors-warnings'
logLevel: 'warn'
}
));

View File

@@ -26,7 +26,6 @@ const config = {
reporter: [
['list'],
['junit', { outputFile: 'test-results/results.xml' }],
['allure-playwright']
]
};

View File

@@ -157,10 +157,5 @@ test.describe('Sine Wave Generator', () => {
y: 28
}
});
// Verify that where we click on canvas shows the number we clicked on
// Note that any number will do, we just care that a number exists
await expect(page.locator('.value-to-display-nearestValue')).toContainText(/[+-]?([0-9]*[.])?[0-9]+/);
});
});

View File

@@ -1,42 +1,42 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
This test suite is dedicated to tests which verify the basic operations surrounding moving objects.
*/
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.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.
});
});
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
This test suite is dedicated to tests which verify the basic operations surrounding moving objects.
*/
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.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.
});
});

View File

@@ -1,46 +1,46 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
This test suite is dedicated to tests which verify the basic operations surrounding importAsJSON.
*/
const { test, expect } = require('@playwright/test');
test.describe('ExportAsJSON', () => {
test.fixme('Verify that domain object can be importAsJSON from Tree', async ({ page }) => {
//Verify that an testdata JSON file can be imported from Tree
//Verify correctness of imported domain object
});
test.fixme('Verify that domain object can be importAsJSON from 3 dot menu on folder', async ({ page }) => {
//Verify that an testdata JSON file can be imported from 3 dot menu on folder domain object
//Verify correctness of imported domain object
});
test.fixme('Verify that a nested Objects can be importAsJSON', async ({ page }) => {
// Testdata with hierarchy
// ImportAsJSON on Tree
// Verify Hierarchy
});
test.fixme('Verify that the ImportAsJSON dropdown does not appear for the item X', async ({ page }) => {
// Other than non-persistible objects
});
});
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
This test suite is dedicated to tests which verify the basic operations surrounding importAsJSON.
*/
const { test, expect } = require('@playwright/test');
test.describe('ExportAsJSON', () => {
test.fixme('Verify that domain object can be importAsJSON from Tree', async ({ page }) => {
//Verify that an testdata JSON file can be imported from Tree
//Verify correctness of imported domain object
});
test.fixme('Verify that domain object can be importAsJSON from 3 dot menu on folder', async ({ page }) => {
//Verify that an testdata JSON file can be imported from 3 dot menu on folder domain object
//Verify correctness of imported domain object
});
test.fixme('Verify that a nested Objects can be importAsJSON', async ({ page }) => {
// Testdata with hierarchy
// ImportAsJSON on Tree
// Verify Hierarchy
});
test.fixme('Verify that the ImportAsJSON dropdown does not appear for the item X', async ({ page }) => {
// Other than non-persistible objects
});
});

View File

@@ -1,66 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
This test suite is dedicated to tests which verify the basic operations surrounding Clock.
*/
const { test, expect } = require('@playwright/test');
test.describe('Clock Generator', () => {
test('Timezone dropdown will collapse when clicked outside or on dropdown icon again', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/4878'
});
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
//Click the Create button
await page.click('button:has-text("Create")');
// Click Clock
await page.click('text=Clock');
// Click .icon-arrow-down
await page.locator('.icon-arrow-down').click();
//verify if the autocomplete dropdown is visible
await expect(page.locator(".optionPreSelected")).toBeVisible();
// Click .icon-arrow-down
await page.locator('.icon-arrow-down').click();
// Verify clicking on the autocomplete arrow collapses the dropdown
await expect(page.locator(".optionPreSelected")).not.toBeVisible();
// Click timezone input to open dropdown
await page.locator('.autocompleteInput').click();
//verify if the autocomplete dropdown is visible
await expect(page.locator(".optionPreSelected")).toBeVisible();
// Verify clicking outside the autocomplete dropdown collapses it
await page.locator('text=Timezone').click();
// Verify clicking on the autocomplete arrow collapses the dropdown
await expect(page.locator(".optionPreSelected")).not.toBeVisible();
});
});

View File

@@ -1,217 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
This test suite is dedicated to tests which verify the basic operations surrounding imagery,
but only assume that example imagery is present.
*/
const { test, expect } = require('@playwright/test');
test.describe('Example Imagery', () => {
test.beforeEach(async ({ page }) => {
page.on('console', msg => console.log(msg.text()))
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
//Click the Create button
await page.click('button:has-text("Create")');
// Click text=Example Imagery
await page.click('text=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')
]);
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
});
const backgroundImageSelector = '.c-imagery__main-image__background-image';
test('Can use Mouse Wheel to zoom in and out of latest image', async ({ page }) => {
const bgImageLocator = await page.locator(backgroundImageSelector);
const deltaYStep = 100; //equivalent to 1x zoom
await bgImageLocator.hover();
const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox();
// zoom in
await bgImageLocator.hover();
await page.mouse.wheel(0, deltaYStep * 2);
// wait for zoom animation to finish
await bgImageLocator.hover();
const imageMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox();
// zoom out
await bgImageLocator.hover();
await page.mouse.wheel(0, -deltaYStep);
// wait for zoom animation to finish
await bgImageLocator.hover();
const imageMouseZoomedOut = await page.locator(backgroundImageSelector).boundingBox();
expect(imageMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height);
expect(imageMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width);
expect(imageMouseZoomedOut.height).toBeLessThan(imageMouseZoomedIn.height);
expect(imageMouseZoomedOut.width).toBeLessThan(imageMouseZoomedIn.width);
});
test('Can use alt+drag to move around image once zoomed in', async ({ page }) => {
const deltaYStep = 100; //equivalent to 1x zoom
const bgImageLocator = await page.locator(backgroundImageSelector);
await bgImageLocator.hover();
// zoom in
await page.mouse.wheel(0, deltaYStep * 2);
await bgImageLocator.hover();
const zoomedBoundingBox = await bgImageLocator.boundingBox();
const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
// move to the right
// center the mouse pointer
await page.mouse.move(imageCenterX, imageCenterY);
// pan right
await page.keyboard.down('Alt');
await page.mouse.down();
await page.mouse.move(imageCenterX - 200, imageCenterY, 10);
await page.mouse.up();
await page.keyboard.up('Alt');
const afterRightPanBoundingBox = await bgImageLocator.boundingBox();
expect(zoomedBoundingBox.x).toBeGreaterThan(afterRightPanBoundingBox.x);
// pan left
await page.keyboard.down('Alt');
await page.mouse.down();
await page.mouse.move(imageCenterX, imageCenterY, 10);
await page.mouse.up();
await page.keyboard.up('Alt');
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 page.mouse.down();
await page.mouse.move(imageCenterX, imageCenterY + 200, 10);
await page.mouse.up();
await page.keyboard.up('Alt');
const afterUpPanBoundingBox = await bgImageLocator.boundingBox();
expect(afterUpPanBoundingBox.y).toBeGreaterThan(afterLeftPanBoundingBox.y);
// pan down
await page.keyboard.down('Alt');
await page.mouse.down();
await page.mouse.move(imageCenterX, imageCenterY - 200, 10);
await page.mouse.up();
await page.keyboard.up('Alt');
const afterDownPanBoundingBox = await bgImageLocator.boundingBox();
expect(afterDownPanBoundingBox.y).toBeLessThan(afterUpPanBoundingBox.y);
});
test('Can use + - buttons to zoom on the image', async ({ page }) => {
const bgImageLocator = await page.locator(backgroundImageSelector);
await bgImageLocator.hover();
const zoomInBtn = await page.locator('.t-btn-zoom-in');
const zoomOutBtn = await page.locator('.t-btn-zoom-out');
const initialBoundingBox = await bgImageLocator.boundingBox();
await zoomInBtn.click();
await zoomInBtn.click();
// wait for zoom animation to finish
await bgImageLocator.hover();
const zoomedInBoundingBox = await bgImageLocator.boundingBox();
expect(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
expect(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
await zoomOutBtn.click();
// wait for zoom animation to finish
await bgImageLocator.hover();
const zoomedOutBoundingBox = await bgImageLocator.boundingBox();
expect(zoomedOutBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height);
expect(zoomedOutBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width);
});
test('Can use the reset button to reset the image', async ({ page }) => {
const bgImageLocator = await page.locator(backgroundImageSelector);
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();
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();
await bgImageLocator.hover();
const resetBoundingBox = await bgImageLocator.boundingBox();
expect.soft(resetBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height);
expect.soft(resetBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width);
expect.soft(resetBoundingBox.height).toEqual(initialBoundingBox.height);
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.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.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.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');
});

View File

@@ -22,14 +22,14 @@
/*
Collection of Visual Tests set to run in a default context. The tests within this suite
are only meant to run against openmct's app.js started by `npm run start` within the
are only meant to run against openmct's app.js started by `npm run start` within the
`./e2e/playwright-visual.config.js` file.
These should only use functional expect statements to verify assumptions about the state
These should only use functional expect statements to verify assumptions about the state
in a test and not for functional verification of correctness. Visual tests are not supposed
to "fail" on assertions. Instead, they should be used to detect changes between builds or branches.
Note: Larger testsuite sizes are OK due to the setup time associated with these tests.
Note: Larger testsuite sizes are OK due to the setup time associated with these tests.
*/
const { test, expect } = require('@playwright/test');

View File

@@ -35,8 +35,8 @@ define([
phase: 0
};
function GeneratorProvider(openmct) {
this.workerInterface = new WorkerInterface(openmct);
function GeneratorProvider() {
this.workerInterface = new WorkerInterface();
}
GeneratorProvider.prototype.canProvideTelemetry = function (domainObject) {

View File

@@ -21,13 +21,20 @@
*****************************************************************************/
define([
'raw-loader!./generatorWorker.js',
'uuid'
], function (
workerText,
uuid
) {
function WorkerInterface(openmct) {
// eslint-disable-next-line no-undef
const workerUrl = `${openmct.getAssetPath()}${__OPENMCT_ROOT_RELATIVE__}generatorWorker.js`;
var workerBlob = new Blob(
[workerText],
{type: 'application/javascript'}
);
var workerUrl = URL.createObjectURL(workerBlob);
function WorkerInterface() {
this.worker = new Worker(workerUrl);
this.worker.onmessage = this.onMessage.bind(this);
this.callbacks = {};

View File

@@ -146,7 +146,7 @@ define([
}
});
openmct.telemetry.addProvider(new GeneratorProvider(openmct));
openmct.telemetry.addProvider(new GeneratorProvider());
openmct.telemetry.addProvider(new GeneratorMetadataProvider());
openmct.telemetry.addProvider(new SinewaveLimitProvider());
};

View File

@@ -38,10 +38,6 @@ module.exports = (config) => {
{
pattern: 'dist/inMemorySearchWorker.js*',
included: false
},
{
pattern: 'dist/generatorWorker.js*',
included: false
}
],
port: 9876,
@@ -62,6 +58,22 @@ module.exports = (config) => {
FirefoxESR: {
base: 'FirefoxHeadless',
name: 'FirefoxESR'
},
PlaywrightChrome: {
base: 'ChromiumHeadless'
},
PlaywrightSafari: {
base: 'WebKitHeadless'
},
PlaywrightChromeCanary: {
base: 'ChromiumHeadless',
launchOptions: {
channel: 'chrome-canary'
}
},
PlaywrightFirefox: {
base: 'FirefoxHeadless',
debug: true
}
},
colors: true,
@@ -91,12 +103,17 @@ module.exports = (config) => {
showSpecTiming: true,
failFast: false
},
plugins: [
'karma-*',
'@onslip/karma-playwright-launcher'
],
preprocessors: {
'indexTest.js': ['webpack', 'sourcemap']
},
webpack: webpackConfig,
webpackMiddleware: {
stats: 'errors-warnings'
stats: 'errors-only',
logLevel: 'warn'
},
concurrency: 1,
singleRun: true,

View File

@@ -1,18 +1,14 @@
{
"name": "openmct",
"version": "2.0.2-SNAPSHOT",
"version": "2.0.1-SNAPSHOT",
"description": "The Open MCT core platform",
"devDependencies": {
"@babel/eslint-parser": "7.16.3",
"@braintree/sanitize-url": "6.0.0",
"@percy/cli": "1.0.0-beta.76",
"@onslip/karma-playwright-launcher": "0.2.1",
"@percy/cli": "1.0.0-beta.75",
"@percy/playwright": "1.0.1",
"@playwright/test": "1.19.2",
"@types/eventemitter3": "^1.0.0",
"@types/jasmine": "^4.0.1",
"@types/karma": "^6.3.2",
"@types/lodash": "^4.14.178",
"@types/mocha": "^9.1.0",
"allure-playwright": "2.0.0-beta.15",
"babel-loader": "8.2.3",
"babel-plugin-istanbul": "6.1.1",
@@ -32,14 +28,16 @@
"eventemitter3": "1.2.0",
"exports-loader": "0.7.0",
"express": "4.13.1",
"file-loader": "6.2.0",
"file-saver": "2.0.5",
"git-rev-sync": "3.0.2",
"html-loader": "0.5.5",
"html2canvas": "1.4.1",
"imports-loader": "0.8.0",
"jasmine-core": "4.0.1",
"jsdoc": "3.5.5",
"karma": "6.3.15",
"karma-chrome-launcher": "3.1.1",
"karma-chrome-launcher": "3.1.0",
"karma-cli": "2.0.0",
"karma-coverage": "2.1.1",
"karma-coverage-istanbul-reporter": "3.0.3",
@@ -49,22 +47,22 @@
"karma-sourcemap-loader": "0.3.8",
"karma-spec-reporter": "0.0.33",
"karma-webpack": "5.0.0",
"lighthouse": "9.5.0",
"location-bar": "3.0.1",
"lodash": "4.17.21",
"mini-css-extract-plugin": "2.6.0",
"lodash": "4.17.12",
"mini-css-extract-plugin": "2.4.5",
"moment": "2.29.1",
"moment-duration-format": "2.2.2",
"moment-timezone": "0.5.34",
"moment-timezone": "0.5.28",
"node-bourbon": "4.2.3",
"painterro": "1.2.56",
"plotly.js-basic-dist": "2.5.0",
"plotly.js-gl2d-dist": "2.5.0",
"printj": "1.3.1",
"request": "2.88.2",
"raw-loader": "4.0.2",
"request": "2.69.0",
"resolve-url-loader": "4.0.0",
"sass": "1.49.9",
"sass-loader": "12.6.0",
"sass": "1.49.0",
"sass-loader": "12.4.0",
"sinon": "13.0.1",
"style-loader": "^1.0.1",
"uuid": "3.3.3",
@@ -74,13 +72,13 @@
"vue-template-compiler": "2.6.14",
"webpack": "5.68.0",
"webpack-cli": "4.9.2",
"webpack-dev-middleware": "5.3.1",
"webpack-hot-middleware": "2.25.1",
"webpack-dev-middleware": "3.7.3",
"webpack-hot-middleware": "2.22.3",
"webpack-merge": "5.8.0",
"zepto": "1.2.0"
},
"scripts": {
"clean": "rm -rf ./dist ./node_modules ./package-lock.json",
"clean": "rm -rf ./dist ./node_modules; rm package-lock.json",
"clean-test-lint": "npm run clean; npm install; npm run test; npm run lint",
"start": "node app.js",
"lint": "eslint example src --ext .js,.vue openmct.js",
@@ -93,6 +91,11 @@
"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:coverage": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" COVERAGE=true karma start --single-run",
"test:coverage:pw": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" COVERAGE=true karma start --single-run --browsers=PlaywrightChrome",
"test:coverage:pwsafari": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" COVERAGE=true karma start --single-run --browsers=PlaywrightSafari",
"test:coverage:pwcanary": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" COVERAGE=true karma start --single-run --browsers=PlaywrightChromeCanary",
"test:coverage:pwfirefox": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run --browsers=PlaywrightFirefox",
"test:e2e:ci": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome smoke default condition timeConductor",
"test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome",
"test:e2e:visual": "percy exec --config ./e2e/.percy.yml -- npx playwright test --config=e2e/playwright-visual.config.js default",
@@ -110,7 +113,7 @@
"url": "https://github.com/nasa/openmct.git"
},
"engines": {
"node": ">=14.19.1"
"node": ">=12.22.0"
},
"browserslist": [
"Firefox ESR",

View File

@@ -65,11 +65,10 @@ export default class FormControl {
*/
_getControlViewProvider(control) {
const self = this;
let rowComponent;
return {
show(element, model, onChange) {
rowComponent = new Vue({
const rowComponent = new Vue({
el: element,
components: {
FormControlComponent: DEFAULT_CONTROLS_MAP[control]
@@ -87,9 +86,6 @@ export default class FormControl {
});
return rowComponent;
},
destroy() {
rowComponent.$destroy();
}
};
}

View File

@@ -22,19 +22,17 @@
<template>
<div class="form-control autocomplete">
<span class="autocompleteInputAndArrow">
<input
v-model="field"
class="autocompleteInput"
type="text"
@click="inputClicked()"
@keydown="keyDown($event)"
>
<span
class="icon-arrow-down"
@click="arrowClicked()"
></span>
</span>
<input
v-model="field"
class="autocompleteInput"
type="text"
@click="inputClicked()"
@keydown="keyDown($event)"
>
<span
class="icon-arrow-down"
@click="arrowClicked()"
></span>
<div
class="autocompleteOptions"
@blur="hideOptions = true"
@@ -110,21 +108,10 @@ export default {
this.$emit('onChange', data);
}
},
hideOptions(newValue) {
if (!newValue) {
// adding a event listener when the hideOpntions is false (dropdown is visible)
// handleoutsideclick can collapse the dropdown when clicked outside autocomplete
document.body.addEventListener('click', this.handleOutsideClick);
} else {
//removing event listener when hideOptions become true (dropdown is collapsed)
document.body.removeEventListener('click', this.handleOutsideClick);
}
}
},
mounted() {
this.options = this.model.options;
this.autocompleteInputAndArrow = this.$el.getElementsByClassName('autocompleteInputAndArrow')[0];
this.autocompleteInputElement = this.$el.getElementsByClassName('autocompleteInput')[0];
if (this.options[0].name) {
// If "options" include name, value pair
@@ -136,9 +123,6 @@ export default {
this.optionNames = this.options;
}
},
destroyed() {
document.body.removeEventListener('click', this.handleOutsideClick);
},
methods: {
decrementOptionIndex() {
if (this.optionIndex === 0) {
@@ -192,21 +176,7 @@ export default {
// to show them all the options
this.showFilteredOptions = false;
this.autocompleteInputElement.select();
if (this.hideOptions) {
this.showOptions();
} else {
this.hideOptions = true;
}
},
handleOutsideClick(event) {
// if click event is detected outside autocomplete (both input & arrow) while the
// dropdown is visible, this will collapse the dropdown.
const clickedInsideAutocomplete = this.autocompleteInputAndArrow.contains(event.target);
if (!clickedInsideAutocomplete && !this.hideOptions) {
this.hideOptions = true;
}
this.showOptions();
},
optionMouseover(optionId) {
this.optionIndex = optionId;

View File

@@ -365,7 +365,3 @@ class TimeContext extends EventEmitter {
}
export default TimeContext;
/**
@typedef {{start: number, end: number}} Bounds
*/

View File

@@ -15,8 +15,7 @@ export default function (folderName, couchPlugin, searchFilter) {
return Promise.resolve({
identifier,
type: 'folder',
name: folderName || "CouchDB Documents",
location: 'ROOT'
name: folderName || "CouchDB Documents"
});
}
}

View File

@@ -85,8 +85,7 @@ describe('the plugin', function () {
expect(object).toEqual({
identifier,
type: 'folder',
name: 'CouchDB Documents',
location: 'ROOT'
name: "CouchDB Documents"
});
});
});

View File

@@ -166,7 +166,7 @@ export default {
},
computed: {
gridSize() {
return this.domainObject.configuration.layoutGrid.map(Number);
return this.domainObject.configuration.layoutGrid;
},
layoutItems() {
return this.domainObject.configuration.items;

View File

@@ -43,7 +43,7 @@
<template v-for="(container, index) in containers">
<drop-hint
v-if="index === 0 && containers.length > 1"
:key="`hint-top-${container.id}`"
:key="index"
class="c-fl-frame__drop-hint"
:index="-1"
:allow-drop="allowContainerDrop"
@@ -51,7 +51,7 @@
/>
<container-component
:key="`component-${container.id}`"
:key="container.id"
class="c-fl__container"
:index="index"
:container="container"
@@ -66,7 +66,7 @@
<resize-handle
v-if="index !== (containers.length - 1)"
:key="`handle-${container.id}`"
:key="index"
:index="index"
:orientation="rowsLayout ? 'vertical' : 'horizontal'"
:is-editing="isEditing"
@@ -77,7 +77,7 @@
<drop-hint
v-if="containers.length > 1"
:key="`hint-bottom-${container.id}`"
:key="index"
class="c-fl-frame__drop-hint"
:index="index"
:allow-drop="allowContainerDrop"

View File

@@ -1,273 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
<template>
<div class="h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover c-image-controls__controls">
<div class="c-image-controls__control c-image-controls__zoom icon-magnify">
<div class="c-button-set c-button-set--strip-h">
<button
class="c-button t-btn-zoom-out icon-minus"
title="Zoom out"
@click="zoomOut"
></button>
<button
class="c-button t-btn-zoom-in icon-plus"
title="Zoom in"
@click="zoomIn"
></button>
</div>
<button
class="c-button t-btn-zoom-lock"
title="Lock current zoom and pan across all images"
:class="{'icon-unlocked': !panZoomLocked, 'icon-lock': panZoomLocked}"
@click="toggleZoomLock"
></button>
<button
class="c-button icon-reset t-btn-zoom-reset"
title="Remove zoom and pan"
@click="handleResetImage"
></button>
<span class="c-image-controls__zoom-factor">x{{ formattedZoomFactor }}</span>
</div>
<div class="c-image-controls__control c-image-controls__brightness-contrast">
<span
class="c-image-controls__sliders"
draggable="true"
@dragstart.stop.prevent
>
<div class="c-image-controls__input icon-brightness">
<input
v-model="filters.contrast"
type="range"
min="0"
max="500"
@change="notifyFiltersChanged"
>
</div>
<div class="c-image-controls__input icon-contrast">
<input
v-model="filters.brightness"
type="range"
min="0"
max="500"
@change="notifyFiltersChanged"
>
</div>
</span>
<span class="t-reset-btn-holder c-imagery__lc__reset-btn c-image-controls__btn-reset">
<button
class="c-icon-link icon-reset t-btn-reset"
@click="handleResetFilters"
></button>
</span>
</div>
</div>
</template>
<script>
import _ from 'lodash';
const DEFAULT_FILTER_VALUES = {
brightness: '100',
contrast: '100'
};
const ZOOM_LIMITS_MAX_DEFAULT = 20;
const ZOOM_LIMITS_MIN_DEFAULT = 1;
const ZOOM_STEP = 1;
const ZOOM_WHEEL_SENSITIVITY_REDUCTION = 0.01;
export default {
inject: ['openmct', 'domainObject'],
props: {
zoomFactor: {
type: Number,
required: true
},
imageUrl: String
},
data() {
return {
altPressed: false,
shiftPressed: false,
metaPressed: false,
panZoomLocked: false,
wheelZooming: false,
filters: {
brightness: 100,
contrast: 100
}
};
},
computed: {
formattedZoomFactor() {
return Number.parseFloat(this.zoomFactor).toPrecision(2);
},
cursorStates() {
const isPannable = this.altPressed && this.zoomFactor > 1;
const showCursorZoomIn = this.metaPressed && !this.shiftPressed;
const showCursorZoomOut = this.metaPressed && this.shiftPressed;
const modifierKeyPressed = Boolean(this.metaPressed || this.shiftPressed || this.altPressed);
return {
isPannable,
showCursorZoomIn,
showCursorZoomOut,
modifierKeyPressed
};
}
},
watch: {
imageUrl(newUrl, oldUrl) {
// reset image pan/zoom if newUrl only if not locked
if (newUrl && !this.panZoomLocked) {
this.$emit('resetImage');
}
},
cursorStates(states) {
this.$emit('cursorsUpdated', states);
}
},
mounted() {
document.addEventListener('keydown', this.handleKeyDown);
document.addEventListener('keyup', this.handleKeyUp);
this.clearWheelZoom = _.debounce(this.clearWheelZoom, 600);
},
beforeDestroy() {
document.removeEventListener('keydown', this.handleKeyDown);
document.removeEventListener('keyup', this.handleKeyUp);
},
methods: {
handleResetImage() {
this.$emit('resetImage');
},
handleUpdatePanZoom(options) {
this.$emit('panZoomUpdated', options);
},
toggleZoomLock() {
this.panZoomLocked = !this.panZoomLocked;
},
notifyFiltersChanged() {
this.$emit('filtersUpdated', this.filters);
},
handleResetFilters() {
this.filters = DEFAULT_FILTER_VALUES;
this.notifyFiltersChanged();
},
limitZoomRange(factor) {
return Math.min(Math.max(ZOOM_LIMITS_MIN_DEFAULT, factor), ZOOM_LIMITS_MAX_DEFAULT);
},
// used to increment the zoom without knowledge of current level
processZoom(increment, userCoordX, userCoordY) {
const newFactor = this.limitZoomRange(this.zoomFactor + increment);
this.zoomImage(newFactor, userCoordX, userCoordY);
},
zoomImage(newScaleFactor, screenClientX, screenClientY) {
if (!(newScaleFactor || Number.isInteger(newScaleFactor))) {
console.error('Scale factor provided is invalid');
return;
}
if (newScaleFactor > ZOOM_LIMITS_MAX_DEFAULT) {
newScaleFactor = ZOOM_LIMITS_MAX_DEFAULT;
}
if (newScaleFactor <= 0 || newScaleFactor <= ZOOM_LIMITS_MIN_DEFAULT) {
return this.handleResetImage();
}
this.handleUpdatePanZoom({
newScaleFactor,
screenClientX,
screenClientY
});
},
wheelZoom(e) {
// only use x,y coordinates on scrolling in
if (this.wheelZooming === false && e.deltaY > 0) {
this.wheelZooming = true;
// grab first x,y coordinates
this.processZoom(e.deltaY * ZOOM_WHEEL_SENSITIVITY_REDUCTION, e.clientX, e.clientY);
} else {
// ignore subsequent event x,y so scroll drift doesn't occur
this.processZoom(e.deltaY * ZOOM_WHEEL_SENSITIVITY_REDUCTION);
}
// debounced method that will only fire after the scroll series is complete
this.clearWheelZoom();
},
/* debounced method so that wheelZooming state will
** remain true through a zoom event series
*/
clearWheelZoom() {
this.wheelZooming = false;
},
handleKeyDown(event) {
if (event.key === 'Alt') {
this.altPressed = true;
}
if (event.metaKey) {
this.metaPressed = true;
}
if (event.shiftKey) {
this.shiftPressed = true;
}
},
handleKeyUp(event) {
if (event.key === 'Alt') {
this.altPressed = false;
}
this.shiftPressed = false;
if (!event.metaKey) {
this.metaPressed = false;
}
},
zoomIn() {
this.processZoom(ZOOM_STEP);
},
zoomOut() {
this.processZoom(-ZOOM_STEP);
},
// attached to onClick listener in ImageryView
handlePanZoomClick(e) {
if (this.altPressed) {
return this.$emit('startPan', e);
}
if (!(this.metaPressed && e.button === 0)) {
return;
}
const newScaleFactor = this.zoomFactor + (this.shiftPressed ? -ZOOM_STEP : ZOOM_STEP);
this.zoomImage(newScaleFactor, e.clientX, e.clientY);
}
}
};
</script>

View File

@@ -29,78 +29,59 @@
@mouseover="focusElement"
>
<div class="c-imagery__main-image-wrapper has-local-controls">
<ImageControls
ref="imageControls"
:zoom-factor="zoomFactor"
:image-url="imageUrl"
@resetImage="resetImage"
@panZoomUpdated="handlePanZoomUpdate"
@filtersUpdated="setFilters"
@cursorsUpdated="setCursorStates"
@startPan="startPan"
/>
<div class="h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover c-image-controls__controls">
<span
class="c-image-controls__sliders"
draggable="true"
@dragstart="startDrag"
>
<div class="c-image-controls__slider-wrapper icon-brightness">
<input
v-model="filters.brightness"
type="range"
min="0"
max="500"
>
</div>
<div class="c-image-controls__slider-wrapper icon-contrast">
<input
v-model="filters.contrast"
type="range"
min="0"
max="500"
>
</div>
</span>
<span class="t-reset-btn-holder c-imagery__lc__reset-btn c-image-controls__btn-reset">
<a
class="s-icon-button icon-reset t-btn-reset"
@click="filters={brightness: 100, contrast: 100}"
></a>
</span>
</div>
<div
ref="imageBG"
class="c-imagery__main-image__bg"
:class="{
'paused unnsynced': isPaused && !isFixed,
'stale': false,
'pannable': cursorStates.isPannable,
'cursor-zoom-in': cursorStates.showCursorZoomIn,
'cursor-zoom-out': cursorStates.showCursorZoomOut
}"
:class="{'paused unnsynced': isPaused && !isFixed,'stale':false }"
@click="expand"
>
<div
v-if="zoomFactor > 1"
class="c-imagery__hints"
>Alt-drag to pan</div>
<div
ref="focusedImageWrapper"
class="image-wrapper"
:style="{
'width': `${sizedImageWidth}px`,
'height': `${sizedImageHeight}px`
'width': `${sizedImageDimensions.width}px`,
'height': `${sizedImageDimensions.height}px`
}"
@mousedown="handlePanZoomClick"
>
<img
ref="focusedImage"
class="c-imagery__main-image__image js-imageryView-image "
class="c-imagery__main-image__image js-imageryView-image"
:src="imageUrl"
:draggable="!isSelectable"
:style="{
'filter': `brightness(${filters.brightness}%) contrast(${filters.contrast}%)`
}"
:data-openmct-image-timestamp="time"
:data-openmct-object-keystring="keyString"
>
<div
v-if="imageUrl"
ref="focusedImageElement"
class="c-imagery__main-image__background-image"
:draggable="!isSelectable"
:style="{
'filter': `brightness(${filters.brightness}%) contrast(${filters.contrast}%)`,
'background-image':
`${imageUrl ? (
`url(${imageUrl}),
repeating-linear-gradient(
45deg,
transparent,
transparent 4px,
rgba(125,125,125,.2) 4px,
rgba(125,125,125,.2) 8px
)`
) : ''}`,
'transform': `scale(${zoomFactor}) translate(${imageTranslateX}px, ${imageTranslateY}px)`,
'transition': `${!pan && animateZoom ? 'transform 250ms ease-in' : 'initial'}`,
'width': `${sizedImageWidth}px`,
'height': `${sizedImageHeight}px`,
}"
></div>
<Compass
v-if="shouldDisplayCompass"
:compass-rose-sizing-classes="compassRoseSizingClasses"
@@ -153,7 +134,7 @@
v-if="!isFixed"
class="c-button icon-pause pause-play"
:class="{'is-paused': isPaused}"
@click="paused(!isPaused)"
@click="paused(!isPaused, 'button')"
></button>
</div>
</div>
@@ -175,7 +156,7 @@
:key="image.url + image.time"
class="c-imagery__thumb c-thumb"
:class="{ selected: focusedImageIndex === index && isPaused }"
@click="thumbnailClicked(index)"
@click="setFocusedImage(index, thumbnailClick)"
>
<a
href=""
@@ -201,13 +182,12 @@
</template>
<script>
import eventHelpers from '../lib/eventHelpers';
import _ from 'lodash';
import moment from 'moment';
import RelatedTelemetry from './RelatedTelemetry/RelatedTelemetry';
import Compass from './Compass/Compass.vue';
import ImageControls from './ImageControls.vue';
import imageryData from "../../imagery/mixins/imageryData";
const REFRESH_CSS_MS = 500;
@@ -227,12 +207,9 @@ const ARROW_LEFT = 37;
const SCROLL_LATENCY = 250;
const ZOOM_SCALE_DEFAULT = 1;
export default {
components: {
Compass,
ImageControls
Compass
},
mixins: [imageryData],
inject: ['openmct', 'domainObject', 'objectPath', 'currentView'],
@@ -255,6 +232,10 @@ export default {
timeSystem: timeSystem,
keyString: undefined,
autoScroll: true,
filters: {
brightness: 100,
contrast: 100
},
thumbnailClick: THUMBNAIL_CLICKED,
isPaused: false,
refreshCSS: false,
@@ -266,37 +247,19 @@ export default {
focusedImageNaturalAspectRatio: undefined,
imageContainerWidth: undefined,
imageContainerHeight: undefined,
sizedImageWidth: 0,
sizedImageHeight: 0,
lockCompass: true,
resizingWindow: false,
timeContext: undefined,
zoomFactor: ZOOM_SCALE_DEFAULT,
filters: {
brightness: 100,
contrast: 100
},
cursorStates: {
isPannable: false,
showCursorZoomIn: false,
showCursorZoomOut: false,
modifierKeyPressed: false
},
imageTranslateX: 0,
imageTranslateY: 0,
pan: undefined,
animateZoom: true,
imagePanned: false
timeContext: undefined
};
},
computed: {
compassRoseSizingClasses() {
let compassRoseSizingClasses = '';
if (this.sizedImageWidth < 300) {
if (this.sizedImageDimensions.width < 300) {
compassRoseSizingClasses = '--rose-small --rose-min';
} else if (this.sizedImageWidth < 500) {
} else if (this.sizedImageDimensions.width < 500) {
compassRoseSizingClasses = '--rose-small';
} else if (this.sizedImageWidth > 1000) {
} else if (this.sizedImageDimensions.width > 1000) {
compassRoseSizingClasses = '--rose-max';
}
@@ -365,18 +328,10 @@ export default {
return result;
},
shouldDisplayCompass() {
const imageHeightAndWidth = this.sizedImageHeight !== 0
&& this.sizedImageWidth !== 0;
const display = this.focusedImage !== undefined
return this.focusedImage !== undefined
&& this.focusedImageNaturalAspectRatio !== undefined
&& this.imageContainerWidth !== undefined
&& this.imageContainerHeight !== undefined
&& imageHeightAndWidth
&& this.zoomFactor === 1
&& this.imagePanned !== true;
return display;
&& this.imageContainerHeight !== undefined;
},
isSpacecraftPositionFresh() {
let isFresh = undefined;
@@ -438,6 +393,20 @@ export default {
return isFresh;
},
sizedImageDimensions() {
let sizedImageDimensions = {};
if ((this.imageContainerWidth / this.imageContainerHeight) > this.focusedImageNaturalAspectRatio) {
// container is wider than image
sizedImageDimensions.width = this.imageContainerHeight * this.focusedImageNaturalAspectRatio;
sizedImageDimensions.height = this.imageContainerHeight;
} else {
// container is taller than image
sizedImageDimensions.width = this.imageContainerWidth;
sizedImageDimensions.height = this.imageContainerWidth / this.focusedImageNaturalAspectRatio;
}
return sizedImageDimensions;
},
isFixed() {
let clock;
if (this.timeContext) {
@@ -447,16 +416,6 @@ export default {
}
return clock === undefined;
},
isSelectable() {
return true;
},
sizedImageDimensions() {
return {
width: this.sizedImageWidth,
height: this.sizedImageHeight
};
}
},
watch: {
@@ -465,35 +424,16 @@ export default {
const newSize = newHistory.length;
let imageIndex;
if (this.focusedImageTimestamp !== undefined) {
const foundImageIndex = newHistory.findIndex(img => img.time === this.focusedImageTimestamp);
imageIndex = foundImageIndex > -1
? foundImageIndex
: newSize - 1;
const foundImageIndex = this.imageHistory.findIndex(image => {
return image.time === this.focusedImageTimestamp;
});
imageIndex = foundImageIndex > -1 ? foundImageIndex : newSize - 1;
} else {
imageIndex = newSize > 0
? newSize - 1
: undefined;
imageIndex = newSize > 0 ? newSize - 1 : undefined;
}
this.nextImageIndex = imageIndex;
if (this.previousFocusedImage && newHistory.length) {
const matchIndex = this.matchIndexOfPreviousImage(
this.previousFocusedImage,
newHistory
);
if (matchIndex > -1) {
this.setFocusedImage(matchIndex);
} else {
this.paused();
}
}
if (!this.isPaused) {
this.setFocusedImage(imageIndex);
this.scrollToRight();
}
this.setFocusedImage(imageIndex, false);
this.scrollToRight();
},
deep: true
},
@@ -505,10 +445,6 @@ export default {
}
},
async mounted() {
eventHelpers.extend(this);
this.focusedImageWrapper = this.$refs.focusedImageWrapper;
this.focusedImageElement = this.$refs.focusedImageElement;
//We only need to use this till the user focuses an image manually
if (this.focusedImageTimestamp !== undefined) {
this.isPaused = true;
@@ -549,7 +485,6 @@ export default {
this.thumbWrapperResizeObserver.observe(this.$refs.thumbsWrapper);
}
this.listenTo(this.focusedImageWrapper, 'wheel', this.wheelZoom, this);
},
beforeDestroy() {
this.stopFollowingTimeContext();
@@ -574,9 +509,6 @@ export default {
}
}
}
this.stopListening(this.focusedImageWrapper, 'wheel', this.wheelZoom, this);
},
methods: {
setTimeContext() {
@@ -593,11 +525,6 @@ export default {
}
},
expand() {
// check for modifier keys so it doesnt interfere with the layout
if (this.cursorStates.modifierKeyPressed) {
return;
}
const actionCollection = this.openmct.actions.getActionsCollection(this.objectPath, this.currentView);
const visibleActions = actionCollection.getVisibleActions();
const viewLargeAction = visibleActions
@@ -703,7 +630,6 @@ export default {
focusElement() {
this.$el.focus();
},
handleScroll() {
const thumbsWrapper = this.$refs.thumbsWrapper;
if (!thumbsWrapper || this.resizingWindow) {
@@ -714,15 +640,20 @@ export default {
const disableScroll = scrollWidth > Math.ceil(scrollLeft + clientWidth);
this.autoScroll = !disableScroll;
},
paused(state) {
this.isPaused = Boolean(state);
paused(state, type) {
this.isPaused = state;
if (!state) {
this.previousFocusedImage = null;
this.setFocusedImage(this.nextImageIndex);
this.autoScroll = true;
this.scrollToRight();
if (type === 'button') {
this.setFocusedImage(this.imageHistory.length - 1);
}
if (this.nextImageIndex) {
this.setFocusedImage(this.nextImageIndex);
delete this.nextImageIndex;
}
this.autoScroll = true;
this.scrollToRight();
},
scrollToFocused() {
const thumbsWrapper = this.$refs.thumbsWrapper;
@@ -761,24 +692,51 @@ export default {
&& x.time === previous.time
));
},
thumbnailClicked(index) {
this.setFocusedImage(index);
this.paused(true);
this.setPreviousFocusedImage(index);
},
setPreviousFocusedImage(index) {
this.focusedImageTimestamp = undefined;
this.previousFocusedImage = this.imageHistory[index]
? JSON.parse(JSON.stringify(this.imageHistory[index]))
: undefined;
},
setFocusedImage(index) {
setFocusedImage(index, thumbnailClick = false) {
let focusedIndex = index;
if (!(Number.isInteger(index) && index > -1)) {
return;
}
this.focusedImageIndex = index;
if (thumbnailClick) {
//We use the props till the user changes what they want to see
this.focusedImageTimestamp = undefined;
//set the previousFocusedImage when a user chooses an image
this.previousFocusedImage = this.imageHistory[focusedIndex] ? JSON.parse(JSON.stringify(this.imageHistory[focusedIndex])) : undefined;
}
if (this.previousFocusedImage) {
// determine if the previous image exists in the new bounds of imageHistory
if (!thumbnailClick) {
const matchIndex = this.matchIndexOfPreviousImage(
this.previousFocusedImage,
this.imageHistory
);
focusedIndex = matchIndex > -1 ? matchIndex : this.imageHistory.length - 1;
}
if (!(this.isPaused || thumbnailClick)
|| focusedIndex === this.imageHistory.length - 1) {
delete this.previousFocusedImage;
}
}
this.focusedImageIndex = focusedIndex;
//TODO: do we even need this anymore?
if (this.isPaused && !thumbnailClick && this.focusedImageTimestamp === undefined) {
this.nextImageIndex = focusedIndex;
//this could happen if bounds changes
if (this.focusedImageIndex > this.imageHistory.length - 1) {
this.focusedImageIndex = focusedIndex;
}
return;
}
if (thumbnailClick && !this.isPaused) {
this.paused(true);
}
},
trackDuration() {
if (this.canTrackDuration) {
@@ -816,7 +774,7 @@ export default {
let index = this.focusedImageIndex;
this.thumbnailClicked(++index);
this.setFocusedImage(++index, THUMBNAIL_CLICKED);
if (index === this.imageHistory.length - 1) {
this.paused(false);
}
@@ -829,50 +787,14 @@ export default {
let index = this.focusedImageIndex;
if (index === this.imageHistory.length - 1) {
this.thumbnailClicked(this.imageHistory.length - 2);
this.setFocusedImage(this.imageHistory.length - 2, THUMBNAIL_CLICKED);
} else {
this.thumbnailClicked(--index);
this.setFocusedImage(--index, THUMBNAIL_CLICKED);
}
},
resetImage() {
this.imagePanned = false;
this.zoomFactor = ZOOM_SCALE_DEFAULT;
this.imageTranslateX = 0;
this.imageTranslateY = 0;
},
handlePanZoomUpdate({ newScaleFactor, screenClientX, screenClientY }) {
if (!this.isPaused) {
this.paused(true);
}
if (!(screenClientX || screenClientY)) {
return this.updatePanZoom(newScaleFactor, 0, 0);
}
// handle mouse events
const imageRect = this.focusedImageWrapper.getBoundingClientRect();
const imageContainerX = screenClientX - imageRect.left;
const imageContainerY = screenClientY - imageRect.top;
const offsetFromCenterX = (imageRect.width / 2) - imageContainerX;
const offsetFromCenterY = (imageRect.height / 2) - imageContainerY;
this.updatePanZoom(newScaleFactor, offsetFromCenterX, offsetFromCenterY);
},
updatePanZoom(newScaleFactor, offsetFromCenterX, offsetFromCenterY) {
const currentScale = this.zoomFactor;
const previousTranslateX = this.imageTranslateX;
const previousTranslateY = this.imageTranslateY;
const offsetXInOriginalScale = offsetFromCenterX / currentScale;
const offsetYInOriginalScale = offsetFromCenterY / currentScale;
const translateX = offsetXInOriginalScale + previousTranslateX;
const translateY = offsetYInOriginalScale + previousTranslateY;
this.imageTranslateX = translateX;
this.imageTranslateY = translateY;
this.zoomFactor = newScaleFactor;
},
handlePanZoomClick(e) {
this.$refs.imageControls.handlePanZoomClick(e);
startDrag(e) {
e.preventDefault();
e.stopPropagation();
},
arrowDownHandler(event) {
let key = event.keyCode;
@@ -935,7 +857,7 @@ export default {
// TODO - should probably cache this
img.addEventListener('load', () => {
this.setSizedImageDimensions();
this.focusedImageNaturalAspectRatio = img.naturalWidth / img.naturalHeight;
}, { once: true });
},
resizeImageContainer() {
@@ -950,21 +872,6 @@ export default {
if (this.$refs.imageBG.clientHeight !== this.imageContainerHeight) {
this.imageContainerHeight = this.$refs.imageBG.clientHeight;
}
this.setSizedImageDimensions();
},
setSizedImageDimensions() {
this.focusedImageNaturalAspectRatio = this.$refs.focusedImage.naturalWidth / this.$refs.focusedImage.naturalHeight;
if ((this.imageContainerWidth / this.imageContainerHeight) > this.focusedImageNaturalAspectRatio) {
// container is wider than image
this.sizedImageWidth = this.imageContainerHeight * this.focusedImageNaturalAspectRatio;
this.sizedImageHeight = this.imageContainerHeight;
} else {
// container is taller than image
this.sizedImageWidth = this.imageContainerWidth;
this.sizedImageHeight = this.imageContainerWidth / this.focusedImageNaturalAspectRatio;
}
},
handleThumbWindowResizeStart() {
if (!this.autoScroll) {
@@ -983,73 +890,6 @@ export default {
this.$nextTick(() => {
this.resizingWindow = false;
});
},
// debounced method
clearWheelZoom() {
this.$refs.imageControls.clearWheelZoom();
},
wheelZoom(e) {
e.preventDefault();
if (!this.isPaused) {
this.paused(true);
}
this.$refs.imageControls.wheelZoom(e);
},
startPan(e) {
e.preventDefault();
if (!this.pan && this.zoomFactor > 1) {
this.animateZoom = false;
this.imagePanned = true;
this.pan = {
x: e.clientX,
y: e.clientY
};
this.listenTo(window, 'mouseup', this.onMouseUp, this);
this.listenTo(window, 'mousemove', this.trackMousePosition, this);
}
return false;
},
trackMousePosition(e) {
if (!e.altKey) {
return this.onMouseUp(e);
}
this.updatePan(e);
e.preventDefault();
},
updatePan(e) {
if (!this.pan) {
return;
}
const dX = e.clientX - this.pan.x;
const dY = e.clientY - this.pan.y;
this.pan = {
x: e.clientX,
y: e.clientY
};
this.updatePanZoom(this.zoomFactor, dX, dY);
},
endPan() {
this.pan = undefined;
this.animateZoom = true;
},
onMouseUp(event) {
this.stopListening(window, 'mouseup', this.onMouseUp, this);
this.stopListening(window, 'mousemove', this.trackMousePosition, this);
if (this.pan) {
return this.endPan(event);
}
},
setFilters(filtersObj) {
this.filters = filtersObj;
},
setCursorStates(states) {
this.cursorStates = states;
}
}
};

View File

@@ -18,11 +18,6 @@
flex: 1 1 auto;
}
.image-wrapper {
overflow: visible clip;
background-image: repeating-linear-gradient(45deg, transparent, transparent 4px, rgba(125, 125, 125, 0.2) 4px, rgba(125, 125, 125, 0.2) 8px);
}
&__main-image {
&__bg {
background-color: $colorPlotBg;
@@ -32,46 +27,18 @@
justify-content: center;
flex: 1 1 auto;
height: 0;
overflow: hidden;
&.unnsynced{
@include sUnsynced();
}
&.cursor-zoom-in {
cursor: zoom-in;
}
&.cursor-zoom-out {
cursor: zoom-out;
}
&.pannable {
@include cursorGrab();
}
}
}
&__background-image {
background-position: center;
background-repeat: no-repeat;
background-size: contain;
}
&__image {
height: 100%;
width: 100%;
visibility: hidden;
display: contents;
}
}
&__hints {
$m: $interiorMargin;
background: rgba(black, 0.2);
border-radius: $smallCr;
padding: 2px $interiorMargin;
position: absolute;
right: $m;
top: $m;
opacity: 0.9;
z-index: 2;
}
&__control-bar,
&__time {
display: flex;
@@ -210,8 +177,8 @@
z-index: 2;
background: $colorLocalControlOvrBg;
border-radius: $basicCr;
max-width: 250px;
min-width: 170px;
max-width: 200px;
min-width: 70px;
width: 35%;
align-items: center;
padding: $interiorMargin $interiorMarginLg;
@@ -235,7 +202,6 @@
&__lc {
&__reset-btn {
// Span that holds bracket graphics and button
$bc: $scrollbarTrackColorBg;
&:before,
@@ -256,50 +222,18 @@
border-bottom: 1px solid $bc;
margin-top: 2px;
}
.c-icon-link {
color: $colorBtnFg;
}
}
}
}
.c-image-controls {
// Brightness/contrast
&__controls {
display: flex;
align-items: stretch;
flex-direction: column;
> * + * {
margin-top: $interiorMargin;
}
[class*='c-button'] { flex: 0 0 auto; }
}
&__control,
&__input {
// Sliders and reset element
display: flex;
align-items: center;
width: 100%;
&:before {
color: rgba($colorMenuFg, 0.5);
margin-right: $interiorMarginSm;
}
}
&__input {
// A wrapper is needed to add the type icon to left of each control
input[type='range'] {
//width: 100%; // Do we need this?
}
}
&__zoom {
> * + * { margin-left: $interiorMargin; }
margin-right: $interiorMargin; // Need some extra space due to proximity to close button
}
&__sliders {
@@ -312,6 +246,21 @@
}
}
&__slider-wrapper {
// A wrapper is needed to add the type icon to left of each range input
display: flex;
align-items: center;
&:before {
color: rgba($colorMenuFg, 0.5);
margin-right: $interiorMarginSm;
}
input[type='range'] {
width: 100px;
}
}
&__btn-reset {
flex: 0 0 auto;
}

View File

@@ -1,98 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, 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.
*****************************************************************************/
define([], function () {
const helperFunctions = {
listenTo: function (object, event, callback, context) {
if (!this._listeningTo) {
this._listeningTo = [];
}
const listener = {
object: object,
event: event,
callback: callback,
context: context,
_cb: context ? callback.bind(context) : callback
};
if (object.$watch && event.indexOf('change:') === 0) {
const scopePath = event.replace('change:', '');
listener.unlisten = object.$watch(scopePath, listener._cb, true);
} else if (object.$on) {
listener.unlisten = object.$on(event, listener._cb);
} else if (object.addEventListener) {
object.addEventListener(event, listener._cb);
} else {
object.on(event, listener._cb);
}
this._listeningTo.push(listener);
},
stopListening: function (object, event, callback, context) {
if (!this._listeningTo) {
this._listeningTo = [];
}
this._listeningTo.filter(function (listener) {
if (object && object !== listener.object) {
return false;
}
if (event && event !== listener.event) {
return false;
}
if (callback && callback !== listener.callback) {
return false;
}
if (context && context !== listener.context) {
return false;
}
return true;
})
.map(function (listener) {
if (listener.unlisten) {
listener.unlisten();
} else if (listener.object.removeEventListener) {
listener.object.removeEventListener(listener.event, listener._cb);
} else {
listener.object.off(listener.event, listener._cb);
}
return listener;
})
.forEach(function (listener) {
this._listeningTo.splice(this._listeningTo.indexOf(listener), 1);
}, this);
},
extend: function (object) {
object.listenTo = helperFunctions.listenTo;
object.stopListening = helperFunctions.stopListening;
}
};
return helperFunctions;
});

View File

@@ -483,42 +483,6 @@ describe("The Imagery View Layouts", () => {
});
});
});
xit('should change the image zoom factor when using the zoom buttons', async (done) => {
await Vue.nextTick();
let imageSizeBefore;
let imageSizeAfter;
// test clicking the zoom in button
imageSizeBefore = parent.querySelector('.c-imagery_main-image_background-image').getBoundingClientRect();
parent.querySelector('.t-btn-zoom-in').click();
await Vue.nextTick();
imageSizeAfter = parent.querySelector('.c-imagery_main-image_background-image').getBoundingClientRect();
expect(imageSizeAfter.height).toBeGreaterThan(imageSizeBefore.height);
expect(imageSizeAfter.width).toBeGreaterThan(imageSizeBefore.width);
// test clicking the zoom out button
imageSizeBefore = parent.querySelector('.c-imagery_main-image_background-image').getBoundingClientRect();
parent.querySelector('.t-btn-zoom-out').click();
await Vue.nextTick();
imageSizeAfter = parent.querySelector('.c-imagery_main-image_background-image').getBoundingClientRect();
expect(imageSizeAfter.height).toBeLessThan(imageSizeBefore.height);
expect(imageSizeAfter.width).toBeLessThan(imageSizeBefore.width);
done();
});
xit('should reset the zoom factor on the image when clicking the zoom button', async (done) => {
await Vue.nextTick();
// test clicking the zoom reset button
// zoom in to scale up the image dimensions
parent.querySelector('.t-btn-zoom-in').click();
await Vue.nextTick();
let imageSizeBefore = parent.querySelector('.c-imagery_main-image_background-image').getBoundingClientRect();
await Vue.nextTick();
parent.querySelector('.t-btn-zoom-reset').click();
let imageSizeAfter = parent.querySelector('.c-imagery_main-image_background-image').getBoundingClientRect();
expect(imageSizeAfter.height).toBeLessThan(imageSizeBefore.height);
expect(imageSizeAfter.width).toBeLessThan(imageSizeBefore.width);
done();
});
it('clear data action is installed', () => {
expect(clearDataAction).toBeDefined();
});

View File

@@ -90,17 +90,6 @@ export default class LinkAction {
validate(currentParent) {
return (data) => {
// default current parent to ROOT, if it's undefined, then it's a root level item
if (currentParent === undefined) {
currentParent = {
identifier: {
key: 'ROOT',
namespace: ''
}
};
}
const parentCandidate = data.value[0];
const currentParentKeystring = this.openmct.objects.makeKeyString(currentParent.identifier);
const parentCandidateKeystring = this.openmct.objects.makeKeyString(parentCandidate.identifier);

View File

@@ -76,9 +76,6 @@
<div
ref="chartContainer"
class="gl-plot-chart-wrapper"
:class="[
{ 'alt-pressed': altPressed },
]"
>
<mct-chart
:rectangles="rectangles"
@@ -233,7 +230,6 @@ export default {
},
data() {
return {
altPressed: false,
highlights: [],
lockHighlightPoint: false,
tickWidth: 0,
@@ -272,8 +268,6 @@ export default {
}
},
mounted() {
document.addEventListener('keydown', this.handleKeyDown);
document.addEventListener('keyup', this.handleKeyUp);
eventHelpers.extend(this);
this.updateRealTime = this.updateRealTime.bind(this);
this.updateDisplayBounds = this.updateDisplayBounds.bind(this);
@@ -305,21 +299,9 @@ export default {
},
beforeDestroy() {
document.removeEventListener('keydown', this.handleKeyDown);
document.removeEventListener('keyup', this.handleKeyUp);
this.destroy();
},
methods: {
handleKeyDown(event) {
if (event.key === 'Alt') {
this.altPressed = true;
}
},
handleKeyUp(event) {
if (event.key === 'Alt') {
this.altPressed = false;
}
},
setTimeContext() {
this.stopFollowingTimeContext();
@@ -661,7 +643,7 @@ export default {
this.positionOverElement = {
x: event.clientX - this.chartElementBounds.left,
y: this.chartElementBounds.height
- (event.clientY - this.chartElementBounds.top)
- (event.clientY - this.chartElementBounds.top)
};
this.positionOverPlot = {

View File

@@ -112,10 +112,6 @@ export default {
mounted() {
eventHelpers.extend(this);
if (!this.axisType) {
throw new Error("axis-type prop expected");
}
this.axis = this.getAxisFromConfig();
this.tickCount = 4;
@@ -130,16 +126,15 @@ export default {
},
methods: {
getAxisFromConfig() {
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
/** @type {import('./configuration/PlotConfigurationModel').default} */
let config = configStore.get(configId);
if (!config) {
throw new Error('config is missing');
if (!this.axisType) {
return;
}
return config[this.axisType];
const configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
let config = configStore.get(configId);
if (config) {
return config[this.axisType];
}
},
/**
* Determine whether ticks should be regenerated for a given range.
@@ -215,8 +210,8 @@ export default {
if (this.shouldRegenerateTicks(range, forceRegeneration)) {
let newTicks = this.getTicks();
this.tickRange = {
min: Math.min(...newTicks),
max: Math.max(...newTicks),
min: Math.min.apply(Math, newTicks),
max: Math.max.apply(Math, newTicks),
step: newTicks[1] - newTicks[0]
};

View File

@@ -23,9 +23,6 @@
import eventHelpers from '../lib/eventHelpers';
export default class MCTChartAlarmLineSet {
/**
* @param {Bounds} bounds
*/
constructor(series, chart, offset, bounds) {
this.series = series;
this.chart = chart;
@@ -43,9 +40,6 @@ export default class MCTChartAlarmLineSet {
}
}
/**
* @param {Bounds} bounds
*/
updateBounds(bounds) {
this.bounds = bounds;
this.getLimitPoints(this.series);
@@ -112,7 +106,3 @@ export default class MCTChartAlarmLineSet {
}
}
/**
@typedef {import('@/api/time/TimeContext').Bounds} Bounds
*/

View File

@@ -23,7 +23,7 @@
import MCTChartSeriesElement from './MCTChartSeriesElement';
export default class MCTChartLineLinear extends MCTChartSeriesElement {
addPoint(point, start) {
addPoint(point, start, count) {
this.buffer[start] = point.x;
this.buffer[start + 1] = point.y;
}

View File

@@ -45,7 +45,7 @@ export default class MCTChartLineStepAfter extends MCTChartSeriesElement {
return 2 + ((index - 1) * 4);
}
addPoint(point, start) {
addPoint(point, start, count) {
if (start === 0 && this.count === 0) {
// First point is easy.
this.buffer[start] = point.x;

View File

@@ -24,7 +24,7 @@ import MCTChartSeriesElement from './MCTChartSeriesElement';
// TODO: Is this needed? This is identical to MCTChartLineLinear. Why is it a different class?
export default class MCTChartPointSet extends MCTChartSeriesElement {
addPoint(point, start) {
addPoint(point, start, count) {
this.buffer[start] = point.x;
this.buffer[start + 1] = point.y;
}

View File

@@ -22,7 +22,6 @@
import eventHelpers from '../lib/eventHelpers';
/** @abstract */
export default class MCTChartSeriesElement {
constructor(series, chart, offset) {
this.series = series;
@@ -73,11 +72,9 @@ export default class MCTChartSeriesElement {
}
}
/** @abstract */
removePoint(index) {}
/** @abstract */
addPoint(point, index) {}
removePoint(point, index, count) {
// by default, do nothing.
}
remove(point, index, series) {
const vertexCount = this.vertexCountForPointAtIndex(index);
@@ -158,18 +155,3 @@ export default class MCTChartSeriesElement {
}
}
/** @typedef {any} TODO */
/** @typedef {import('../configuration/PlotSeries').default} PlotSeries */
/**
@typedef {{
x: (x: number) => number
y: (y: number) => number
xVal: (point: Point, pSeries: PlotSeries) => number
yVal: (point: Point, pSeries: PlotSeries) => number
xKey: TODO
yKey: TODO
}} Offset
*/

View File

@@ -21,17 +21,11 @@
*****************************************************************************/
import Model from './Model';
/**
* @template {object} T
* @template {object} O
* @extends {Model<T, O>}
*/
export default class Collection extends Model {
/** @type {Constructor} */
modelClass = Model;
initialize(options) {
super.initialize(options);
this.modelClass = Model;
if (options.models) {
this.models = options.models.map(this.modelFn, this);
} else {
@@ -113,7 +107,3 @@ export default class Collection extends Model {
this.stopListening();
}
}
/** @typedef {any} TODO */
/** @typedef {new (...args: any[]) => object} Constructor */

View File

@@ -19,47 +19,32 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
class ConfigStore {
/** @type {Record<string, Destroyable>} */
store = {};
/**
@param {string} id
*/
deleteStore(id) {
const obj = this.store[id];
if (obj) {
if (obj.destroy) {
obj.destroy();
}
delete this.store[id];
}
}
deleteAll() {
Object.keys(this.store).forEach(id => this.deleteStore(id));
}
/**
@param {string} id
@param {any} config
*/
add(id, config) {
this.store[id] = config;
}
/**
@param {string} id
*/
get(id) {
return this.store[id];
}
function ConfigStore() {
this.store = {};
}
ConfigStore.prototype.deleteStore = function (id) {
if (this.store[id]) {
if (this.store[id].destroy) {
this.store[id].destroy();
}
delete this.store[id];
}
};
ConfigStore.prototype.deleteAll = function () {
Object.keys(this.store).forEach(id => this.deleteStore(id));
};
ConfigStore.prototype.add = function (id, config) {
this.store[id] = config;
};
ConfigStore.prototype.get = function (id) {
return this.store[id];
};
const STORE = new ConfigStore();
export default STORE;
/** @typedef {{destroy?(): void}} Destroyable */

View File

@@ -42,10 +42,7 @@ export default class LegendModel extends Model {
}
}
/**
* @override
*/
defaultModel(options) {
defaults(options) {
return {
position: 'top',
expandByDefault: false,

View File

@@ -20,18 +20,11 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import EventEmitter from 'eventemitter3';
import EventEmitter from 'EventEmitter';
import eventHelpers from "../lib/eventHelpers";
import _ from 'lodash';
/**
* @template {object} T
* @template {object} O
*/
export default class Model extends EventEmitter {
/**
* @param {ModelOptions<T, O>} options
*/
constructor(options) {
super();
@@ -42,14 +35,10 @@ export default class Model extends EventEmitter {
options = {};
}
// FIXME: this.id is defined as a method further below, but here it is
// assigned a possibly-undefined value. Is this code unused?
this.id = options.id;
/** @type {ModelType<T>} */
this.model = options.model;
this.collection = options.collection;
const defaults = this.defaultModel(options);
const defaults = this.defaults(options);
if (!this.model) {
this.model = options.model = defaults;
} else {
@@ -57,23 +46,14 @@ export default class Model extends EventEmitter {
}
this.initialize(options);
/** @type {keyof ModelType<T> } */
this.idAttr = 'id';
}
/**
* @param {ModelOptions<T, O>} options
* @returns {ModelType<T>}
*/
defaultModel(options) {
defaults(options) {
return {};
}
/**
* @param {ModelOptions<T, O>} options
*/
initialize(options) {
initialize(model) {
}
@@ -89,29 +69,14 @@ export default class Model extends EventEmitter {
return this.get(this.idAttr);
}
/**
* @template {keyof ModelType<T>} K
* @param {K} attribute
* @returns {ModelType<T>[K]}
*/
get(attribute) {
return this.model[attribute];
}
/**
* @template {keyof ModelType<T>} K
* @param {K} attribute
* @returns boolean
*/
has(attribute) {
return _.has(this.model, attribute);
}
/**
* @template {keyof ModelType<T>} K
* @param {K} attribute
* @param {ModelType<T>[K]} value
*/
set(attribute, value) {
const oldValue = this.model[attribute];
this.model[attribute] = value;
@@ -119,10 +84,6 @@ export default class Model extends EventEmitter {
this.emit('change:' + attribute, value, oldValue, this);
}
/**
* @template {keyof ModelType<T>} K
* @param {K} attribute
*/
unset(attribute) {
const oldValue = this.model[attribute];
delete this.model[attribute];
@@ -130,26 +91,3 @@ export default class Model extends EventEmitter {
this.emit('change:' + attribute, undefined, oldValue, this);
}
}
/** @typedef {any} TODO */
/** @typedef {TODO} OpenMCT */
/**
@template {object} T
@typedef {{
id?: string
} & T} ModelType
*/
/**
@template {object} T
@template {object} O
@typedef {{
model?: ModelType<T>
models?: T[]
openmct: OpenMCT
id?: string
[k: string]: unknown
} & O} ModelOptions
*/

View File

@@ -26,30 +26,20 @@ import SeriesCollection from "./SeriesCollection";
import XAxisModel from "./XAxisModel";
import YAxisModel from "./YAxisModel";
import LegendModel from "./LegendModel";
/**
* PlotConfiguration model stores the configuration of a plot and some
* limited state. The indiidual parts of the plot configuration model
* handle setting defaults and updating in response to various changes.
*
* @extends {Model<PlotConfigModelType, PlotConfigModelOptions>}
*/
export default class PlotConfigurationModel extends Model {
/**
* Initializes all sub models and then passes references to submodels
* to those that need it.
*
* @override
* @param {import('./Model').ModelOptions<PlotConfigModelType, PlotConfigModelOptions>} options
*/
initialize(options) {
this.openmct = options.openmct;
// This is a type assertion for TypeScript, this error is never thrown in practice.
if (!options.model) {
throw new Error('Not a collection model.');
}
this.xAxis = new XAxisModel({
model: options.model.xAxis,
plot: this,
@@ -86,8 +76,6 @@ export default class PlotConfigurationModel extends Model {
}
/**
* Retrieve the persisted series config for a given identifier.
* @param {import('./PlotSeries').Identifier} identifier
* @returns {import('./PlotSeries').PlotSeriesModelType=}
*/
getPersistedSeriesConfig(identifier) {
const domainObject = this.get('domainObject');
@@ -135,48 +123,15 @@ export default class PlotConfigurationModel extends Model {
/**
* Return defaults, which are extracted from the passed in domain
* object.
* @override
* @param {import('./Model').ModelOptions<PlotConfigModelType, PlotConfigModelOptions>} options
*/
defaultModel(options) {
defaults(options) {
return {
series: [],
domainObject: options.domainObject,
xAxis: {},
yAxis: _.cloneDeep(options.domainObject.configuration?.yAxis ?? {}),
legend: _.cloneDeep(options.domainObject.configuration?.legend ?? {})
xAxis: {
},
yAxis: _.cloneDeep(_.get(options.domainObject, 'configuration.yAxis', {})),
legend: _.cloneDeep(_.get(options.domainObject, 'configuration.legend', {}))
};
}
}
/** @typedef {any} TODO */
/** @typedef {import('./PlotSeries').default} PlotSeries */
/**
@typedef {{
configuration: {
series: import('./PlotSeries').PlotSeriesModelType[]
}
}} SomeDomainObject_NeedsName
*/
/**
@typedef {{
xAxis: import('./XAxisModel').XAxisModelType
yAxis: import('./YAxisModel').YAxisModelType
legend: TODO
series: PlotSeries[]
domainObject: SomeDomainObject_NeedsName
}} PlotConfigModelType
*/
/** @typedef {TODO} SomeOtherDomainObject */
/**
TODO: Is SomeOtherDomainObject the same domain object as with SomeDomainObject_NeedsName?
@typedef {{
plot: import('./PlotConfigurationModel').default
domainObject: SomeOtherDomainObject
}} PlotConfigModelOptions
*/

View File

@@ -59,15 +59,9 @@ import configStore from "../configuration/ConfigStore";
* `metadata`: the Open MCT Telemetry Metadata Manager for the associated
* telemetry point.
* `formats`: the Open MCT format map for this telemetry point.
*
* @extends {Model<PlotSeriesModelType, PlotSeriesModelOptions>}
*/
export default class PlotSeries extends Model {
/**
@param {import('./Model').ModelOptions<PlotSeriesModelType, PlotSeriesModelOptions>} options
*/
constructor(options) {
super(options);
this.listenTo(this, 'change:xKey', this.onXKeyChange, this);
@@ -82,10 +76,8 @@ export default class PlotSeries extends Model {
/**
* Set defaults for telemetry series.
* @param {import('./Model').ModelOptions<PlotSeriesModelType, PlotSeriesModelOptions>} options
* @override
*/
defaultModel(options) {
defaults(options) {
this.metadata = options
.openmct
.telemetry
@@ -117,21 +109,13 @@ export default class PlotSeries extends Model {
/**
* Remove real-time subscription when destroyed.
* @override
*/
destroy() {
super.destroy();
onDestroy(model) {
if (this.unsubscribe) {
this.unsubscribe();
}
}
/**
* Set defaults for telemetry series.
* @override
* @param {import('./Model').ModelOptions<PlotSeriesModelType, PlotSeriesModelOptions>} options
*/
initialize(options) {
this.openmct = options.openmct;
this.domainObject = options.domainObject;
@@ -152,11 +136,9 @@ export default class PlotSeries extends Model {
});
this.openmct.time.on('bounds', this.updateLimits);
this.on('destroy', this.onDestroy, this);
}
/**
* @param {Bounds} bounds
*/
updateLimits(bounds) {
this.emit('limitBounds', bounds);
}
@@ -206,7 +188,7 @@ export default class PlotSeries extends Model {
return this.openmct
.telemetry
.request(this.domainObject, options)
.then((points) => {
.then(function (points) {
const data = this.getSeriesData();
const newPoints = _(data)
.concat(points)
@@ -214,7 +196,7 @@ export default class PlotSeries extends Model {
.uniq(true, point => [this.getXVal(point), this.getYVal(point)].join())
.value();
this.reset(newPoints);
})
}.bind(this))
.catch((error) => {
console.warn('Error fetching data', error);
});
@@ -519,55 +501,8 @@ export default class PlotSeries extends Model {
/**
* Update the series data with the given value.
* @returns {Array<{
cos: number
sin: number
mctLimitState: {
cssClass: string
high: number
low: {sin: number, cos: number}
name: string
}
utc: number
wavelength: number
yesterday: number
}>}
*/
getSeriesData() {
return configStore.get(this.dataStoreId) || [];
}
}
/** @typedef {any} TODO */
/** @typedef {{key: string, namespace: string}} Identifier */
/**
@typedef {{
identifier: Identifier
name: string
unit: string
xKey: string
yKey: string
markers: boolean
markerShape: keyof typeof MARKER_SHAPES
markerSize: number
alarmMarkers: boolean
limitLines: boolean
interpolate: boolean
stats: TODO
}} PlotSeriesModelType
*/
/**
@typedef {{
model: PlotSeriesModelType
collection: import('./SeriesCollection').default
persistedConfig: PlotSeriesModelType
filters: TODO
}} PlotSeriesModelOptions
*/
/**
@typedef {import('@/api/time/TimeContext').Bounds} Bounds
*/

View File

@@ -26,14 +26,8 @@ import Collection from "./Collection";
import Color from "@/ui/color/Color";
import ColorPalette from "@/ui/color/ColorPalette";
/**
* @extends {Collection<SeriesCollectionModelType, SeriesCollectionOptions>}
*/
export default class SeriesCollection extends Collection {
/**
@override
@param {import('./Model').ModelOptions<SeriesCollectionModelType, SeriesCollectionOptions>} options
*/
initialize(options) {
super.initialize(options);
this.modelClass = PlotSeries;
@@ -89,15 +83,11 @@ export default class SeriesCollection extends Collection {
// Clone to prevent accidental mutation by ref.
seriesConfig = JSON.parse(JSON.stringify(seriesConfig));
if (!seriesConfig) {
throw "not possible";
}
this.add(new PlotSeries({
model: seriesConfig,
domainObject: domainObject,
openmct: this.openmct,
collection: this,
openmct: this.openmct,
persistedConfig: this.plot
.getPersistedSeriesConfig(domainObject.identifier),
filters: filters
@@ -173,13 +163,3 @@ export default class SeriesCollection extends Collection {
})[0];
}
}
/**
@typedef {PlotSeries} SeriesCollectionModelType
*/
/**
@typedef {{
plot: import('./PlotConfigurationModel').default
}} SeriesCollectionOptions
*/

View File

@@ -20,38 +20,22 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import Model from "./Model";
/**
* @extends {Model<XAxisModelType, XAxisModelOptions>}
*/
export default class XAxisModel extends Model {
// Despite providing template types to the Model class, we still need to
// re-define the type of the following initialize() method's options arg. Tracking
// issue for this: https://github.com/microsoft/TypeScript/issues/32082
// When they fix it, we can remove the `@param` we have here.
/**
* @override
* @param {import('./Model').ModelOptions<XAxisModelType, XAxisModelOptions>} options
* TODO: doc strings.
*/
export default class XAxisModel extends Model {
initialize(options) {
this.plot = options.plot;
// This is a type assertion for TypeScript, this error is not thrown in practice.
if (!options.model) {
throw new Error('Not a collection model.');
}
this.set('label', options.model.name || '');
this.on('change:range', (newValue) => {
if (!this.get('frozen')) {
this.set('displayRange', newValue);
this.on('change:range', function (newValue, oldValue, model) {
if (!model.get('frozen')) {
model.set('displayRange', newValue);
}
});
this.on('change:frozen', ((frozen) => {
this.on('change:frozen', ((frozen, oldValue, model) => {
if (!frozen) {
this.set('range', this.get('range'));
model.set('range', this.get('range'));
}
}));
@@ -61,10 +45,6 @@ export default class XAxisModel extends Model {
this.listenTo(this, 'change:key', this.changeKey, this);
}
/**
* @param {string} newKey
*/
changeKey(newKey) {
const series = this.plot.series.first();
if (series) {
@@ -88,17 +68,12 @@ export default class XAxisModel extends Model {
plotSeries.reset();
});
}
/**
* @param {import('./Model').ModelOptions<XAxisModelType, XAxisModelOptions>} options
* @override
*/
defaultModel(options) {
defaults(options) {
const bounds = options.openmct.time.bounds();
const timeSystem = options.openmct.time.timeSystem();
const format = options.openmct.telemetry.getFormatter(timeSystem.timeFormat);
/** @type {XAxisModelType} */
const defaultModel = {
return {
name: timeSystem.name,
key: timeSystem.key,
format: format.format.bind(format),
@@ -108,42 +83,5 @@ export default class XAxisModel extends Model {
},
frozen: false
};
return defaultModel;
}
}
/** @typedef {any} TODO */
/** @typedef {TODO} OpenMCT */
/**
@typedef {{
min: number
max: number
}} NumberRange
*/
/**
@typedef {import("./Model").ModelType<{
range: NumberRange
displayRange: NumberRange
frozen: boolean
label: string
format: (n: number) => string
values: Array<TODO>
}>} AxisModelType
*/
/**
@typedef {AxisModelType & {
name: string
key: string
}} XAxisModelType
*/
/**
@typedef {{
plot: import('./PlotConfigurationModel').default
}} XAxisModelOptions
*/

View File

@@ -23,32 +23,27 @@ import _ from 'lodash';
import Model from './Model';
/**
* YAxis model
*
* TODO: docstrings.
*
* has the following Model properties:
*
* `autoscale`: boolean, whether or not to autoscale.
* `autoscalePadding`: float, percent of padding to display in plots.
* `displayRange`: the current display range for the x Axis.
* `format`: the formatter for the axis.
* `frozen`: boolean, if true, displayRange will not be updated automatically.
* Used to temporarily disable automatic updates during user interaction.
* `label`: label to display on axis.
* `stats`: Min and Max Values of data, automatically updated by observing
* plot series.
* `values`: for enumerated types, an array of possible display values.
* `range`: the user-configured range to use for display, when autoscale is
* disabled.
*
* @extends {Model<YAxisModelType, YAxisModelOptions>}
*/
export default class YAxisModel extends Model {
/**
* @override
* @param {import('./Model').ModelOptions<YAxisModelType, YAxisModelOptions>} options
* YAxis model
*
* TODO: docstrings.
*
* has the following Model properties:
*
* `autoscale`: boolean, whether or not to autoscale.
* `autoscalePadding`: float, percent of padding to display in plots.
* `displayRange`: the current display range for the x Axis.
* `format`: the formatter for the axis.
* `frozen`: boolean, if true, displayRange will not be updated automatically.
* Used to temporarily disable automatic updates during user interaction.
* `label`: label to display on axis.
* `stats`: Min and Max Values of data, automatically updated by observing
* plot series.
* `values`: for enumerated types, an array of possible display values.
* `range`: the user-configured range to use for display, when autoscale is
* disabled.
*
*/
export default class YAxisModel extends Model {
initialize(options) {
this.plot = options.plot;
this.listenTo(this, 'change:stats', this.calculateAutoscaleExtents, this);
@@ -58,9 +53,6 @@ export default class YAxisModel extends Model {
this.listenTo(this, 'change:range', this.updateDisplayRange, this);
this.updateDisplayRange(this.get('range'));
}
/**
* @param {import('./SeriesCollection').default} seriesCollection
*/
listenToSeriesCollection(seriesCollection) {
this.seriesCollection = seriesCollection;
this.listenTo(this.seriesCollection, 'add', (series => {
@@ -146,9 +138,6 @@ export default class YAxisModel extends Model {
}
}, this);
}
/**
* @param {import('./PlotSeries').default} series
*/
trackSeries(series) {
this.listenTo(series, 'change:stats', seriesStats => {
if (!seriesStats) {
@@ -174,13 +163,12 @@ export default class YAxisModel extends Model {
}
}
/**
* Update yAxis format, values, and label from known series.
* @param {import('./SeriesCollection').default} seriesCollection
*/
updateFromSeries(seriesCollection) {
* Update yAxis format, values, and label from known series.
*/
updateFromSeries(series) {
const plotModel = this.plot.get('domainObject');
const label = _.get(plotModel, 'configuration.yAxis.label');
const sampleSeries = seriesCollection.first();
const sampleSeries = series.first();
if (!sampleSeries) {
if (!label) {
this.unset('label');
@@ -195,7 +183,7 @@ export default class YAxisModel extends Model {
this.set('format', yFormat.format.bind(yFormat));
this.set('values', yMetadata.values);
if (!label) {
const labelName = seriesCollection.map(function (s) {
const labelName = series.map(function (s) {
return s.metadata ? s.metadata.value(s.get('yKey')).name : '';
}).reduce(function (a, b) {
if (a === undefined) {
@@ -215,7 +203,7 @@ export default class YAxisModel extends Model {
return;
}
const labelUnits = seriesCollection.map(function (s) {
const labelUnits = series.map(function (s) {
return s.metadata ? s.metadata.value(s.get('yKey')).units : '';
}).reduce(function (a, b) {
if (a === undefined) {
@@ -236,13 +224,7 @@ export default class YAxisModel extends Model {
}
}
}
/**
* @override
* @param {import('./Model').ModelOptions<YAxisModelType, YAxisModelOptions>} options
* @returns {YAxisModelType}
*/
defaultModel(options) {
// @ts-ignore incomplete YAxisModelType object used for default value.
defaults(options) {
return {
frozen: false,
autoscale: true,
@@ -250,20 +232,3 @@ export default class YAxisModel extends Model {
};
}
}
/** @typedef {any} TODO */
/**
@typedef {import('./XAxisModel').AxisModelType & {
autoscale: boolean
autoscalePadding: number
stats: import('./XAxisModel').NumberRange
values: Array<TODO>
}} YAxisModelType
*/
/**
@typedef {{
plot: import('./PlotConfigurationModel').default
}} YAxisModelOptions
*/

View File

@@ -110,7 +110,6 @@ DrawWebGL.prototype.onContextLost = function (event) {
this.emit('error');
this.isContextLost = true;
this.destroy();
// TODO re-initialize and re-draw on context restored
};
DrawWebGL.prototype.initContext = function () {

View File

@@ -90,10 +90,3 @@ const helperFunctions = {
};
export default helperFunctions;
/**
@typedef {{
listenTo: (object: any, event: any, callback: any, context: any) => void
stopListening: (object: any, event: any, callback: any, context: any) => void
}} EventHelpers
*/

View File

@@ -92,33 +92,23 @@ export default class RemoveAction {
this.openmct.editor.save();
}
if (!this.isAlias(child, parent)) {
const parentKeyString = this.openmct.objects.makeKeyString(parent.identifier);
const isAlias = parentKeyString !== child.location;
if (!isAlias) {
this.openmct.objects.mutate(child, 'location', null);
}
}
isAlias(child, parent) {
if (parent === undefined) {
// then it's a root item, not an alias
return false;
}
const parentKeyString = this.openmct.objects.makeKeyString(parent.identifier);
const childLocation = child.location;
return childLocation !== parentKeyString;
}
appliesTo(objectPath) {
const parent = objectPath[1];
const parentType = parent && this.openmct.types.get(parent.type);
const child = objectPath[0];
const locked = child.locked ? child.locked : parent && parent.locked;
const isEditing = this.openmct.editor.isEditing();
const isPersistable = this.openmct.objects.isPersistable(child.identifier);
const isAlias = this.isAlias(child, parent);
let parent = objectPath[1];
let parentType = parent && this.openmct.types.get(parent.type);
let child = objectPath[0];
let locked = child.locked ? child.locked : parent && parent.locked;
let isEditing = this.openmct.editor.isEditing();
let isPersistable = this.openmct.objects.isPersistable(child.identifier);
if (locked || (!isPersistable && !isAlias)) {
if (locked || !isPersistable) {
return false;
}

View File

@@ -32,12 +32,10 @@
<div class="c-conductor__time-bounds">
<conductor-inputs-fixed
v-if="isFixed"
:input-bounds="viewBounds"
@updated="saveFixedOffsets"
/>
<conductor-inputs-realtime
v-else
:input-bounds="viewBounds"
@updated="saveClockOffsets"
/>
<ConductorModeIcon class="c-conductor__mode-icon" />

View File

@@ -72,12 +72,6 @@ export default {
default() {
return undefined;
}
},
inputBounds: {
type: Object,
default() {
return undefined;
}
}
},
data() {
@@ -105,12 +99,6 @@ export default {
watch: {
keyString() {
this.setTimeContext();
},
inputBounds: {
handler(newBounds) {
this.handleNewBounds(newBounds);
},
deep: true
}
},
mounted() {

View File

@@ -22,7 +22,7 @@
ref="startOffset"
class="c-button c-conductor__delta-button"
title="Set the time offset after now"
@click.prevent.stop="showTimePopupStart"
@click.prevent="showTimePopupStart"
>
{{ offsets.start }}
</button>
@@ -61,7 +61,7 @@
ref="endOffset"
class="c-button c-conductor__delta-button"
title="Set the time offset preceding now"
@click.prevent.stop="showTimePopupEnd"
@click.prevent="showTimePopupEnd"
>
{{ offsets.end }}
</button>
@@ -87,12 +87,6 @@ export default {
default() {
return undefined;
}
},
inputBounds: {
type: Object,
default() {
return undefined;
}
}
},
data() {
@@ -125,12 +119,6 @@ export default {
watch: {
keyString() {
this.setTimeContext();
},
inputBounds: {
handler(newBounds) {
this.handleNewBounds(newBounds);
},
deep: true
}
},
mounted() {

View File

@@ -160,10 +160,6 @@ mct-plot {
bottom: 0;
left: 0;
}
.alt-pressed {
// When alt is being pressed and user is hovering over the plot, set the cursor
@include cursorGrab();
}
.gl-plot-axis-area.gl-plot-x {
top: auto;

View File

@@ -122,9 +122,6 @@
flex: 1 1 auto;
height: 0; // Chrome 73 overflow bug fix
overflow: auto;
//To accommodate independent time conductor controls
display: flex;
flex-direction: column;
.u-fills-container {
// Expand component types that fill a container

View File

@@ -7,14 +7,7 @@
"allowJs": true,
"checkJs": false,
"strict": true,
"esModuleInterop": true,
"noImplicitOverride": true,
"module": "esnext",
"moduleResolution": "node",
"paths": {
// matches the alias in webpack config, so that types for those imports are visible.
"@/*": ["src/*"]
}
"moduleResolution": "node"
}
}

View File

@@ -18,7 +18,6 @@ const gitBranch = require('child_process')
const config = {
entry: {
openmct: './openmct.js',
generatorWorker: './example/generator/generatorWorker.js',
couchDBChangesFeed: './src/plugins/persistence/couch/CouchChangesFeed.js',
inMemorySearchWorker: './src/api/objects/InMemorySearchWorker.js',
espressoTheme: './src/plugins/themes/espresso-theme.scss',
@@ -99,7 +98,7 @@ const config = {
},
{
test: /\.html$/,
type: 'asset/source'
use: 'html-loader'
},
{
test: /zepto/,
@@ -109,24 +108,25 @@ const config = {
]
},
{
test: /\.(jpg|jpeg|png|svg)$/,
type: 'asset/resource',
generator: {
filename: 'images/[name][ext]'
}
},
{
test: /\.ico$/,
type: 'asset/resource',
generator: {
filename: 'icons/[name][ext]'
}
},
{
test: /\.(woff|woff2?|eot|ttf)$/,
type: 'asset/resource',
generator: {
filename: 'fonts/[name][ext]'
test: /\.(jpg|jpeg|png|svg|ico|woff|woff2?|eot|ttf)$/,
loader: 'file-loader',
options: {
name: '[name].[ext]',
outputPath(url, resourcePath, context) {
if (/\.(jpg|jpeg|png|svg)$/.test(url)) {
return `images/${url}`;
}
if (/\.ico$/.test(url)) {
return `icons/${url}`;
}
if (/\.(woff|woff2?|eot|ttf)$/.test(url)) {
return `fonts/${url}`;
} else {
return `${url}`;
}
}
}
}
]
@@ -134,5 +134,4 @@ const config = {
stats: 'errors-warnings'
};
// eslint-disable-next-line no-undef
module.exports = config;