diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 7ba460118a..747ec79bb6 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -27,7 +27,7 @@ assignees: '' #### Environment + * Open MCT Version: * Deployment Type: * OS: diff --git a/e2e/playwright-visual.config.js b/e2e/playwright-visual.config.js index 7f6df513fc..fbf2c02011 100644 --- a/e2e/playwright-visual.config.js +++ b/e2e/playwright-visual.config.js @@ -4,10 +4,10 @@ /** @type {import('@playwright/test').PlaywrightTestConfig} */ const config = { - retries: 0, - testDir: 'tests', + retries: 0, // visual tests should never retry due to snapshot comparison errors + testDir: 'tests/visual', timeout: 90 * 1000, - workers: 1, + workers: 1, // visual tests should never run in parallel due to test pollution webServer: { command: 'npm run start', port: 8080, @@ -17,7 +17,7 @@ const config = { use: { browserName: "chromium", baseURL: 'http://localhost:8080/', - headless: true, + headless: true, // this needs to remain headless to avoid visual changes due to GPU ignoreHTTPSErrors: true, screenshot: 'on', trace: 'off', @@ -25,8 +25,7 @@ const config = { }, reporter: [ ['list'], - ['junit', { outputFile: 'test-results/results.xml' }], - ['allure-playwright'] + ['junit', { outputFile: 'test-results/results.xml' }] ] }; diff --git a/e2e/tests/plugins/imagery/exampleImagery.e2e.spec.js b/e2e/tests/plugins/imagery/exampleImagery.e2e.spec.js index ef31628c61..1f98aa340a 100644 --- a/e2e/tests/plugins/imagery/exampleImagery.e2e.spec.js +++ b/e2e/tests/plugins/imagery/exampleImagery.e2e.spec.js @@ -344,6 +344,95 @@ test('Example Imagery in Display layout', async ({ page }) => { expect(backgroundImageUrl2 >= backgroundImageUrl1); }); +test.describe('Example imagery thumbnails resize in display layouts', () => { + + test('Resizing the layout changes thumbnail visibility and size', async ({ page }) => { + await page.goto('/', { waitUntil: 'networkidle' }); + + const thumbsWrapperLocator = await page.locator('.c-imagery__thumbs-wrapper'); + // Click button:has-text("Create") + await page.locator('button:has-text("Create")').click(); + + // Click li:has-text("Display Layout") + await page.locator('li:has-text("Display Layout")').click(); + const displayLayoutTitleField = page.locator('text=Properties Title Notes Horizontal grid (px) Vertical grid (px) Horizontal size ( >> input[type="text"]'); + await displayLayoutTitleField.click(); + + await displayLayoutTitleField.fill('Thumbnail Display Layout'); + + // Click text=OK + await Promise.all([ + page.waitForNavigation(), + page.locator('text=OK').click() + ]); + + // Click text=Snapshot Save and Finish Editing Save and Continue Editing >> button >> nth=1 + await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click(); + + // Click text=Save and Finish Editing + await page.locator('text=Save and Finish Editing').click(); + + // Click button:has-text("Create") + await page.locator('button:has-text("Create")').click(); + + // Click li:has-text("Example Imagery") + await page.locator('li:has-text("Example Imagery")').click(); + + const imageryTitleField = page.locator('text=Properties Title Notes Images url list (comma separated) Image load delay (milli >> input[type="text"]'); + // Click text=Properties Title Notes Images url list (comma separated) Image load delay (milli >> input[type="text"] + await imageryTitleField.click(); + + // Fill text=Properties Title Notes Images url list (comma separated) Image load delay (milli >> input[type="text"] + await imageryTitleField.fill('Thumbnail Example Imagery'); + + // Click text=OK + await Promise.all([ + page.waitForNavigation(), + page.locator('text=OK').click() + ]); + + // Click text=Thumbnail Example Imagery Imagery Layout Snapshot >> button >> nth=0 + await Promise.all([ + page.waitForNavigation(), + page.locator('text=Thumbnail Example Imagery Imagery Layout Snapshot >> button').first().click() + ]); + + // Edit mode + await page.locator('text=Thumbnail Display Layout Snapshot >> button').nth(3).click(); + + // Click on example imagery to expose toolbar + await page.locator('text=Thumbnail Example Imagery Snapshot Large View').click(); + + // expect thumbnails not be visible when first added + await expect.soft(thumbsWrapperLocator.isHidden()).toBeTruthy(); + + // Resize the example imagery vertically to change the thumbnail visibility + /* + The following arbitrary values are added to observe the separate visual + conditions of the thumbnails (hidden, small thumbnails, regular thumbnails). + Specifically, height is set to 50px for small thumbs and 100px for regular + */ + // Click #mct-input-id-103 + await page.locator('#mct-input-id-103').click(); + + // Fill #mct-input-id-103 + await page.locator('#mct-input-id-103').fill('50'); + + expect(thumbsWrapperLocator.isVisible()).toBeTruthy(); + await expect(thumbsWrapperLocator).toHaveClass(/is-small-thumbs/); + + // Resize the example imagery vertically to change the thumbnail visibility + // Click #mct-input-id-103 + await page.locator('#mct-input-id-103').click(); + + // Fill #mct-input-id-103 + await page.locator('#mct-input-id-103').fill('100'); + + expect(thumbsWrapperLocator.isVisible()).toBeTruthy(); + await expect(thumbsWrapperLocator).not.toHaveClass(/is-small-thumbs/); + }); +}); + test.describe('Example Imagery in Flexible layout', () => { test.fixme('Can use Mouse Wheel to zoom in and out of previous image'); test.fixme('Can use alt+drag to move around image once zoomed in'); diff --git a/e2e/tests/plugins/timeConductor/timeConductor.e2e.spec.js b/e2e/tests/plugins/timeConductor/timeConductor.e2e.spec.js index 8ca1285aa8..ed759a53df 100644 --- a/e2e/tests/plugins/timeConductor/timeConductor.e2e.spec.js +++ b/e2e/tests/plugins/timeConductor/timeConductor.e2e.spec.js @@ -23,9 +23,9 @@ const { test } = require('../../../fixtures.js'); const { expect } = require('@playwright/test'); -test.describe('Time counductor operations', () => { +test.describe('Time conductor operations', () => { test('validate start time does not exceeds end time', async ({ page }) => { - //Go to baseURL + // Go to baseURL await page.goto('/', { waitUntil: 'networkidle' }); const year = new Date().getFullYear(); @@ -73,37 +73,163 @@ test.describe('Time counductor operations', () => { // Try to change the realtime offsets when in realtime (local clock) mode. test.describe('Time conductor input fields real-time mode', () => { test('validate input fields in real-time mode', async ({ page }) => { - //Go to baseURL + const startOffset = { + secs: '23' + }; + + const endOffset = { + secs: '31' + }; + + // Go to baseURL await page.goto('/', { waitUntil: 'networkidle' }); - // Click fixed timespan button - await page.locator('.c-button__label >> text=Fixed Timespan').click(); + // Switch to real-time mode + await setRealTimeMode(page); - // Click local clock - await page.locator('.icon-clock >> text=Local Clock').click(); - - // Click time offset button - await page.locator('.c-conductor__delta-button >> text=00:30:00').click(); - - // Input start time offset - await page.fill('.pr-time-controls__secs', '23'); - - // Click the check button - await page.locator('.icon-check').click(); + // Set start time offset + await setStartOffset(page, startOffset); // Verify time was updated on time offset button - await expect(page.locator('.c-conductor__delta-button').first()).toContainText('00:30:23'); + await expect(page.locator('data-testid=conductor-start-offset-button')).toContainText('00:30:23'); - // Click time offset set preceding now button - await page.locator('.c-conductor__delta-button >> text=00:00:30').click(); - - // Input preceding time offset - await page.fill('.pr-time-controls__secs', '31'); - - // Click the check buttons - await page.locator('.icon-check').click(); + // Set end time offset + await setEndOffset(page, endOffset); // Verify time was updated on preceding time offset button - await expect(page.locator('.c-conductor__delta-button').nth(1)).toContainText('00:00:31'); + await expect(page.locator('data-testid=conductor-end-offset-button')).toContainText('00:00:31'); + }); + + /** + * Verify that offsets and url params are preserved when switching + * between fixed timespan and real-time mode. + */ + test('preserve offsets and url params when switching between fixed and real-time mode', async ({ page }) => { + const startOffset = { + mins: '30', + secs: '23' + }; + + const endOffset = { + secs: '01' + }; + + // Convert offsets to milliseconds + const startDelta = (30 * 60 * 1000) + (23 * 1000); + const endDelta = (1 * 1000); + + // Go to baseURL + await page.goto('/', { waitUntil: 'networkidle' }); + + // Switch to real-time mode + await setRealTimeMode(page); + + // Set start time offset + await setStartOffset(page, startOffset); + + // Set end time offset + await setEndOffset(page, endOffset); + + // Switch to fixed timespan mode + await setFixedTimeMode(page); + + // Switch back to real-time mode + await setRealTimeMode(page); + + // Verify updated start time offset persists after mode switch + await expect(page.locator('data-testid=conductor-start-offset-button')).toContainText('00:30:23'); + + // Verify updated end time offset persists after mode switch + await expect(page.locator('data-testid=conductor-end-offset-button')).toContainText('00:00:01'); + + // Verify url parameters persist after mode switch + await page.waitForNavigation(); + expect(page.url()).toContain(`startDelta=${startDelta}`); + expect(page.url()).toContain(`endDelta=${endDelta}`); }); }); + +/** + * @typedef {Object} OffsetValues + * @property {string | undefined} hours + * @property {string | undefined} mins + * @property {string | undefined} secs + */ + +/** + * Set the values (hours, mins, secs) for the start time offset when in realtime mode + * @param {import('@playwright/test').Page} page + * @param {OffsetValues} offset + */ +async function setStartOffset(page, offset) { + const startOffsetButton = page.locator('data-testid=conductor-start-offset-button'); + await setTimeConductorOffset(page, offset, startOffsetButton); +} + +/** + * Set the values (hours, mins, secs) for the end time offset when in realtime mode + * @param {import('@playwright/test').Page} page + * @param {OffsetValues} offset + */ +async function setEndOffset(page, offset) { + const endOffsetButton = page.locator('data-testid=conductor-end-offset-button'); + await setTimeConductorOffset(page, offset, endOffsetButton); +} + +/** + * Set the time conductor to fixed timespan mode + * @param {import('@playwright/test').Page} page + */ +async function setFixedTimeMode(page) { + await setTimeConductorMode(page, true); +} + +/** + * Set the time conductor to realtime mode + * @param {import('@playwright/test').Page} page + */ +async function setRealTimeMode(page) { + await setTimeConductorMode(page, false); +} + +/** + * Set the values (hours, mins, secs) for the TimeConductor offsets when in realtime mode + * @param {import('@playwright/test').Page} page + * @param {OffsetValues} offset + * @param {import('@playwright/test').Locator} offsetButton + */ +async function setTimeConductorOffset(page, {hours, mins, secs}, offsetButton) { + await offsetButton.click(); + + if (hours) { + await page.fill('.pr-time-controls__hrs', hours); + } + + if (mins) { + await page.fill('.pr-time-controls__mins', mins); + } + + if (secs) { + await page.fill('.pr-time-controls__secs', secs); + } + + // Click the check button + await page.locator('.icon-check').click(); +} + +/** + * Set the time conductor mode to either fixed timespan or realtime mode. + * @param {import('@playwright/test').Page} page + * @param {boolean} [isFixedTimespan=true] true for fixed timespan mode, false for realtime mode; default is true + */ +async function setTimeConductorMode(page, isFixedTimespan = true) { + // Click 'mode' button + await page.locator('.c-mode-button').click(); + + // Switch time conductor mode + if (isFixedTimespan) { + await page.locator('data-testid=conductor-modeOption-fixed').click(); + } else { + await page.locator('data-testid=conductor-modeOption-realtime').click(); + } +} diff --git a/e2e/tests/visual/default.visual.spec.js b/e2e/tests/visual/default.visual.spec.js index 7a870c4128..364cdb9d1d 100644 --- a/e2e/tests/visual/default.visual.spec.js +++ b/e2e/tests/visual/default.visual.spec.js @@ -47,7 +47,10 @@ test.beforeEach(async ({ context }) => { path: path.join(__dirname, '../../..', './node_modules/sinon/pkg/sinon.js') }); await context.addInitScript(() => { - window.__clock = sinon.useFakeTimers(); //Set browser clock to UNIX Epoch + window.__clock = sinon.useFakeTimers({ + now: 0, + shouldAdvanceTime: true + }); //Set browser clock to UNIX Epoch }); }); @@ -171,3 +174,24 @@ test('Visual - Sine Wave Generator Form', async ({ page }) => { await page.waitForTimeout(VISUAL_GRACE_PERIOD); await percySnapshot(page, 'removed amplitude property value'); }); + +test('Visual - Save Successful Banner', async ({ page }) => { + //Go to baseURL + await page.goto('/', { waitUntil: 'networkidle' }); + + //Click the Create button + await page.click('button:has-text("Create")'); + + //NOTE Something other than example imagery + await page.click('text=Timer'); + + // Click text=OK + await page.click('text=OK'); + await page.locator('.c-message-banner__message').hover({ trial: true }); + await percySnapshot(page, 'Banner message shown'); + + //Wait until Save Banner is gone + await page.waitForSelector('.c-message-banner__message', { state: 'detached'}); + await percySnapshot(page, 'Banner message gone'); + +}); diff --git a/index.html b/index.html index 5aae47c16f..4cfcacb001 100644 --- a/index.html +++ b/index.html @@ -196,6 +196,8 @@ openmct.install(openmct.plugins.Clock({ enableClockIndicator: true })); openmct.install(openmct.plugins.Timer()); openmct.install(openmct.plugins.Timelist()); + openmct.install(openmct.plugins.BarChart()); + openmct.install(openmct.plugins.ScatterPlot()); openmct.start(); diff --git a/package.json b/package.json index 0497345d35..652d261ccf 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "devDependencies": { "@babel/eslint-parser": "7.16.3", "@braintree/sanitize-url": "6.0.0", - "@percy/cli": "1.0.4", + "@percy/cli": "1.2.1", "@percy/playwright": "1.0.3", "@playwright/test": "1.21.1", "@types/eventemitter3": "^1.0.0", @@ -37,7 +37,7 @@ "imports-loader": "0.8.0", "jasmine-core": "4.1.1", "jsdoc": "3.5.5", - "karma": "6.3.18", + "karma": "6.3.20", "karma-chrome-launcher": "3.1.1", "karma-cli": "2.0.0", "karma-coverage": "2.2.0", @@ -48,7 +48,7 @@ "karma-sourcemap-loader": "0.3.8", "karma-spec-reporter": "0.0.34", "karma-webpack": "5.0.0", - "lighthouse": "9.5.0", + "lighthouse": "9.6.1", "location-bar": "3.0.1", "lodash": "4.17.21", "mini-css-extract-plugin": "2.6.0", @@ -64,7 +64,7 @@ "resolve-url-loader": "5.0.0", "sass": "1.49.9", "sass-loader": "12.6.0", - "sinon": "13.0.1", + "sinon": "14.0.0", "style-loader": "^1.0.1", "uuid": "3.3.3", "vue": "2.6.14", @@ -73,7 +73,7 @@ "vue-template-compiler": "2.6.14", "webpack": "5.68.0", "webpack-cli": "4.9.2", - "webpack-dev-middleware": "5.3.1", + "webpack-dev-middleware": "5.3.3", "webpack-hot-middleware": "2.25.1", "webpack-merge": "5.8.0", "zepto": "1.2.0" @@ -95,7 +95,7 @@ "test:e2e:ci": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome smoke default condition timeConductor branding clock exampleImagery", "test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome", "test:e2e:updatesnapshots": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome --grep @snapshot --update-snapshots", - "test:e2e:visual": "percy exec --config ./e2e/.percy.yml -- npx playwright test --config=e2e/playwright-visual.config.js default", + "test:e2e:visual": "percy exec --config ./e2e/.percy.yml -- npx playwright test --config=e2e/playwright-visual.config.js", "test:e2e:full": "npx playwright test --config=e2e/playwright-ci.config.js", "test:watch": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --no-single-run", "jsdoc": "jsdoc -c jsdoc.json -R API.md -r -d dist/docs/api", diff --git a/src/MCT.js b/src/MCT.js index 3ea51a1d4e..0e46eab52e 100644 --- a/src/MCT.js +++ b/src/MCT.js @@ -242,8 +242,6 @@ define([ // Plugins that are installed by default this.install(this.plugins.Plot()); - this.install(this.plugins.ScatterPlot()); - this.install(this.plugins.BarChart()); this.install(this.plugins.TelemetryTable.default()); this.install(PreviewPlugin.default()); this.install(LicensesPlugin.default()); diff --git a/src/api/menu/components/Menu.vue b/src/api/menu/components/Menu.vue index 0073062e0e..4d0cf39372 100644 --- a/src/api/menu/components/Menu.vue +++ b/src/api/menu/components/Menu.vue @@ -12,6 +12,7 @@ :key="action.name" :class="[action.cssClass, action.isDisabled ? 'disabled' : '']" :title="action.description" + :data-testid="action.testId || false" @click="action.onItemClicked" > {{ action.name }} @@ -37,6 +38,7 @@ :key="action.name" :class="action.cssClass" :title="action.description" + :data-testid="action.testId || false" @click="action.onItemClicked" > {{ action.name }} diff --git a/src/api/menu/components/SuperMenu.vue b/src/api/menu/components/SuperMenu.vue index d7fe6a7a67..7b66c68b65 100644 --- a/src/api/menu/components/SuperMenu.vue +++ b/src/api/menu/components/SuperMenu.vue @@ -15,6 +15,7 @@ :key="action.name" :class="[action.cssClass, action.isDisabled ? 'disabled' : '']" :title="action.description" + :data-testid="action.testId || false" @click="action.onItemClicked" @mouseover="toggleItemDescription(action)" @mouseleave="toggleItemDescription()" @@ -45,6 +46,7 @@ :key="action.name" :class="action.cssClass" :title="action.description" + :data-testid="action.testId || false" @click="action.onItemClicked" @mouseover="toggleItemDescription(action)" @mouseleave="toggleItemDescription()" diff --git a/src/exporters/ImageExporter.js b/src/exporters/ImageExporter.js index 5ff877cc61..22a9ed2e2e 100644 --- a/src/exporters/ImageExporter.js +++ b/src/exporters/ImageExporter.js @@ -51,7 +51,7 @@ class ImageExporter { const overlays = this.openmct.overlays; const dialog = overlays.dialog({ iconClass: 'info', - message: 'Caputuring an image', + message: 'Capturing image, please wait...', buttons: [ { label: 'Cancel', diff --git a/src/plugins/DeviceClassifier/src/DeviceClassifier.js b/src/plugins/DeviceClassifier/src/DeviceClassifier.js index d305112d70..7bba65d964 100644 --- a/src/plugins/DeviceClassifier/src/DeviceClassifier.js +++ b/src/plugins/DeviceClassifier/src/DeviceClassifier.js @@ -52,7 +52,6 @@ export default (agent, document) => { if (agent.isMobile()) { const mediaQuery = window.matchMedia("(orientation: landscape)"); function eventHandler(event) { - console.log("changed"); if (event.matches) { body.classList.remove("portrait"); body.classList.add("landscape"); diff --git a/src/plugins/gauge/components/Gauge.vue b/src/plugins/gauge/components/Gauge.vue index 91036ce965..19fa4a8218 100644 --- a/src/plugins/gauge/components/Gauge.vue +++ b/src/plugins/gauge/components/Gauge.vue @@ -85,14 +85,13 @@ class="c-dial__bg" viewBox="0 0 10 10" > -
@@ -235,13 +234,13 @@ >
@@ -275,6 +274,7 @@ import { DIAL_VALUE_DEG_OFFSET, getLimitDegree } from '../gauge-limit-util'; const LIMIT_PADDING_IN_PERCENT = 10; +const DEFAULT_CURRENT_VALUE = '--'; export default { name: 'Gauge', @@ -283,7 +283,7 @@ export default { let gaugeController = this.domainObject.configuration.gaugeController; return { - curVal: 0, + curVal: DEFAULT_CURRENT_VALUE, digits: 3, precision: gaugeController.precision, displayMinMax: gaugeController.isDisplayMinMax, @@ -319,6 +319,45 @@ export default { return VIEWBOX_STR.replace('X', this.digits * DIGITS_RATIO); }, + isDialLowLimit() { + return this.limitLow.length > 0 && this.dialLowLimitDeg < getLimitDegree('low', 'max'); + }, + isDialLowLimitLow() { + return this.dialLowLimitDeg >= getLimitDegree('low', 'q1'); + }, + isDialLowLimitMid() { + return this.dialLowLimitDeg >= getLimitDegree('low', 'q2'); + }, + isDialLowLimitHigh() { + return this.dialLowLimitDeg >= getLimitDegree('low', 'q3'); + }, + isDialHighLimit() { + return this.limitHigh.length > 0 && this.dialHighLimitDeg < getLimitDegree('high', 'max'); + }, + isDialHighLimitLow() { + return this.dialHighLimitDeg <= getLimitDegree('high', 'max'); + }, + isDialHighLimitMid() { + return this.dialHighLimitDeg <= getLimitDegree('high', 'q2'); + }, + isDialHighLimitHigh() { + return this.dialHighLimitDeg <= getLimitDegree('high', 'q3'); + }, + isDialFilledValueLow() { + return this.degValue >= getLimitDegree('low', 'q1'); + }, + isDialFilledValueMid() { + return this.degValue >= getLimitDegree('low', 'q2'); + }, + isDialFilledValueHigh() { + return this.degValue >= getLimitDegree('low', 'q3'); + }, + isMeterLimitHigh() { + return this.limitHigh.length > 0 && this.meterHighLimitPerc > 0; + }, + isMeterLimitLow() { + return this.limitLow.length > 0 && this.meterLowLimitPerc > 0; + }, typeDial() { return this.matchGaugeType('dial'); }, @@ -459,13 +498,14 @@ export default { this.unsubscribe = null; } - this.metadata = null; + this.curVal = DEFAULT_CURRENT_VALUE; this.formats = null; - this.valueKey = null; - this.limitHigh = null; - this.limitLow = null; + this.limitHigh = ''; + this.limitLow = ''; + this.metadata = null; this.rangeHigh = null; this.rangeLow = null; + this.valueKey = null; }, request(domainObject = this.telemetryObject) { this.metadata = this.openmct.telemetry.getMetadata(domainObject); @@ -518,13 +558,20 @@ export default { } else if (telemetryLimit.WATCH) { limits = telemetryLimit.WATCH; } else { - this.openmct.notifications.error('No limits definition for given telemetry'); + this.openmct.notifications.error('No limits definition for given telemetry, hiding low and high limits'); + this.displayMinMax = false; + this.limitHigh = ''; + this.limitLow = ''; + + return; } this.limitHigh = this.round(limits.high[this.valueKey]); this.limitLow = this.round(limits.low[this.valueKey]); this.rangeHigh = this.round(this.limitHigh + this.limitHigh * LIMIT_PADDING_IN_PERCENT / 100); this.rangeLow = this.round(this.limitLow - Math.abs(this.limitLow * LIMIT_PADDING_IN_PERCENT / 100)); + + this.displayMinMax = this.domainObject.configuration.gaugeController.isDisplayMinMax; }, updateValue(datum) { this.datum = datum; diff --git a/src/plugins/notebook/components/Notebook.vue b/src/plugins/notebook/components/Notebook.vue index f045832a9a..ee76300db4 100644 --- a/src/plugins/notebook/components/Notebook.vue +++ b/src/plugins/notebook/components/Notebook.vue @@ -177,7 +177,7 @@ export default { SearchResults, Sidebar }, - inject: ['openmct', 'snapshotContainer'], + inject: ['agent', 'openmct', 'snapshotContainer'], props: { domainObject: { type: Object, @@ -455,12 +455,9 @@ export default { - tablet portrait - in a layout frame (within .c-so-view) */ - const classList = document.querySelector('body').classList; - const isPhone = Array.from(classList).includes('phone'); - const isTablet = Array.from(classList).includes('tablet'); - // address in https://github.com/nasa/openmct/issues/4875 - // eslint-disable-next-line compat/compat - const isPortrait = window.screen.orientation.type.includes('portrait'); + const isPhone = this.agent.isPhone(); + const isTablet = this.agent.isTablet(); + const isPortrait = this.agent.isPortrait(); const isInLayout = Boolean(this.$el.closest('.c-so-view')); const sidebarCoversEntries = (isPhone || (isTablet && isPortrait) || isInLayout); this.sidebarCoversEntries = sidebarCoversEntries; diff --git a/src/plugins/notebook/plugin.js b/src/plugins/notebook/plugin.js index 033b04f8fc..be02262972 100644 --- a/src/plugins/notebook/plugin.js +++ b/src/plugins/notebook/plugin.js @@ -8,6 +8,7 @@ import { notebookImageMigration, IMAGE_MIGRATION_VER } from '../notebook/utils/n import { NOTEBOOK_TYPE } from './notebook-constants'; import Vue from 'vue'; +import Agent from '@/utils/agent/Agent'; export default function NotebookPlugin() { return function install(openmct) { @@ -18,7 +19,7 @@ export default function NotebookPlugin() { } openmct.actions.register(new CopyToNotebookAction(openmct)); - + const agent = new Agent(window); const notebookType = { name: 'Notebook', description: 'Create and save timestamped notes with embedded object snapshots.', @@ -142,6 +143,7 @@ export default function NotebookPlugin() { Notebook }, provide: { + agent, openmct, snapshotContainer }, diff --git a/src/plugins/plot/MctPlot.vue b/src/plugins/plot/MctPlot.vue index ea5d8dd398..8ae4f08bf5 100644 --- a/src/plugins/plot/MctPlot.vue +++ b/src/plugins/plot/MctPlot.vue @@ -154,6 +154,22 @@ > +
+ + +
@@ -213,16 +229,16 @@ export default { }; } }, - gridLines: { + initGridLines: { type: Boolean, default() { return true; } }, - cursorGuide: { + initCursorGuide: { type: Boolean, default() { - return true; + return false; } }, plotTickWidth: { @@ -252,7 +268,9 @@ export default { isTimeOutOfSync: false, showLimitLineLabels: undefined, isFrozenOnMouseDown: false, - hasSameRangeValue: true + hasSameRangeValue: true, + cursorGuide: this.initCursorGuide, + gridLines: this.initGridLines }; }, computed: { @@ -273,6 +291,14 @@ export default { return this.plotTickWidth || this.tickWidth; } }, + watch: { + initGridLines(newGridLines) { + this.gridLines = newGridLines; + }, + initCursorGuide(newCursorGuide) { + this.cursorGuide = newCursorGuide; + } + }, mounted() { document.addEventListener('keydown', this.handleKeyDown); document.addEventListener('keyup', this.handleKeyUp); @@ -1130,6 +1156,14 @@ export default { }, legendHoverChanged(data) { this.showLimitLineLabels = data; + }, + toggleCursorGuide() { + this.cursorGuide = !this.cursorGuide; + this.$emit('cursorGuide', this.cursorGuide); + }, + toggleGridLines() { + this.gridLines = !this.gridLines; + this.$emit('gridLines', this.gridLines); } } }; diff --git a/src/plugins/plot/Plot.vue b/src/plugins/plot/Plot.vue index beb3b92456..2decf3a029 100644 --- a/src/plugins/plot/Plot.vue +++ b/src/plugins/plot/Plot.vue @@ -24,41 +24,6 @@ ref="plotWrapper" class="c-plot holder holder-plot has-control-bar" > -
- - - - - - -
' + template: '' }); }, + getViewContext() { + if (!component) { + return {}; + } + + return component.$refs.plotComponent.getViewContext(); + }, destroy: function () { component.$destroy(); component = undefined; diff --git a/src/plugins/plot/actions/ViewActions.js b/src/plugins/plot/actions/ViewActions.js new file mode 100644 index 0000000000..7ebf7fe726 --- /dev/null +++ b/src/plugins/plot/actions/ViewActions.js @@ -0,0 +1,57 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2022, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ +import {isPlotView} from "@/plugins/plot/actions/utils"; + +const exportPNG = { + name: 'Export as PNG', + key: 'export-as-png', + description: 'Export This View\'s Data as PNG', + cssClass: 'c-icon-button icon-download', + group: 'view', + invoke(objectPath, view) { + view.getViewContext().exportPNG(); + } +}; + +const exportJPG = { + name: 'Export as JPG', + key: 'export-as-jpg', + description: 'Export This View\'s Data as JPG', + cssClass: 'c-icon-button icon-download', + group: 'view', + invoke(objectPath, view) { + view.getViewContext().exportJPG(); + } +}; + +const viewActions = [ + exportPNG, + exportJPG +]; + +viewActions.forEach(action => { + action.appliesTo = (objectPath, view = {}) => { + return isPlotView(view); + }; +}); + +export default viewActions; diff --git a/src/plugins/plot/actions/utils.js b/src/plugins/plot/actions/utils.js new file mode 100644 index 0000000000..2bebbecf4d --- /dev/null +++ b/src/plugins/plot/actions/utils.js @@ -0,0 +1,3 @@ +export function isPlotView(view) { + return view.key === 'plot-single' || view.key === 'plot-overlay' || view.key === 'plot-stacked'; +} diff --git a/src/plugins/plot/overlayPlot/OverlayPlotViewProvider.js b/src/plugins/plot/overlayPlot/OverlayPlotViewProvider.js index 3a70da2848..e34bb59d2e 100644 --- a/src/plugins/plot/overlayPlot/OverlayPlotViewProvider.js +++ b/src/plugins/plot/overlayPlot/OverlayPlotViewProvider.js @@ -65,9 +65,16 @@ export default function OverlayPlotViewProvider(openmct) { } }; }, - template: '' + template: '' }); }, + getViewContext() { + if (!component) { + return {}; + } + + return component.$refs.plotComponent.getViewContext(); + }, destroy: function () { component.$destroy(); component = undefined; diff --git a/src/plugins/plot/plugin.js b/src/plugins/plot/plugin.js index 93c3122ca5..89f6f005b7 100644 --- a/src/plugins/plot/plugin.js +++ b/src/plugins/plot/plugin.js @@ -25,6 +25,7 @@ import StackedPlotViewProvider from './stackedPlot/StackedPlotViewProvider'; import PlotsInspectorViewProvider from './inspector/PlotsInspectorViewProvider'; import OverlayPlotCompositionPolicy from './overlayPlot/OverlayPlotCompositionPolicy'; import StackedPlotCompositionPolicy from './stackedPlot/StackedPlotCompositionPolicy'; +import PlotViewActions from "./actions/ViewActions"; export default function () { return function install(openmct) { @@ -67,6 +68,9 @@ export default function () { openmct.composition.addPolicy(new OverlayPlotCompositionPolicy(openmct).allow); openmct.composition.addPolicy(new StackedPlotCompositionPolicy(openmct).allow); + + PlotViewActions.forEach(action => { + openmct.actions.register(action); + }); }; } - diff --git a/src/plugins/plot/pluginSpec.js b/src/plugins/plot/pluginSpec.js index f1a0e475b5..a78107ffee 100644 --- a/src/plugins/plot/pluginSpec.js +++ b/src/plugins/plot/pluginSpec.js @@ -724,16 +724,16 @@ describe("the plugin", function () { }); it("turns on cursor Guides all telemetry objects", (done) => { - expect(plotViewComponentObject.cursorGuide).toBeFalse(); - plotViewComponentObject.toggleCursorGuide(); + expect(plotViewComponentObject.$children[0].cursorGuide).toBeFalse(); + plotViewComponentObject.$children[0].cursorGuide = true; Vue.nextTick(() => { - expect(plotViewComponentObject.$children[0].component.$children[0].cursorGuide).toBeTrue(); + expect(plotViewComponentObject.$children[0].cursorGuide).toBeTrue(); done(); }); }); it("shows grid lines for all telemetry objects", () => { - expect(plotViewComponentObject.gridLines).toBeTrue(); + expect(plotViewComponentObject.$children[0].gridLines).toBeTrue(); let gridLinesContainer = element.querySelectorAll(".gl-plot-display-area .js-ticks"); let visible = 0; gridLinesContainer.forEach(el => { @@ -745,10 +745,10 @@ describe("the plugin", function () { }); it("hides grid lines for all telemetry objects", (done) => { - expect(plotViewComponentObject.gridLines).toBeTrue(); - plotViewComponentObject.toggleGridLines(); + expect(plotViewComponentObject.$children[0].gridLines).toBeTrue(); + plotViewComponentObject.$children[0].gridLines = false; Vue.nextTick(() => { - expect(plotViewComponentObject.gridLines).toBeFalse(); + expect(plotViewComponentObject.$children[0].gridLines).toBeFalse(); let gridLinesContainer = element.querySelectorAll(".gl-plot-display-area .js-ticks"); let visible = 0; gridLinesContainer.forEach(el => { diff --git a/src/plugins/plot/stackedPlot/StackedPlot.vue b/src/plugins/plot/stackedPlot/StackedPlot.vue index 1c63a0413d..1d409d1990 100644 --- a/src/plugins/plot/stackedPlot/StackedPlot.vue +++ b/src/plugins/plot/stackedPlot/StackedPlot.vue @@ -22,41 +22,6 @@