mirror of
https://github.com/nasa/openmct.git
synced 2025-03-22 03:55:31 +00:00
Merge branch 'master' of https://github.com/nasa/openmct
This commit is contained in:
commit
4e79725897
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -27,7 +27,7 @@ assignees: ''
|
||||
|
||||
#### Environment
|
||||
<!--- If encountered on local machine, execute the following:
|
||||
<!--- npx envinfo --system --browsers --npmPackages --binaries --languages --markdown -->
|
||||
<!--- npx envinfo --system --browsers --npmPackages --binaries --markdown -->
|
||||
* Open MCT Version: <!--- date of build, version, or SHA -->
|
||||
* Deployment Type: <!--- npm dev? VIPER Dev? openmct-yamcs? -->
|
||||
* OS:
|
||||
|
@ -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' }]
|
||||
]
|
||||
};
|
||||
|
||||
|
@ -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');
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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');
|
||||
|
||||
});
|
||||
|
@ -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();
|
||||
</script>
|
||||
</html>
|
||||
|
12
package.json
12
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",
|
||||
|
@ -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());
|
||||
|
@ -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 }}
|
||||
|
@ -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()"
|
||||
|
@ -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',
|
||||
|
@ -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");
|
||||
|
@ -85,14 +85,13 @@
|
||||
class="c-dial__bg"
|
||||
viewBox="0 0 10 10"
|
||||
>
|
||||
|
||||
<g
|
||||
v-if="limitLow !== null && dialLowLimitDeg < getLimitDegree('low', 'max')"
|
||||
v-if="isDialLowLimit"
|
||||
class="c-dial__limit-low"
|
||||
:style="`transform: rotate(${dialLowLimitDeg}deg)`"
|
||||
>
|
||||
<rect
|
||||
v-if="dialLowLimitDeg >= getLimitDegree('low', 'q1')"
|
||||
v-if="isDialLowLimitLow"
|
||||
class="c-dial__low-limit__low"
|
||||
x="5"
|
||||
y="5"
|
||||
@ -100,7 +99,7 @@
|
||||
height="5"
|
||||
/>
|
||||
<rect
|
||||
v-if="dialLowLimitDeg >= getLimitDegree('low', 'q2')"
|
||||
v-if="isDialLowLimitMid"
|
||||
class="c-dial__low-limit__mid"
|
||||
x="5"
|
||||
y="0"
|
||||
@ -108,7 +107,7 @@
|
||||
height="5"
|
||||
/>
|
||||
<rect
|
||||
v-if="dialLowLimitDeg >= getLimitDegree('low', 'q3')"
|
||||
v-if="isDialLowLimitHigh"
|
||||
class="c-dial__low-limit__high"
|
||||
x="0"
|
||||
y="0"
|
||||
@ -118,12 +117,12 @@
|
||||
</g>
|
||||
|
||||
<g
|
||||
v-if="limitHigh !== null && dialHighLimitDeg < getLimitDegree('high', 'max')"
|
||||
v-if="isDialHighLimit"
|
||||
class="c-dial__limit-high"
|
||||
:style="`transform: rotate(${dialHighLimitDeg}deg)`"
|
||||
>
|
||||
<rect
|
||||
v-if="dialHighLimitDeg <= getLimitDegree('high', 'max')"
|
||||
v-if="isDialHighLimitLow"
|
||||
class="c-dial__high-limit__low"
|
||||
x="0"
|
||||
y="5"
|
||||
@ -131,7 +130,7 @@
|
||||
height="5"
|
||||
/>
|
||||
<rect
|
||||
v-if="dialHighLimitDeg <= getLimitDegree('high', 'q2')"
|
||||
v-if="isDialHighLimitMid"
|
||||
class="c-dial__high-limit__mid"
|
||||
x="0"
|
||||
y="0"
|
||||
@ -139,7 +138,7 @@
|
||||
height="5"
|
||||
/>
|
||||
<rect
|
||||
v-if="dialHighLimitDeg <= getLimitDegree('high', 'q3')"
|
||||
v-if="isDialHighLimitHigh"
|
||||
class="c-dial__high-limit__high"
|
||||
x="5"
|
||||
y="0"
|
||||
@ -159,7 +158,7 @@
|
||||
:style="`transform: rotate(${degValueFilledDial}deg)`"
|
||||
>
|
||||
<rect
|
||||
v-if="degValue >= getLimitDegree('low', 'q1')"
|
||||
v-if="isDialFilledValueLow"
|
||||
class="c-dial__filled-value__low"
|
||||
x="5"
|
||||
y="5"
|
||||
@ -167,7 +166,7 @@
|
||||
height="5"
|
||||
/>
|
||||
<rect
|
||||
v-if="degValue >= getLimitDegree('low', 'q2')"
|
||||
v-if="isDialFilledValueMid"
|
||||
class="c-dial__filled-value__mid"
|
||||
x="5"
|
||||
y="0"
|
||||
@ -175,7 +174,7 @@
|
||||
height="5"
|
||||
/>
|
||||
<rect
|
||||
v-if="degValue >= getLimitDegree('low', 'q3')"
|
||||
v-if="isDialFilledValueHigh"
|
||||
class="c-dial__filled-value__high"
|
||||
x="0"
|
||||
y="0"
|
||||
@ -216,13 +215,13 @@
|
||||
></div>
|
||||
|
||||
<div
|
||||
v-if="limitHigh !== null && meterHighLimitPerc > 0"
|
||||
v-if="isMeterLimitHigh"
|
||||
class="c-meter__limit-high"
|
||||
:style="`height: ${meterHighLimitPerc}%`"
|
||||
></div>
|
||||
|
||||
<div
|
||||
v-if="limitLow !== null && meterLowLimitPerc > 0"
|
||||
v-if="isMeterLimitLow"
|
||||
class="c-meter__limit-low"
|
||||
:style="`height: ${meterLowLimitPerc}%`"
|
||||
></div>
|
||||
@ -235,13 +234,13 @@
|
||||
></div>
|
||||
|
||||
<div
|
||||
v-if="limitHigh !== null && meterHighLimitPerc > 0"
|
||||
v-if="isMeterLimitHigh"
|
||||
class="c-meter__limit-high"
|
||||
:style="`width: ${meterHighLimitPerc}%`"
|
||||
></div>
|
||||
|
||||
<div
|
||||
v-if="limitLow !== null && meterLowLimitPerc > 0"
|
||||
v-if="isMeterLimitLow"
|
||||
class="c-meter__limit-low"
|
||||
:style="`width: ${meterLowLimitPerc}%`"
|
||||
></div>
|
||||
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
},
|
||||
|
@ -154,6 +154,22 @@
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
<div class="c-button-set c-button-set--strip-h">
|
||||
<button
|
||||
class="c-button icon-crosshair"
|
||||
:class="{ 'is-active': cursorGuide }"
|
||||
title="Toggle cursor guides"
|
||||
@click="toggleCursorGuide"
|
||||
>
|
||||
</button>
|
||||
<button
|
||||
class="c-button"
|
||||
:class="{ 'icon-grid-on': gridLines, 'icon-grid-off': !gridLines }"
|
||||
title="Toggle grid lines"
|
||||
@click="toggleGridLines"
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--Cursor guides-->
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -24,41 +24,6 @@
|
||||
ref="plotWrapper"
|
||||
class="c-plot holder holder-plot has-control-bar"
|
||||
>
|
||||
<div
|
||||
v-if="!options.compact"
|
||||
class="c-control-bar"
|
||||
>
|
||||
<span class="c-button-set c-button-set--strip-h">
|
||||
<button
|
||||
class="c-button icon-download"
|
||||
title="Export This View's Data as PNG"
|
||||
@click="exportPNG()"
|
||||
>
|
||||
<span class="c-button__label">PNG</span>
|
||||
</button>
|
||||
<button
|
||||
class="c-button"
|
||||
title="Export This View's Data as JPG"
|
||||
@click="exportJPG()"
|
||||
>
|
||||
<span class="c-button__label">JPG</span>
|
||||
</button>
|
||||
</span>
|
||||
<button
|
||||
class="c-button icon-crosshair"
|
||||
:class="{ 'is-active': cursorGuide }"
|
||||
title="Toggle cursor guides"
|
||||
@click="toggleCursorGuide"
|
||||
>
|
||||
</button>
|
||||
<button
|
||||
class="c-button"
|
||||
:class="{ 'icon-grid-on': gridLines, 'icon-grid-off': !gridLines }"
|
||||
title="Toggle grid lines"
|
||||
@click="toggleGridLines"
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref="plotContainer"
|
||||
@ -70,8 +35,8 @@
|
||||
class="c-loading--overlay loading"
|
||||
></div>
|
||||
<mct-plot
|
||||
:grid-lines="gridLines"
|
||||
:cursor-guide="cursorGuide"
|
||||
:init-grid-lines="gridLines"
|
||||
:init-cursor-guide="cursorGuide"
|
||||
:options="options"
|
||||
@loadingUpdated="loadingUpdated"
|
||||
@statusUpdated="setStatus"
|
||||
@ -135,15 +100,14 @@ export default {
|
||||
this.imageExporter.exportPNG(plotElement, 'plot.png', 'export-plot');
|
||||
},
|
||||
|
||||
toggleCursorGuide() {
|
||||
this.cursorGuide = !this.cursorGuide;
|
||||
},
|
||||
|
||||
toggleGridLines() {
|
||||
this.gridLines = !this.gridLines;
|
||||
},
|
||||
setStatus(status) {
|
||||
this.status = status;
|
||||
},
|
||||
getViewContext() {
|
||||
return {
|
||||
exportPNG: this.exportPNG,
|
||||
exportJPG: this.exportJPG
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -80,9 +80,16 @@ export default function PlotViewProvider(openmct) {
|
||||
}
|
||||
};
|
||||
},
|
||||
template: '<plot :options="options"></plot>'
|
||||
template: '<plot ref="plotComponent" :options="options"></plot>'
|
||||
});
|
||||
},
|
||||
getViewContext() {
|
||||
if (!component) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return component.$refs.plotComponent.getViewContext();
|
||||
},
|
||||
destroy: function () {
|
||||
component.$destroy();
|
||||
component = undefined;
|
||||
|
57
src/plugins/plot/actions/ViewActions.js
Normal file
57
src/plugins/plot/actions/ViewActions.js
Normal file
@ -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;
|
3
src/plugins/plot/actions/utils.js
Normal file
3
src/plugins/plot/actions/utils.js
Normal file
@ -0,0 +1,3 @@
|
||||
export function isPlotView(view) {
|
||||
return view.key === 'plot-single' || view.key === 'plot-overlay' || view.key === 'plot-stacked';
|
||||
}
|
@ -65,9 +65,16 @@ export default function OverlayPlotViewProvider(openmct) {
|
||||
}
|
||||
};
|
||||
},
|
||||
template: '<plot :options="options"></plot>'
|
||||
template: '<plot ref="plotComponent" :options="options"></plot>'
|
||||
});
|
||||
},
|
||||
getViewContext() {
|
||||
if (!component) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return component.$refs.plotComponent.getViewContext();
|
||||
},
|
||||
destroy: function () {
|
||||
component.$destroy();
|
||||
component = undefined;
|
||||
|
@ -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);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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 => {
|
||||
|
@ -22,41 +22,6 @@
|
||||
|
||||
<template>
|
||||
<div class="c-plot c-plot--stacked holder holder-plot has-control-bar">
|
||||
<div
|
||||
v-show="!hideExportButtons && !options.compact"
|
||||
class="c-control-bar"
|
||||
>
|
||||
<span class="c-button-set c-button-set--strip-h">
|
||||
<button
|
||||
class="c-button icon-download"
|
||||
title="Export This View's Data as PNG"
|
||||
@click="exportPNG()"
|
||||
>
|
||||
<span class="c-button__label">PNG</span>
|
||||
</button>
|
||||
<button
|
||||
class="c-button"
|
||||
title="Export This View's Data as JPG"
|
||||
@click="exportJPG()"
|
||||
>
|
||||
<span class="c-button__label">JPG</span>
|
||||
</button>
|
||||
</span>
|
||||
<button
|
||||
class="c-button icon-crosshair"
|
||||
:class="{ 'is-active': cursorGuide }"
|
||||
title="Toggle cursor guides"
|
||||
@click="toggleCursorGuide"
|
||||
>
|
||||
</button>
|
||||
<button
|
||||
class="c-button"
|
||||
:class="{ 'icon-grid-on': gridLines, 'icon-grid-off': !gridLines }"
|
||||
title="Toggle grid lines"
|
||||
@click="toggleGridLines"
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
<div class="l-view-section">
|
||||
<stacked-plot-item
|
||||
v-for="object in compositionObjects"
|
||||
@ -69,6 +34,8 @@
|
||||
:plot-tick-width="maxTickWidth"
|
||||
@plotTickWidth="onTickWidthChange"
|
||||
@loadingUpdated="loadingUpdated"
|
||||
@cursorGuide="onCursorGuideChange"
|
||||
@gridLines="onGridLinesChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -184,20 +151,24 @@ export default {
|
||||
this.hideExportButtons = false;
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
toggleCursorGuide() {
|
||||
this.cursorGuide = !this.cursorGuide;
|
||||
},
|
||||
|
||||
toggleGridLines() {
|
||||
this.gridLines = !this.gridLines;
|
||||
},
|
||||
onTickWidthChange(width, plotId) {
|
||||
if (!Object.prototype.hasOwnProperty.call(this.tickWidthMap, plotId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$set(this.tickWidthMap, plotId, width);
|
||||
},
|
||||
onCursorGuideChange(cursorGuide) {
|
||||
this.cursorGuide = cursorGuide === true;
|
||||
},
|
||||
onGridLinesChange(gridLines) {
|
||||
this.gridLines = gridLines === true;
|
||||
},
|
||||
getViewContext() {
|
||||
return {
|
||||
exportPNG: this.exportPNG,
|
||||
exportJPG: this.exportJPG
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -96,6 +96,8 @@ export default {
|
||||
}
|
||||
|
||||
const onTickWidthChange = this.onTickWidthChange;
|
||||
const onCursorGuideChange = this.onCursorGuideChange;
|
||||
const onGridLinesChange = this.onGridLinesChange;
|
||||
const loadingUpdated = this.loadingUpdated;
|
||||
const setStatus = this.setStatus;
|
||||
|
||||
@ -121,16 +123,24 @@ export default {
|
||||
return {
|
||||
...getProps(),
|
||||
onTickWidthChange,
|
||||
onCursorGuideChange,
|
||||
onGridLinesChange,
|
||||
loadingUpdated,
|
||||
setStatus
|
||||
};
|
||||
},
|
||||
template: '<div ref="plotWrapper" class="l-view-section u-style-receiver js-style-receiver" :class="{\'s-status-timeconductor-unsynced\': status && status === \'timeconductor-unsynced\'}"><div v-show="!!loading" class="c-loading--overlay loading"></div><mct-plot :grid-lines="gridLines" :cursor-guide="cursorGuide" :plot-tick-width="plotTickWidth" :options="options" @plotTickWidth="onTickWidthChange" @statusUpdated="setStatus" @loadingUpdated="loadingUpdated"/></div>'
|
||||
template: '<div ref="plotWrapper" class="l-view-section u-style-receiver js-style-receiver" :class="{\'s-status-timeconductor-unsynced\': status && status === \'timeconductor-unsynced\'}"><div v-show="!!loading" class="c-loading--overlay loading"></div><mct-plot :init-grid-lines="gridLines" :init-cursor-guide="cursorGuide" :plot-tick-width="plotTickWidth" :options="options" @plotTickWidth="onTickWidthChange" @cursorGuide="onCursorGuideChange" @gridLines="onGridLinesChange" @statusUpdated="setStatus" @loadingUpdated="loadingUpdated"/></div>'
|
||||
});
|
||||
},
|
||||
onTickWidthChange() {
|
||||
this.$emit('plotTickWidth', ...arguments);
|
||||
},
|
||||
onCursorGuideChange() {
|
||||
this.$emit('cursorGuide', ...arguments);
|
||||
},
|
||||
onGridLinesChange() {
|
||||
this.$emit('gridLines', ...arguments);
|
||||
},
|
||||
setStatus(status) {
|
||||
this.status = status;
|
||||
this.updateComponentProp('status', status);
|
||||
|
@ -67,9 +67,16 @@ export default function StackedPlotViewProvider(openmct) {
|
||||
}
|
||||
};
|
||||
},
|
||||
template: '<stacked-plot :options="options"></stacked-plot>'
|
||||
template: '<stacked-plot ref="plotComponent" :options="options"></stacked-plot>'
|
||||
});
|
||||
},
|
||||
getViewContext() {
|
||||
if (!component) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return component.$refs.plotComponent.getViewContext();
|
||||
},
|
||||
destroy: function () {
|
||||
component.$destroy();
|
||||
component = undefined;
|
||||
|
@ -22,6 +22,7 @@
|
||||
ref="startOffset"
|
||||
class="c-button c-conductor__delta-button"
|
||||
title="Set the time offset after now"
|
||||
data-testid="conductor-start-offset-button"
|
||||
@click.prevent.stop="showTimePopupStart"
|
||||
>
|
||||
{{ offsets.start }}
|
||||
@ -61,6 +62,7 @@
|
||||
ref="endOffset"
|
||||
class="c-button c-conductor__delta-button"
|
||||
title="Set the time offset preceding now"
|
||||
data-testid="conductor-end-offset-button"
|
||||
@click.prevent.stop="showTimePopupEnd"
|
||||
>
|
||||
{{ offsets.end }}
|
||||
|
@ -105,6 +105,7 @@ export default {
|
||||
name: 'Fixed Timespan',
|
||||
description: 'Query and explore data that falls between two fixed datetimes.',
|
||||
cssClass: 'icon-tabular',
|
||||
testId: 'conductor-modeOption-fixed',
|
||||
onItemClicked: () => this.setOption(key)
|
||||
};
|
||||
} else {
|
||||
@ -116,6 +117,7 @@ export default {
|
||||
description: "Monitor streaming data in real-time. The Time "
|
||||
+ "Conductor and displays will automatically advance themselves based on this clock. " + clock.description,
|
||||
cssClass: clock.cssClass || 'icon-clock',
|
||||
testId: 'conductor-modeOption-realtime',
|
||||
onItemClicked: () => this.setOption(key)
|
||||
};
|
||||
}
|
||||
@ -148,7 +150,8 @@ export default {
|
||||
if (clockKey === undefined) {
|
||||
this.openmct.time.stopClock();
|
||||
} else {
|
||||
this.openmct.time.clock(clockKey, configuration.clockOffsets);
|
||||
const offsets = this.openmct.time.clockOffsets() || configuration.clockOffsets;
|
||||
this.openmct.time.clock(clockKey, offsets);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -89,7 +89,21 @@ export default class Agent {
|
||||
* @returns {boolean} true in portrait mode
|
||||
*/
|
||||
isPortrait() {
|
||||
return this.window.innerWidth < this.window.innerHeight;
|
||||
const { screen } = this.window;
|
||||
const hasScreenOrientation = screen && Object.prototype.hasOwnProperty.call(screen, 'orientation');
|
||||
const hasWindowOrientation = Object.prototype.hasOwnProperty.call(this.window, 'orientation');
|
||||
|
||||
if (hasScreenOrientation) {
|
||||
return screen.orientation.type.includes('portrait');
|
||||
} else if (hasWindowOrientation) {
|
||||
// Use window.orientation API if available (e.g. Safari mobile)
|
||||
// which returns [-90, 0, 90, 180] based on device orientation.
|
||||
const { orientation } = this.window;
|
||||
|
||||
return Math.abs(orientation / 90) % 2 === 0;
|
||||
} else {
|
||||
return this.window.innerWidth < this.window.innerHeight;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Check if the user's device is in a landscape-style
|
||||
|
@ -68,7 +68,7 @@ describe("The Agent", function () {
|
||||
expect(agent.isTablet()).toBeTruthy();
|
||||
});
|
||||
|
||||
it("detects display orientation", function () {
|
||||
it("detects display orientation by innerHeight and innerWidth", function () {
|
||||
agent = new Agent(testWindow);
|
||||
testWindow.innerWidth = 1024;
|
||||
testWindow.innerHeight = 400;
|
||||
@ -80,6 +80,34 @@ describe("The Agent", function () {
|
||||
expect(agent.isLandscape()).toBeFalsy();
|
||||
});
|
||||
|
||||
it("detects display orientation by screen.orientation", function () {
|
||||
agent = new Agent(testWindow);
|
||||
testWindow.screen = {
|
||||
orientation: {
|
||||
type: "landscape-primary"
|
||||
}
|
||||
};
|
||||
expect(agent.isPortrait()).toBeFalsy();
|
||||
expect(agent.isLandscape()).toBeTruthy();
|
||||
testWindow.screen = {
|
||||
orientation: {
|
||||
type: "portrait-primary"
|
||||
}
|
||||
};
|
||||
expect(agent.isPortrait()).toBeTruthy();
|
||||
expect(agent.isLandscape()).toBeFalsy();
|
||||
});
|
||||
|
||||
it("detects display orientation by window.orientation", function () {
|
||||
agent = new Agent(testWindow);
|
||||
testWindow.orientation = 90;
|
||||
expect(agent.isPortrait()).toBeFalsy();
|
||||
expect(agent.isLandscape()).toBeTruthy();
|
||||
testWindow.orientation = 0;
|
||||
expect(agent.isPortrait()).toBeTruthy();
|
||||
expect(agent.isLandscape()).toBeFalsy();
|
||||
});
|
||||
|
||||
it("detects touch support", function () {
|
||||
testWindow.ontouchstart = null;
|
||||
expect(new Agent(testWindow).isTouch()).toBe(true);
|
||||
|
Loading…
x
Reference in New Issue
Block a user