Merge remote-tracking branch 'origin/master' into telemetry-comps

This commit is contained in:
Scott Bell
2024-10-28 08:43:49 +01:00
31 changed files with 534 additions and 385 deletions

View File

@ -5,7 +5,7 @@ orbs:
executors: executors:
pw-focal-development: pw-focal-development:
docker: docker:
- image: mcr.microsoft.com/playwright:v1.47.2-focal - image: mcr.microsoft.com/playwright:v1.48.1-focal
environment: environment:
NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed
PERCY_POSTINSTALL_BROWSER: 'true' # Needed to store the percy browser in cache deps PERCY_POSTINSTALL_BROWSER: 'true' # Needed to store the percy browser in cache deps
@ -198,7 +198,7 @@ jobs:
steps: steps:
- build_and_install: - build_and_install:
node-version: lts/hydrogen node-version: lts/hydrogen
- run: npx playwright@1.47.2 install #Necessary for bare ubuntu machine - run: npx playwright@1.48.1 install #Necessary for bare ubuntu machine
- run: | - run: |
export $(cat src/plugins/persistence/couch/.env.ci | xargs) export $(cat src/plugins/persistence/couch/.env.ci | xargs)
docker compose -f src/plugins/persistence/couch/couchdb-compose.yaml up --detach docker compose -f src/plugins/persistence/couch/couchdb-compose.yaml up --detach
@ -286,8 +286,8 @@ workflows:
overall-circleci-commit-status: #These jobs run on every commit overall-circleci-commit-status: #These jobs run on every commit
jobs: jobs:
- lint: - lint:
name: node20-lint name: node22-lint
node-version: lts/iron node-version: '22'
- unit-test: - unit-test:
name: node18-chrome name: node18-chrome
node-version: lts/hydrogen node-version: lts/hydrogen
@ -304,8 +304,8 @@ workflows:
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 at night
jobs: jobs:
- unit-test: - unit-test:
name: node20-chrome-nightly name: node22-chrome-nightly
node-version: lts/iron node-version: '22'
- unit-test: - unit-test:
name: node18-chrome name: node18-chrome
node-version: lts/hydrogen node-version: lts/hydrogen

View File

@ -37,7 +37,7 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- run: npx playwright@1.47.2 install - run: npx playwright@1.48.1 install
- name: Start CouchDB Docker Container and Init with Setup Scripts - name: Start CouchDB Docker Container and Init with Setup Scripts
run: | run: |

View File

@ -30,7 +30,7 @@ jobs:
restore-keys: | restore-keys: |
${{ runner.os }}-node- ${{ runner.os }}-node-
- run: npx playwright@1.47.2 install - run: npx playwright@1.48.1 install
- run: npm ci --no-audit --progress=false - run: npm ci --no-audit --progress=false
- name: Run E2E Tests (Repeated 10 Times) - name: Run E2E Tests (Repeated 10 Times)

View File

@ -28,7 +28,7 @@ jobs:
restore-keys: | restore-keys: |
${{ runner.os }}-node- ${{ runner.os }}-node-
- run: npx playwright@1.47.2 install - run: npx playwright@1.48.1 install
- run: npm ci --no-audit --progress=false - run: npm ci --no-audit --progress=false
- run: npm run test:perf:localhost - run: npm run test:perf:localhost
- run: npm run test:perf:contract - run: npm run test:perf:contract

View File

@ -1,116 +0,0 @@
# GitHub Actions Workflow for Automated Releases
name: Automated Release Workflow
on:
schedule:
# Nightly builds at 6 PM PST every day
- cron: '0 2 * * *'
release:
types:
- created
- published
jobs:
nightly-build:
if: github.event_name == 'schedule'
runs-on: ubuntu-latest
name: Nightly Build and Release
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Set Up Node.js
uses: actions/setup-node@v4
with:
node-version: 'lts/iron' # Specify your Node.js version
registry-url: 'https://registry.npmjs.org/'
- name: Install Dependencies
run: npm ci
- name: Bump Version for Nightly
id: bump_version
run: |
PACKAGE_VERSION=$(node -p "require('./package.json').version")
DATE=$(date +%Y%m%d)
NIGHTLY_VERSION=$(echo $PACKAGE_VERSION | awk -F. -v OFS=. '{$NF+=1; print}')-nightly-$DATE
echo "NIGHTLY_VERSION=${NIGHTLY_VERSION}" >> $GITHUB_ENV
- name: Update package.json
run: |
npm version $NIGHTLY_VERSION --no-git-tag-version
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add package.json
git commit -m "chore: bump version to $NIGHTLY_VERSION for nightly build"
- name: Push Changes
uses: ad-m/github-push-action@v0.6.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
branch: ${{ github.ref }}
- name: Build Project
run: npm run build:prod
- name: Publish Nightly to NPM
run: |
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
npm publish --access public --tag nightly
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
prerelease-build:
if: github.event.release.prerelease == true
runs-on: ubuntu-latest
name: Pre-release (Beta) Build and Publish
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Set Up Node.js
uses: actions/setup-node@v4
with:
node-version: '16' # Specify your Node.js version
registry-url: 'https://registry.npmjs.org/'
- name: Install Dependencies
run: npm ci
- name: Build Project
run: npm run build:prod
- name: Publish Beta to NPM
run: |
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
npm publish --access public --tag beta
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
stable-release-build:
if: github.event.release.prerelease == false
runs-on: ubuntu-latest
name: Stable Release Build and Publish
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Set Up Node.js
uses: actions/setup-node@v4
with:
node-version: '16' # Specify your Node.js version
registry-url: 'https://registry.npmjs.org/'
- name: Install Dependencies
run: npm ci
- name: Build Project
run: npm run build:prod
- name: Publish to NPM
run: |
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@ -510,6 +510,10 @@ async function setTimeConductorBounds(page, { submitChanges = true, ...bounds })
// Open the time conductor popup // Open the time conductor popup
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click(); await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
// FIXME: https://github.com/nasa/openmct/pull/7818
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(500);
if (startDate) { if (startDate) {
await page.getByLabel('Start date').fill(startDate); await page.getByLabel('Start date').fill(startDate);
} }

View File

@ -129,6 +129,7 @@ export async function setBoundsToSpanAllActivities(page, planJson, planObjectUrl
*/ */
export function getEarliestStartTime(planJson) { export function getEarliestStartTime(planJson) {
const activities = Object.values(planJson).flat(); const activities = Object.values(planJson).flat();
return Math.min(...activities.map((activity) => activity.start)); return Math.min(...activities.map((activity) => activity.start));
} }
@ -139,6 +140,7 @@ export function getEarliestStartTime(planJson) {
*/ */
export function getLatestEndTime(planJson) { export function getLatestEndTime(planJson) {
const activities = Object.values(planJson).flat(); const activities = Object.values(planJson).flat();
return Math.max(...activities.map((activity) => activity.end)); return Math.max(...activities.map((activity) => activity.end));
} }
@ -151,6 +153,7 @@ export function getFirstActivity(planJson) {
const groups = Object.keys(planJson); const groups = Object.keys(planJson);
const firstGroupKey = groups[0]; const firstGroupKey = groups[0];
const firstGroupItems = planJson[firstGroupKey]; const firstGroupItems = planJson[firstGroupKey];
return firstGroupItems[0]; return firstGroupItems[0];
} }

View File

@ -16,7 +16,7 @@
"devDependencies": { "devDependencies": {
"@percy/cli": "1.27.4", "@percy/cli": "1.27.4",
"@percy/playwright": "1.0.4", "@percy/playwright": "1.0.4",
"@playwright/test": "1.47.2", "@playwright/test": "1.48.1",
"@axe-core/playwright": "4.8.5" "@axe-core/playwright": "4.8.5"
}, },
"author": { "author": {

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -96,9 +96,6 @@ test.describe('Example Imagery Object', () => {
expect(newPage.url()).toContain('.jpg'); expect(newPage.url()).toContain('.jpg');
}); });
// this requires CORS to be enabled in some fashion
test.fixme('Can right click on image and save it as a file', async ({ page }) => {});
test('Can adjust image brightness/contrast by dragging the sliders', async ({ test('Can adjust image brightness/contrast by dragging the sliders', async ({
page, page,
browserName browserName

View File

@ -0,0 +1,93 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, 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 verifies modifying the image location of the example imagery object.
*/
import { createDomainObjectWithDefaults } from '../../../../appActions.js';
import { expect, test } from '../../../../pluginFixtures.js';
test.describe('Example Imagery Object Custom Images', () => {
let exampleImagery;
test.beforeEach(async ({ page }) => {
//Go to baseURL
await page.goto('./', { waitUntil: 'domcontentloaded' });
// Create a default 'Example Imagery' object
exampleImagery = await createDomainObjectWithDefaults(page, {
name: 'Example Imagery',
type: 'Example Imagery'
});
// Verify that the created object is focused
await expect(page.locator('.l-browse-bar__object-name')).toContainText(exampleImagery.name);
await page.getByLabel('Focused Image Element').hover({ trial: true });
// Wait for image thumbnail auto-scroll to complete
await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport();
});
// this requires CORS to be enabled in some fashion
test.fixme('Can right click on image and save it as a file', async ({ page }) => {});
test('Can provide a custom image location for the example imagery object', async ({ page }) => {
// Modify Example Imagery to create a really stable image which will never let us down
await page.getByRole('button', { name: 'More actions' }).click();
await page.getByRole('menuitem', { name: 'Edit Properties...' }).click();
await page
.locator('#imageLocation-textarea')
.fill(
'https://raw.githubusercontent.com/nasa/openmct/554f77c42fec81cf0f63e62b278012cb08d82af9/e2e/test-data/rick.jpg,https://raw.githubusercontent.com/nasa/openmct/554f77c42fec81cf0f63e62b278012cb08d82af9/e2e/test-data/rick.jpg'
);
await page.getByRole('button', { name: 'Save' }).click();
await page.reload({ waitUntil: 'domcontentloaded' });
// Wait for the thumbnails to finish their scroll animation
// (Wait until the rightmost thumbnail is in view)
await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport();
await expect(page.getByLabel('Image Wrapper')).toBeVisible();
});
test.fixme('Can provide a custom image with spaces in name', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7903'
});
await page.goto(exampleImagery.url, { waitUntil: 'domcontentloaded' });
// Modify Example Imagery to create a really stable image which will never let us down
await page.getByRole('button', { name: 'More actions' }).click();
await page.getByRole('menuitem', { name: 'Edit Properties...' }).click();
await page
.locator('#imageLocation-textarea')
.fill(
'https://raw.githubusercontent.com/nasa/openmct/d8c64f183400afb70137221fc1a035e091bea912/e2e/test-data/rick%20space%20roll.jpg'
);
await page.getByRole('button', { name: 'Save' }).click();
await page.reload({ waitUntil: 'domcontentloaded' });
// Wait for the thumbnails to finish their scroll animation
// (Wait until the rightmost thumbnail is in view)
await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport();
await expect(page.getByLabel('Image Wrapper')).toBeVisible();
});
});

View File

@ -117,7 +117,8 @@ test.describe('Telemetry Table', () => {
endTimeStamp.setUTCMinutes(endTimeStamp.getUTCMinutes() - 5); endTimeStamp.setUTCMinutes(endTimeStamp.getUTCMinutes() - 5);
const endDate = endTimeStamp.toISOString().split('T')[0]; const endDate = endTimeStamp.toISOString().split('T')[0];
const endTime = endTimeStamp.toISOString().split('T')[1]; const milliseconds = endTimeStamp.getMilliseconds();
const endTime = endTimeStamp.toISOString().split('T')[1].replace(`.${milliseconds}Z`, '');
await setTimeConductorBounds(page, { endDate, endTime }); await setTimeConductorBounds(page, { endDate, endTime });

View File

@ -24,65 +24,210 @@ import {
setEndOffset, setEndOffset,
setFixedTimeMode, setFixedTimeMode,
setRealTimeMode, setRealTimeMode,
setStartOffset, setStartOffset
setTimeConductorBounds
} from '../../../../appActions.js'; } from '../../../../appActions.js';
import { expect, test } from '../../../../pluginFixtures.js'; import { expect, test } from '../../../../pluginFixtures.js';
test.describe('Time conductor operations', () => { test.describe('Time conductor operations', () => {
test('validate start time does not exceed end time', async ({ page }) => { const DAY = '2024-01-01';
const DAY_AFTER = '2024-01-02';
const ONE_O_CLOCK = '01:00:00';
const TWO_O_CLOCK = '02:00:00';
test.beforeEach(async ({ page }) => {
// Go to baseURL // Go to baseURL
await page.goto('./', { waitUntil: 'domcontentloaded' }); await page.goto('./', { waitUntil: 'domcontentloaded' });
const year = new Date().getFullYear(); });
// Set initial valid time bounds test('validate date and time inputs are validated on input event', async ({ page }) => {
const startDate = `${year}-01-01`; const submitButtonLocator = page.getByLabel('Submit time bounds');
const startTime = '01:00:00';
const endDate = `${year}-01-01`;
const endTime = '02:00:00';
await setTimeConductorBounds(page, { startDate, startTime, endDate, endTime });
// Open the time conductor popup // Open the time conductor popup
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click(); await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
// Test invalid start date await test.step('invalid start date disables submit button', async () => {
const invalidStartDate = `${year}-01-02`; const initialStartDate = await page.getByLabel('Start date').inputValue();
const invalidStartDate = `${initialStartDate.substring(0, 5)}${initialStartDate.substring(6)}`;
await page.getByLabel('Start date').fill(invalidStartDate); await page.getByLabel('Start date').fill(invalidStartDate);
await expect(page.getByLabel('Submit time bounds')).toBeDisabled(); await expect(submitButtonLocator).toBeDisabled();
await page.getByLabel('Start date').fill(startDate); await page.getByLabel('Start date').fill(initialStartDate);
await expect(page.getByLabel('Submit time bounds')).toBeEnabled(); await expect(submitButtonLocator).toBeEnabled();
});
// Test invalid end date await test.step('invalid start time disables submit button', async () => {
const invalidEndDate = `${year - 1}-12-31`; const initialStartTime = await page.getByLabel('Start time').inputValue();
await page.getByLabel('End date').fill(invalidEndDate); const invalidStartTime = `${initialStartTime.substring(0, 5)}${initialStartTime.substring(6)}`;
await expect(page.getByLabel('Submit time bounds')).toBeDisabled();
await page.getByLabel('End date').fill(endDate);
await expect(page.getByLabel('Submit time bounds')).toBeEnabled();
// Test invalid start time
const invalidStartTime = '42:00:00';
await page.getByLabel('Start time').fill(invalidStartTime); await page.getByLabel('Start time').fill(invalidStartTime);
await expect(page.getByLabel('Submit time bounds')).toBeDisabled(); await expect(submitButtonLocator).toBeDisabled();
await page.getByLabel('Start time').fill(startTime); await page.getByLabel('Start time').fill(initialStartTime);
await expect(page.getByLabel('Submit time bounds')).toBeEnabled(); await expect(submitButtonLocator).toBeEnabled();
});
await test.step('disable/enable submit button also works with multiple invalid inputs', async () => {
const initialEndDate = await page.getByLabel('End date').inputValue();
const invalidEndDate = `${initialEndDate.substring(0, 5)}${initialEndDate.substring(6)}`;
const initialStartTime = await page.getByLabel('Start time').inputValue();
const invalidStartTime = `${initialStartTime.substring(0, 5)}${initialStartTime.substring(6)}`;
await page.getByLabel('Start time').fill(invalidStartTime);
await expect(submitButtonLocator).toBeDisabled();
await page.getByLabel('End date').fill(invalidEndDate);
await expect(submitButtonLocator).toBeDisabled();
await page.getByLabel('End date').fill(initialEndDate);
await expect(submitButtonLocator).toBeDisabled();
await page.getByLabel('Start time').fill(initialStartTime);
await expect(submitButtonLocator).toBeEnabled();
});
});
test('validate date and time inputs validation is reported on change event', async ({ page }) => {
// Open the time conductor popup
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
await test.step('invalid start date is reported on change event, not on input event', async () => {
const initialStartDate = await page.getByLabel('Start date').inputValue();
const invalidStartDate = `${initialStartDate.substring(0, 5)}${initialStartDate.substring(6)}`;
await page.getByLabel('Start date').fill(invalidStartDate);
await expect(page.getByLabel('Start date')).not.toHaveAttribute('title', 'Invalid Date');
await page.getByLabel('Start date').press('Tab');
await expect(page.getByLabel('Start date')).toHaveAttribute('title', 'Invalid Date');
await page.getByLabel('Start date').fill(initialStartDate);
await expect(page.getByLabel('Start date')).not.toHaveAttribute('title', 'Invalid Date');
});
await test.step('invalid start time is reported on change event, not on input event', async () => {
const initialStartTime = await page.getByLabel('Start time').inputValue();
const invalidStartTime = `${initialStartTime.substring(0, 5)}${initialStartTime.substring(6)}`;
await page.getByLabel('Start time').fill(invalidStartTime);
await expect(page.getByLabel('Start time')).not.toHaveAttribute('title', 'Invalid Time');
await page.getByLabel('Start time').press('Tab');
await expect(page.getByLabel('Start time')).toHaveAttribute('title', 'Invalid Time');
await page.getByLabel('Start time').fill(initialStartTime);
await expect(page.getByLabel('Start time')).not.toHaveAttribute('title', 'Invalid Time');
});
await test.step('invalid end date is reported on change event, not on input event', async () => {
const initialEndDate = await page.getByLabel('End date').inputValue();
const invalidEndDate = `${initialEndDate.substring(0, 5)}${initialEndDate.substring(6)}`;
await page.getByLabel('End date').fill(invalidEndDate);
await expect(page.getByLabel('End date')).not.toHaveAttribute('title', 'Invalid Date');
await page.getByLabel('End date').press('Tab');
await expect(page.getByLabel('End date')).toHaveAttribute('title', 'Invalid Date');
await page.getByLabel('End date').fill(initialEndDate);
await expect(page.getByLabel('End date')).not.toHaveAttribute('title', 'Invalid Date');
});
await test.step('invalid end time is reported on change event, not on input event', async () => {
const initialEndTime = await page.getByLabel('End time').inputValue();
const invalidEndTime = `${initialEndTime.substring(0, 5)}${initialEndTime.substring(6)}`;
// Test invalid end time
const invalidEndTime = '43:00:00';
await page.getByLabel('End time').fill(invalidEndTime); await page.getByLabel('End time').fill(invalidEndTime);
await expect(page.getByLabel('Submit time bounds')).toBeDisabled(); await expect(page.getByLabel('End time')).not.toHaveAttribute('title', 'Invalid Time');
await page.getByLabel('End time').fill(endTime); await page.getByLabel('End time').press('Tab');
await expect(page.getByLabel('Submit time bounds')).toBeEnabled(); await expect(page.getByLabel('End time')).toHaveAttribute('title', 'Invalid Time');
await page.getByLabel('End time').fill(initialEndTime);
await expect(page.getByLabel('End time')).not.toHaveAttribute('title', 'Invalid Time');
});
});
// Submit valid time bounds test('validate start time does not exceed end time on submit', async ({ page }) => {
// Open the time conductor popup
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
// FIXME: https://github.com/nasa/openmct/pull/7818
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(500);
await page.getByLabel('Start date').fill(DAY);
await page.getByLabel('Start time').fill(TWO_O_CLOCK);
await page.getByLabel('End date').fill(DAY);
await page.getByLabel('End time').fill(ONE_O_CLOCK);
await page.getByLabel('Submit time bounds').click(); await page.getByLabel('Submit time bounds').click();
// Verify the submitted time bounds await expect(page.getByLabel('Start date')).toHaveAttribute(
await expect(page.getByLabel('Start bounds')).toHaveText( 'title',
new RegExp(`${startDate} ${startTime}.000Z`) 'Specified start date exceeds end bound'
); );
await expect(page.getByLabel('End bounds')).toHaveText( await expect(page.getByLabel('Start bounds')).not.toHaveText(`${DAY} ${TWO_O_CLOCK}.000Z`);
new RegExp(`${endDate} ${endTime}.000Z`) await expect(page.getByLabel('End bounds')).not.toHaveText(`${DAY} ${ONE_O_CLOCK}.000Z`);
await page.getByLabel('Start date').fill(DAY);
await page.getByLabel('Start time').fill(ONE_O_CLOCK);
await page.getByLabel('End date').fill(DAY);
await page.getByLabel('End time').fill(TWO_O_CLOCK);
await page.getByLabel('Submit time bounds').click();
await expect(page.getByLabel('Start bounds')).toHaveText(`${DAY} ${ONE_O_CLOCK}.000Z`);
await expect(page.getByLabel('End bounds')).toHaveText(`${DAY} ${TWO_O_CLOCK}.000Z`);
});
test('validate start datetime does not exceed end datetime on submit', async ({ page }) => {
// Open the time conductor popup
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
// FIXME: https://github.com/nasa/openmct/pull/7818
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(500);
await page.getByLabel('Start date').fill(DAY_AFTER);
await page.getByLabel('Start time').fill(ONE_O_CLOCK);
await page.getByLabel('End date').fill(DAY);
await page.getByLabel('End time').fill(ONE_O_CLOCK);
await page.getByLabel('Submit time bounds').click();
await expect(page.getByLabel('Start date')).toHaveAttribute(
'title',
'Specified start date exceeds end bound'
); );
await expect(page.getByLabel('Start bounds')).not.toHaveText(
`${DAY_AFTER} ${ONE_O_CLOCK}.000Z`
);
await expect(page.getByLabel('End bounds')).not.toHaveText(`${DAY} ${ONE_O_CLOCK}.000Z`);
await page.getByLabel('Start date').fill(DAY);
await page.getByLabel('Start time').fill(ONE_O_CLOCK);
await page.getByLabel('End date').fill(DAY_AFTER);
await page.getByLabel('End time').fill(ONE_O_CLOCK);
await page.getByLabel('Submit time bounds').click();
await expect(page.getByLabel('Start bounds')).toHaveText(`${DAY} ${ONE_O_CLOCK}.000Z`);
await expect(page.getByLabel('End bounds')).toHaveText(`${DAY_AFTER} ${ONE_O_CLOCK}.000Z`);
});
test('cancelling form does not set bounds', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7791'
});
// Open the time conductor popup
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
await page.getByLabel('Start date').fill(DAY);
await page.getByLabel('Start time').fill(ONE_O_CLOCK);
await page.getByLabel('End date').fill(DAY_AFTER);
await page.getByLabel('End time').fill(ONE_O_CLOCK);
await page.getByLabel('Discard changes and close time popup').click();
await expect(page.getByLabel('Start bounds')).not.toHaveText(`${DAY} ${ONE_O_CLOCK}.000Z`);
await expect(page.getByLabel('End bounds')).not.toHaveText(`${DAY_AFTER} ${ONE_O_CLOCK}.000Z`);
// Open the time conductor popup
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
await page.getByLabel('Start date').fill(DAY);
await page.getByLabel('Start time').fill(ONE_O_CLOCK);
await page.getByLabel('End date').fill(DAY_AFTER);
await page.getByLabel('End time').fill(ONE_O_CLOCK);
await page.getByLabel('Submit time bounds').click();
await expect(page.getByLabel('Start bounds')).toHaveText(`${DAY} ${ONE_O_CLOCK}.000Z`);
await expect(page.getByLabel('End bounds')).toHaveText(`${DAY_AFTER} ${ONE_O_CLOCK}.000Z`);
}); });
}); });
@ -131,77 +276,6 @@ test.describe('Global Time Conductor', () => {
await expect(page.getByLabel('End offset: 01:30:31')).toBeVisible(); await expect(page.getByLabel('End offset: 01:30:31')).toBeVisible();
}); });
test('Input field validation: fixed time mode', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7791'
});
// Switch to fixed time mode
await setFixedTimeMode(page);
// Define valid time bounds for testing
const validBounds = {
startDate: '2024-04-20',
startTime: '00:04:20',
endDate: '2024-04-20',
endTime: '16:04:20'
};
// Set valid time conductor bounds ✌️
await setTimeConductorBounds(page, validBounds);
// Verify that the time bounds are set correctly
await expect(page.getByLabel(`Start bounds: 2024-04-20 00:04:20.000Z`)).toBeVisible();
await expect(page.getByLabel(`End bounds: 2024-04-20 16:04:20.000Z`)).toBeVisible();
// Open the Time Conductor Mode popup
await page.getByLabel('Time Conductor Mode').click();
// Test invalid start date
const invalidStartDate = '2024-04-21';
await page.getByLabel('Start date').fill(invalidStartDate);
await expect(page.getByLabel('Submit time bounds')).toBeDisabled();
await page.getByLabel('Start date').fill(validBounds.startDate);
await expect(page.getByLabel('Submit time bounds')).toBeEnabled();
// Test invalid end date
const invalidEndDate = '2024-04-19';
await page.getByLabel('End date').fill(invalidEndDate);
await expect(page.getByLabel('Submit time bounds')).toBeDisabled();
await page.getByLabel('End date').fill(validBounds.endDate);
await expect(page.getByLabel('Submit time bounds')).toBeEnabled();
// Test invalid start time
const invalidStartTime = '16:04:21';
await page.getByLabel('Start time').fill(invalidStartTime);
await expect(page.getByLabel('Submit time bounds')).toBeDisabled();
await page.getByLabel('Start time').fill(validBounds.startTime);
await expect(page.getByLabel('Submit time bounds')).toBeEnabled();
// Test invalid end time
const invalidEndTime = '00:04:19';
await page.getByLabel('End time').fill(invalidEndTime);
await expect(page.getByLabel('Submit time bounds')).toBeDisabled();
await page.getByLabel('End time').fill(validBounds.endTime);
await expect(page.getByLabel('Submit time bounds')).toBeEnabled();
// Verify that the time bounds remain unchanged after invalid inputs
await expect(page.getByLabel(`Start bounds: 2024-04-20 00:04:20.000Z`)).toBeVisible();
await expect(page.getByLabel(`End bounds: 2024-04-20 16:04:20.000Z`)).toBeVisible();
// Discard changes and verify that bounds remain unchanged
await setTimeConductorBounds(page, {
startDate: validBounds.startDate,
startTime: '04:20:00',
endDate: validBounds.endDate,
endTime: '04:20:20',
submitChanges: false
});
// Verify that the original time bounds are still displayed after discarding changes
await expect(page.getByLabel(`Start bounds: 2024-04-20 00:04:20.000Z`)).toBeVisible();
await expect(page.getByLabel(`End bounds: 2024-04-20 16:04:20.000Z`)).toBeVisible();
});
/** /**
* Verify that offsets and url params are preserved when switching * Verify that offsets and url params are preserved when switching
* between fixed timespan and real-time mode. * between fixed timespan and real-time mode.

View File

@ -26,14 +26,25 @@ import fs from 'fs';
import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../appActions.js'; import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../appActions.js';
import { scanForA11yViolations, test } from '../../avpFixtures.js'; import { scanForA11yViolations, test } from '../../avpFixtures.js';
import { VISUAL_FIXED_URL } from '../../constants.js'; import { VISUAL_FIXED_URL } from '../../constants.js';
import { setBoundsToSpanAllActivities, setDraftStatusForPlan } from '../../helper/planningUtils.js'; import {
getFirstActivity,
setBoundsToSpanAllActivities,
setDraftStatusForPlan
} from '../../helper/planningUtils.js';
const examplePlanSmall2 = JSON.parse( const examplePlanSmall2 = JSON.parse(
fs.readFileSync(new URL('../../test-data/examplePlans/ExamplePlan_Small2.json', import.meta.url)) fs.readFileSync(new URL('../../test-data/examplePlans/ExamplePlan_Small2.json', import.meta.url))
); );
const FIRST_ACTIVITY_SMALL_2 = getFirstActivity(examplePlanSmall2);
test.describe('Visual - Gantt Chart @a11y', () => { test.describe('Visual - Gantt Chart @a11y', () => {
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
// Set the clock to the end of the first activity in the plan
// This is so we can see the "now" line in the plan view
await page.clock.install({ time: FIRST_ACTIVITY_SMALL_2.end + 10000 });
await page.clock.resume();
await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' }); await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });
}); });
test('Gantt Chart View', async ({ page, theme }) => { test('Gantt Chart View', async ({ page, theme }) => {

View File

@ -27,14 +27,21 @@ import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../appAct
import { scanForA11yViolations, test } from '../../avpFixtures.js'; import { scanForA11yViolations, test } from '../../avpFixtures.js';
import { waitForAnimations } from '../../baseFixtures.js'; import { waitForAnimations } from '../../baseFixtures.js';
import { VISUAL_FIXED_URL } from '../../constants.js'; import { VISUAL_FIXED_URL } from '../../constants.js';
import { setBoundsToSpanAllActivities } from '../../helper/planningUtils.js'; import { getFirstActivity, setBoundsToSpanAllActivities } from '../../helper/planningUtils.js';
const examplePlanSmall2 = JSON.parse( const examplePlanSmall2 = JSON.parse(
fs.readFileSync(new URL('../../test-data/examplePlans/ExamplePlan_Small2.json', import.meta.url)) fs.readFileSync(new URL('../../test-data/examplePlans/ExamplePlan_Small2.json', import.meta.url))
); );
const FIRST_ACTIVITY_SMALL_2 = getFirstActivity(examplePlanSmall2);
test.describe('Visual - Time Strip @a11y', () => { test.describe('Visual - Time Strip @a11y', () => {
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
// Set the clock to the end of the first activity in the plan
// This is so we can see the "now" line in the plan view
await page.clock.install({ time: FIRST_ACTIVITY_SMALL_2.end + 10000 });
await page.clock.resume();
await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' }); await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });
}); });
test('Time Strip View', async ({ page, theme }) => { test('Time Strip View', async ({ page, theme }) => {

View File

@ -42,6 +42,7 @@ const examplePlanSmall2 = JSON.parse(
); );
const FIRST_ACTIVITY_SMALL_1 = getFirstActivity(examplePlanSmall1); const FIRST_ACTIVITY_SMALL_1 = getFirstActivity(examplePlanSmall1);
const FIRST_ACTIVITY_SMALL_2 = getFirstActivity(examplePlanSmall2);
test.describe('Visual - Timelist progress bar @clock @a11y', () => { test.describe('Visual - Timelist progress bar @clock @a11y', () => {
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
@ -59,6 +60,11 @@ test.describe('Visual - Timelist progress bar @clock @a11y', () => {
test.describe('Visual - Plan View @a11y', () => { test.describe('Visual - Plan View @a11y', () => {
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
// Set the clock to the end of the first activity in the plan
// This is so we can see the "now" line in the plan view
await page.clock.install({ time: FIRST_ACTIVITY_SMALL_2.end + 10000 });
await page.clock.resume();
await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' }); await page.goto(VISUAL_FIXED_URL, { waitUntil: 'domcontentloaded' });
}); });

26
package-lock.json generated
View File

@ -94,7 +94,7 @@
"webpack-merge": "5.10.0" "webpack-merge": "5.10.0"
}, },
"engines": { "engines": {
"node": ">=18.14.2 <22" "node": ">=18.14.2 <23"
} }
}, },
"e2e": { "e2e": {
@ -105,7 +105,7 @@
"@axe-core/playwright": "4.8.5", "@axe-core/playwright": "4.8.5",
"@percy/cli": "1.27.4", "@percy/cli": "1.27.4",
"@percy/playwright": "1.0.4", "@percy/playwright": "1.0.4",
"@playwright/test": "1.47.2" "@playwright/test": "1.48.1"
} }
}, },
"e2e/node_modules/@percy/cli": { "e2e/node_modules/@percy/cli": {
@ -1561,13 +1561,13 @@
} }
}, },
"node_modules/@playwright/test": { "node_modules/@playwright/test": {
"version": "1.47.2", "version": "1.48.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.47.2.tgz", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.1.tgz",
"integrity": "sha512-jTXRsoSPONAs8Za9QEQdyjFn+0ZQFjCiIztAIF6bi1HqhBzG9Ma7g1WotyiGqFSBRZjIEqMdT8RUlbk1QVhzCQ==", "integrity": "sha512-s9RtWoxkOLmRJdw3oFvhFbs9OJS0BzrLUc8Hf6l2UdCNd1rqeEyD4BhCJkvzeEoD1FsK4mirsWwGerhVmYKtZg==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright": "1.47.2" "playwright": "1.48.1"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -8830,13 +8830,13 @@
} }
}, },
"node_modules/playwright": { "node_modules/playwright": {
"version": "1.47.2", "version": "1.48.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.47.2.tgz", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.1.tgz",
"integrity": "sha512-nx1cLMmQWqmA3UsnjaaokyoUpdVaaDhJhMoxX2qj3McpjnsqFHs516QAKYhqHAgOP+oCFTEOCOAaD1RgD/RQfA==", "integrity": "sha512-j8CiHW/V6HxmbntOfyB4+T/uk08tBy6ph0MpBXwuoofkSnLmlfdYNNkFTYD6ofzzlSqLA1fwH4vwvVFvJgLN0w==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.47.2" "playwright-core": "1.48.1"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -8849,9 +8849,9 @@
} }
}, },
"node_modules/playwright-core": { "node_modules/playwright-core": {
"version": "1.47.2", "version": "1.48.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.47.2.tgz", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.1.tgz",
"integrity": "sha512-3JvMfF+9LJfe16l7AbSmU555PaTl2tPyQsVInqm3id16pdDfvZ8TTZ/pyzmkbDrZTQefyzU7AIHlZqQnxpqHVQ==", "integrity": "sha512-Yw/t4VAFX/bBr1OzwCuOMZkY1Cnb4z/doAFSwf4huqAGWmf9eMNjmK7NiOljCdLmxeRYcGPPmcDgU0zOlzP0YA==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {

View File

@ -140,7 +140,7 @@
"url": "git+https://github.com/nasa/openmct.git" "url": "git+https://github.com/nasa/openmct.git"
}, },
"engines": { "engines": {
"node": ">=18.14.2 <22" "node": ">=18.14.2 <23"
}, },
"browserslist": [ "browserslist": [
"Firefox ESR", "Firefox ESR",

View File

@ -35,7 +35,7 @@ export const DEFAULT_SHELVE_DURATIONS = [
value: 900000 value: 900000
}, },
{ {
name: 'Indefinite', name: 'Unlimited',
value: null value: null
} }
]; ];
@ -136,17 +136,21 @@ export default class FaultManagementAPI {
/** /**
* Retrieves the available shelve durations from the provider, or the default durations if the * Retrieves the available shelve durations from the provider, or the default durations if the
* provider does not provide any. * provider does not provide any.
* @returns {ShelveDuration[]} * @returns {ShelveDuration[] | undefined}
*/ */
getShelveDurations() { getShelveDurations() {
return this.provider?.getShelveDurations() ?? DEFAULT_SHELVE_DURATIONS; if (!this.provider) {
return;
}
return this.provider.getShelveDurations?.() ?? DEFAULT_SHELVE_DURATIONS;
} }
} }
/** /**
* @typedef {Object} ShelveDuration * @typedef {Object} ShelveDuration
* @property {string} name - The name of the shelve duration * @property {string} name - The name of the shelve duration
* @property {number|null} value - The value of the shelve duration in milliseconds, or null for indefinite * @property {number|null} value - The value of the shelve duration in milliseconds, or null for unlimited
*/ */
/** /**

View File

@ -332,7 +332,11 @@ export default {
this.domainObject.configuration.axes.xKey === undefined || this.domainObject.configuration.axes.xKey === undefined ||
this.domainObject.configuration.axes.yKey === undefined this.domainObject.configuration.axes.yKey === undefined
) { ) {
return; const { xKey, yKey } = this.identifyAxesKeys(axisMetadata);
this.openmct.objects.mutate(this.domainObject, 'configuration.axes', {
xKey,
yKey
});
} }
let xValues = []; let xValues = [];
@ -431,6 +435,30 @@ export default {
subscribeToAll() { subscribeToAll() {
const telemetryObjects = Object.values(this.telemetryObjects); const telemetryObjects = Object.values(this.telemetryObjects);
telemetryObjects.forEach(this.subscribeToObject); telemetryObjects.forEach(this.subscribeToObject);
},
identifyAxesKeys(metadata) {
const { xAxisMetadata, yAxisMetadata } = metadata;
let xKey;
let yKey;
// If xAxisMetadata contains array values, use the first one for xKey
const arrayValues = xAxisMetadata.filter((metaDatum) => metaDatum.isArrayValue);
const nonArrayValues = xAxisMetadata.filter((metaDatum) => !metaDatum.isArrayValue);
if (arrayValues.length > 0) {
xKey = arrayValues[0].key;
yKey = arrayValues.length > 1 ? arrayValues[1].key : yAxisMetadata.key;
} else if (nonArrayValues.length > 0) {
xKey = nonArrayValues[0].key;
yKey = 'none';
} else {
// Fallback if no valid xKey or yKey is found
xKey = 'none';
yKey = 'none';
}
return { xKey, yKey };
} }
} }
}; };

View File

@ -28,11 +28,7 @@
{ 'is-style-invisible': styleItem.style && styleItem.style.isStyleInvisible }, { 'is-style-invisible': styleItem.style && styleItem.style.isStyleInvisible },
{ 'c-style-thumb--mixed': mixedStyles.indexOf('backgroundColor') > -1 } { 'c-style-thumb--mixed': mixedStyles.indexOf('backgroundColor') > -1 }
]" ]"
:style="[ :style="[encodedImageUrl ? { backgroundImage: 'url(' + encodedImageUrl + ')' } : itemStyle]"
styleItem.style.imageUrl
? { backgroundImage: 'url(' + styleItem.style.imageUrl + ')' }
: itemStyle
]"
class="c-style-thumb" class="c-style-thumb"
> >
<span <span
@ -62,7 +58,7 @@
@change="updateStyleValue" @change="updateStyleValue"
/> />
<ToolbarButton <ToolbarButton
v-if="hasProperty(styleItem.style.imageUrl)" v-if="hasProperty(encodedImageUrl)"
class="c-style__toolbar-button--image-url" class="c-style__toolbar-button--image-url"
:options="imageUrlOption" :options="imageUrlOption"
@change="updateStyleValue" @change="updateStyleValue"
@ -93,6 +89,8 @@ import ToolbarButton from '@/ui/toolbar/components/ToolbarButton.vue';
import ToolbarColorPicker from '@/ui/toolbar/components/ToolbarColorPicker.vue'; import ToolbarColorPicker from '@/ui/toolbar/components/ToolbarColorPicker.vue';
import ToolbarToggleButton from '@/ui/toolbar/components/ToolbarToggleButton.vue'; import ToolbarToggleButton from '@/ui/toolbar/components/ToolbarToggleButton.vue';
import { encode_url } from '../../../../utils/encoding';
export default { export default {
name: 'StyleEditor', name: 'StyleEditor',
components: { components: {
@ -183,11 +181,14 @@ export default {
}, },
property: 'imageUrl', property: 'imageUrl',
formKeys: ['url'], formKeys: ['url'],
value: { url: this.styleItem.style.imageUrl }, value: { url: this.encodedImageUrl },
isEditing: this.isEditing, isEditing: this.isEditing,
nonSpecific: this.mixedStyles.indexOf('imageUrl') > -1 nonSpecific: this.mixedStyles.indexOf('imageUrl') > -1
}; };
}, },
encodedImageUrl() {
return encode_url(this.styleItem.style.imageUrl);
},
isStyleInvisibleOption() { isStyleInvisibleOption() {
return { return {
value: this.styleItem.style.isStyleInvisible, value: this.styleItem.style.isStyleInvisible,

View File

@ -35,6 +35,7 @@
</template> </template>
<script> <script>
import { encode_url } from '../../../utils/encoding';
import conditionalStylesMixin from '../mixins/objectStyles-mixin.js'; import conditionalStylesMixin from '../mixins/objectStyles-mixin.js';
import LayoutFrame from './LayoutFrame.vue'; import LayoutFrame from './LayoutFrame.vue';
@ -80,12 +81,12 @@ export default {
return this.isEditing || !this.itemStyle?.isStyleInvisible; return this.isEditing || !this.itemStyle?.isStyleInvisible;
}, },
style() { style() {
let backgroundImage = 'url(' + this.item.url + ')'; let backgroundImage = `url('${encode_url(this.item.url)}')`;
let border = '1px solid ' + this.item.stroke; let border = '1px solid ' + this.item.stroke;
if (this.itemStyle) { if (this.itemStyle) {
if (this.itemStyle.imageUrl !== undefined) { if (this.itemStyle.imageUrl !== undefined) {
backgroundImage = 'url(' + this.itemStyle.imageUrl + ')'; backgroundImage = `url('${encode_url(this.itemStyle.imageUrl)}')`;
} }
border = this.itemStyle.border; border = this.itemStyle.border;

View File

@ -330,7 +330,8 @@ export default {
} }
shelveData.comment = data.comment || ''; shelveData.comment = data.comment || '';
shelveData.shelveDuration = data.shelveDuration ?? this.shelveDurations[0].value; shelveData.shelveDuration =
data.shelveDuration === undefined ? this.shelveDurations[0].value : data.shelveDuration;
} else { } else {
shelveData = { shelveData = {
shelved: false shelved: false

View File

@ -42,24 +42,6 @@ export const FAULT_MANAGEMENT_TYPE = 'faultManagement';
export const FAULT_MANAGEMENT_INSPECTOR = 'faultManagementInspector'; export const FAULT_MANAGEMENT_INSPECTOR = 'faultManagementInspector';
export const FAULT_MANAGEMENT_ALARMS = 'alarms'; export const FAULT_MANAGEMENT_ALARMS = 'alarms';
export const FAULT_MANAGEMENT_GLOBAL_ALARMS = 'global-alarm-status'; export const FAULT_MANAGEMENT_GLOBAL_ALARMS = 'global-alarm-status';
export const FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS = [
{
name: '5 Minutes',
value: 300000
},
{
name: '10 Minutes',
value: 600000
},
{
name: '15 Minutes',
value: 900000
},
{
name: 'Indefinite',
value: 0
}
];
export const FAULT_MANAGEMENT_VIEW = 'faultManagement.view'; export const FAULT_MANAGEMENT_VIEW = 'faultManagement.view';
export const FAULT_MANAGEMENT_NAMESPACE = 'faults.taxonomy'; export const FAULT_MANAGEMENT_NAMESPACE = 'faults.taxonomy';
export const FILTER_ITEMS = ['Standard View', 'Acknowledged', 'Unacknowledged', 'Shelved']; export const FILTER_ITEMS = ['Standard View', 'Acknowledged', 'Unacknowledged', 'Shelved'];

View File

@ -38,7 +38,7 @@
<img <img
ref="img" ref="img"
class="c-thumb__image" class="c-thumb__image"
:src="`${image.thumbnailUrl || image.url}`" :src="imageSrc"
fetchpriority="low" fetchpriority="low"
@load="imageLoadCompleted" @load="imageLoadCompleted"
/> />
@ -54,6 +54,8 @@
</template> </template>
<script> <script>
import { encode_url } from '../../../utils/encoding';
const THUMB_PADDING = 4; const THUMB_PADDING = 4;
const BORDER_WIDTH = 2; const BORDER_WIDTH = 2;
@ -96,6 +98,9 @@ export default {
}; };
}, },
computed: { computed: {
imageSrc() {
return `${encode_url(this.image.thumbnailUrl) || encode_url(this.image.url)}`;
},
ariaLabel() { ariaLabel() {
return `Image thumbnail from ${this.image.formattedTime}${this.showAnnotationIndicator ? ', has annotations' : ''}`; return `Image thumbnail from ${this.image.formattedTime}${this.showAnnotationIndicator ? ', has annotations' : ''}`;
}, },

View File

@ -222,6 +222,7 @@ import { TIME_CONTEXT_EVENTS } from '@/api/time/constants.js';
import imageryData from '@/plugins/imagery/mixins/imageryData.js'; import imageryData from '@/plugins/imagery/mixins/imageryData.js';
import { VIEW_LARGE_ACTION_KEY } from '@/plugins/viewLargeAction/viewLargeAction.js'; import { VIEW_LARGE_ACTION_KEY } from '@/plugins/viewLargeAction/viewLargeAction.js';
import { encode_url } from '../../../utils/encoding';
import eventHelpers from '../lib/eventHelpers.js'; import eventHelpers from '../lib/eventHelpers.js';
import AnnotationsCanvas from './AnnotationsCanvas.vue'; import AnnotationsCanvas from './AnnotationsCanvas.vue';
import Compass from './Compass/CompassComponent.vue'; import Compass from './Compass/CompassComponent.vue';
@ -364,7 +365,7 @@ export default {
filter: `brightness(${this.filters.brightness}%) contrast(${this.filters.contrast}%)`, filter: `brightness(${this.filters.brightness}%) contrast(${this.filters.contrast}%)`,
backgroundImage: `${ backgroundImage: `${
this.imageUrl this.imageUrl
? `url(${this.imageUrl}), ? `url(${encode_url(this.imageUrl)}),
repeating-linear-gradient( repeating-linear-gradient(
45deg, 45deg,
transparent, transparent,
@ -789,7 +790,7 @@ export default {
}, },
getVisibleLayerStyles(layer) { getVisibleLayerStyles(layer) {
return { return {
backgroundImage: `url(${layer.source})`, backgroundImage: `url(${encode_url(layer.source)})`,
transform: `scale(${this.zoomFactor}) translate(${this.imageTranslateX / 2}px, ${ transform: `scale(${this.zoomFactor}) translate(${this.imageTranslateX / 2}px, ${
this.imageTranslateY / 2 this.imageTranslateY / 2
}px)`, }px)`,

View File

@ -243,11 +243,12 @@ export default {
if (this.planObject) { if (this.planObject) {
this.showReplacePlanDialog(domainObject); this.showReplacePlanDialog(domainObject);
} else { } else {
this.swimlaneVisibility = this.configuration.swimlaneVisibility;
this.setupPlan(domainObject); this.setupPlan(domainObject);
this.swimlaneVisibility = this.configuration.swimlaneVisibility;
} }
}, },
handleConfigurationChange(newConfiguration) { handleConfigurationChange(newConfiguration) {
this.configuration = this.planViewConfiguration.getConfiguration();
Object.keys(newConfiguration).forEach((key) => { Object.keys(newConfiguration).forEach((key) => {
this[key] = newConfiguration[key]; this[key] = newConfiguration[key];
}); });
@ -423,7 +424,10 @@ export default {
return currentRow || SWIMLANE_PADDING; return currentRow || SWIMLANE_PADDING;
}, },
generateActivities() { generateActivities() {
const groupNames = getValidatedGroups(this.domainObject, this.planData); if (!this.planObject) {
return;
}
const groupNames = getValidatedGroups(this.planObject, this.planData);
if (!groupNames.length) { if (!groupNames.length) {
return; return;

View File

@ -11,18 +11,19 @@
> >
<input <input
ref="startDate" ref="startDate"
v-model="formattedBounds.start" v-model="formattedBounds.startDate"
class="c-input--datetime" class="c-input--datetime"
type="text" type="text"
autocorrect="off" autocorrect="off"
spellcheck="false" spellcheck="false"
aria-label="Start date" aria-label="Start date"
@input="validateAllBounds('startDate')" @input="validateInput('startDate')"
@change="reportValidity('startDate')"
/> />
<DatePicker <DatePicker
v-if="isUTCBased" v-if="isUTCBased"
class="c-ctrl-wrapper--menus-right" class="c-ctrl-wrapper--menus-right"
:default-date-time="formattedBounds.start" :default-date-time="formattedBounds.startDate"
:formatter="timeFormatter" :formatter="timeFormatter"
@date-selected="startDateSelected" @date-selected="startDateSelected"
/> />
@ -37,7 +38,8 @@
autocorrect="off" autocorrect="off"
spellcheck="false" spellcheck="false"
aria-label="Start time" aria-label="Start time"
@input="validateAllBounds('startDate')" @input="validateInput('startTime')"
@change="reportValidity('startTime')"
/> />
</div> </div>
@ -48,18 +50,19 @@
> >
<input <input
ref="endDate" ref="endDate"
v-model="formattedBounds.end" v-model="formattedBounds.endDate"
class="c-input--datetime" class="c-input--datetime"
type="text" type="text"
autocorrect="off" autocorrect="off"
spellcheck="false" spellcheck="false"
aria-label="End date" aria-label="End date"
@input="validateAllBounds('endDate')" @input="validateInput('endDate')"
@change="reportValidity('endDate')"
/> />
<DatePicker <DatePicker
v-if="isUTCBased" v-if="isUTCBased"
class="c-ctrl-wrapper--menus-left" class="c-ctrl-wrapper--menus-left"
:default-date-time="formattedBounds.end" :default-date-time="formattedBounds.endDate"
:formatter="timeFormatter" :formatter="timeFormatter"
@date-selected="endDateSelected" @date-selected="endDateSelected"
/> />
@ -74,14 +77,15 @@
autocorrect="off" autocorrect="off"
spellcheck="false" spellcheck="false"
aria-label="End time" aria-label="End time"
@input="validateAllBounds('endDate')" @input="validateInput('endTime')"
@change="reportValidity('endTime')"
/> />
</div> </div>
<div class="pr-time-input pr-time-input--buttons"> <div class="pr-time-input pr-time-input--buttons">
<button <button
class="c-button c-button--major icon-check" class="c-button c-button--major icon-check"
:disabled="isDisabled" :disabled="hasInputValidityError"
aria-label="Submit time bounds" aria-label="Submit time bounds"
@click.prevent="handleFormSubmission(true)" @click.prevent="handleFormSubmission(true)"
></button> ></button>
@ -125,6 +129,7 @@ export default {
return { return {
timeFormatter: this.getFormatter(timeSystem.timeFormat), timeFormatter: this.getFormatter(timeSystem.timeFormat),
durationFormatter: this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER), durationFormatter: this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER),
timeSystemKey: timeSystem.key,
bounds: { bounds: {
start: bounds.start, start: bounds.start,
end: bounds.end end: bounds.end
@ -136,9 +141,29 @@ export default {
endTime: '' endTime: ''
}, },
isUTCBased: timeSystem.isUTCBased, isUTCBased: timeSystem.isUTCBased,
isDisabled: false inputValidityMap: {
startDate: { valid: true },
startTime: { valid: true },
endDate: { valid: true },
endTime: { valid: true }
},
logicalValidityMap: {
limit: { valid: true },
bounds: { valid: true }
}
}; };
}, },
computed: {
hasInputValidityError() {
return Object.values(this.inputValidityMap).some((isValid) => !isValid.valid);
},
hasLogicalValidationErrors() {
return Object.values(this.logicalValidityMap).some((isValid) => !isValid.valid);
},
isValid() {
return !this.hasInputValidityError && !this.hasLogicalValidationErrors;
}
},
watch: { watch: {
inputBounds: { inputBounds: {
handler(newBounds) { handler(newBounds) {
@ -168,25 +193,17 @@ export default {
this.setBounds(bounds); this.setBounds(bounds);
this.setViewFromBounds(bounds); this.setViewFromBounds(bounds);
}, },
clearAllValidation() {
[this.$refs.startDate, this.$refs.endDate].forEach(this.clearValidationForInput);
},
clearValidationForInput(input) {
if (input) {
input.setCustomValidity('');
input.title = '';
}
},
setBounds(bounds) { setBounds(bounds) {
this.bounds = bounds; this.bounds = bounds;
}, },
setViewFromBounds(bounds) { setViewFromBounds(bounds) {
this.formattedBounds.start = this.timeFormatter.format(bounds.start).split(' ')[0]; this.formattedBounds.startDate = this.timeFormatter.format(bounds.start).split(' ')[0];
this.formattedBounds.end = this.timeFormatter.format(bounds.end).split(' ')[0]; this.formattedBounds.endDate = this.timeFormatter.format(bounds.end).split(' ')[0];
this.formattedBounds.startTime = this.durationFormatter.format(Math.abs(bounds.start)); this.formattedBounds.startTime = this.durationFormatter.format(Math.abs(bounds.start));
this.formattedBounds.endTime = this.durationFormatter.format(Math.abs(bounds.end)); this.formattedBounds.endTime = this.durationFormatter.format(Math.abs(bounds.end));
}, },
setTimeSystem(timeSystem) { setTimeSystem(timeSystem) {
this.timeSystemKey = timeSystem.key;
this.timeFormatter = this.getFormatter(timeSystem.timeFormat); this.timeFormatter = this.getFormatter(timeSystem.timeFormat);
this.durationFormatter = this.getFormatter( this.durationFormatter = this.getFormatter(
timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER
@ -201,10 +218,10 @@ export default {
setBoundsFromView(dismiss) { setBoundsFromView(dismiss) {
if (this.$refs.fixedDeltaInput.checkValidity()) { if (this.$refs.fixedDeltaInput.checkValidity()) {
let start = this.timeFormatter.parse( let start = this.timeFormatter.parse(
`${this.formattedBounds.start} ${this.formattedBounds.startTime}` `${this.formattedBounds.startDate} ${this.formattedBounds.startTime}`
); );
let end = this.timeFormatter.parse( let end = this.timeFormatter.parse(
`${this.formattedBounds.end} ${this.formattedBounds.endTime}` `${this.formattedBounds.endDate} ${this.formattedBounds.endTime}`
); );
this.$emit('update', { start, end }); this.$emit('update', { start, end });
@ -215,96 +232,93 @@ export default {
return false; return false;
} }
}, },
handleFormSubmission(shouldDismiss) { clearAllValidation() {
this.validateAllBounds('startDate'); Object.keys(this.inputValidityMap).forEach(this.clearValidation);
this.validateAllBounds('endDate'); },
clearValidation(refName) {
const input = this.getInput(refName);
if (!this.isDisabled) { input.setCustomValidity('');
input.title = '';
},
handleFormSubmission(shouldDismiss) {
this.validateLimit();
this.reportValidity('limit');
this.validateBounds();
this.reportValidity('bounds');
if (this.isValid) {
this.setBoundsFromView(shouldDismiss); this.setBoundsFromView(shouldDismiss);
} }
}, },
validateAllBounds(ref) { validateInput(refName) {
this.isDisabled = false; this.clearAllValidation();
if (!this.areBoundsFormatsValid()) { const inputType = refName.includes('Date') ? 'Date' : 'Time';
this.isDisabled = true; const formatter = inputType === 'Date' ? this.timeFormatter : this.durationFormatter;
return false; const validationResult = formatter.validate(this.formattedBounds[refName])
} ? { valid: true }
: { valid: false, message: `Invalid ${inputType}` };
let validationResult = { valid: true }; this.inputValidityMap[refName] = validationResult;
const currentInput = this.$refs[ref]; },
validateBounds() {
return [this.$refs.startDate, this.$refs.endDate].every((input) => { const bounds = {
let boundsValues = {
start: this.timeFormatter.parse( start: this.timeFormatter.parse(
`${this.formattedBounds.start} ${this.formattedBounds.startTime}` `${this.formattedBounds.startDate} ${this.formattedBounds.startTime}`
), ),
end: this.timeFormatter.parse( end: this.timeFormatter.parse(
`${this.formattedBounds.end} ${this.formattedBounds.endTime}` `${this.formattedBounds.endDate} ${this.formattedBounds.endTime}`
) )
}; };
//TODO: Do we need limits here? We have conductor limits disabled right now
// const limit = this.getBoundsLimit();
const limit = false;
if (this.isUTCBased && limit && boundsValues.end - boundsValues.start > limit) { this.logicalValidityMap.bounds = this.openmct.time.validateBounds(bounds);
if (input === currentInput) { },
validationResult = { validateLimit(bounds) {
const limit = this.configuration?.menuOptions
?.filter((option) => option.timeSystem === this.timeSystemKey)
?.find((option) => option.limit)?.limit;
if (this.isUTCBased && limit && bounds.end - bounds.start > limit) {
this.logicalValidityMap.limit = {
valid: false, valid: false,
message: 'Start and end difference exceeds allowable limit' message: 'Start and end difference exceeds allowable limit'
}; };
} else {
this.logicalValidityMap.limit = { valid: true };
} }
} else if (input === currentInput) {
validationResult = this.openmct.time.validateBounds(boundsValues);
}
return this.handleValidationResults(input, validationResult);
});
}, },
areBoundsFormatsValid() { reportValidity(refName) {
return [this.$refs.startDate, this.$refs.endDate].every((input) => { const input = this.getInput(refName);
const formattedDate = const validationResult = this.inputValidityMap[refName] ?? this.logicalValidityMap[refName];
input === this.$refs.startDate
? `${this.formattedBounds.start} ${this.formattedBounds.startTime}`
: `${this.formattedBounds.end} ${this.formattedBounds.endTime}`;
const validationResult = this.timeFormatter.validate(formattedDate)
? { valid: true }
: { valid: false, message: 'Invalid date' };
return this.handleValidationResults(input, validationResult);
});
},
getBoundsLimit() {
const configuration = this.configuration.menuOptions
.filter((option) => option.timeSystem === this.timeSystem.key)
.find((option) => option.limit);
const limit = configuration ? configuration.limit : undefined;
return limit;
},
handleValidationResults(input, validationResult) {
if (validationResult.valid !== true) { if (validationResult.valid !== true) {
input.setCustomValidity(validationResult.message); input.setCustomValidity(validationResult.message);
input.title = validationResult.message; input.title = validationResult.message;
this.isDisabled = true; this.hasLogicalValidationErrors = true;
} else { } else {
input.setCustomValidity(''); input.setCustomValidity('');
input.title = ''; input.title = '';
} }
this.$refs.fixedDeltaInput.reportValidity(); this.$refs.fixedDeltaInput.reportValidity();
},
getInput(refName) {
if (Object.keys(this.inputValidityMap).includes(refName)) {
return this.$refs[refName];
}
return validationResult.valid; return this.$refs.startDate;
}, },
startDateSelected(date) { startDateSelected(date) {
this.formattedBounds.start = this.timeFormatter.format(date).split(' ')[0]; this.formattedBounds.startDate = this.timeFormatter.format(date).split(' ')[0];
this.validateAllBounds('startDate'); this.validateInput('startDate');
this.reportValidity('startDate');
}, },
endDateSelected(date) { endDateSelected(date) {
this.formattedBounds.end = this.timeFormatter.format(date).split(' ')[0]; this.formattedBounds.endDate = this.timeFormatter.format(date).split(' ')[0];
this.validateAllBounds('endDate'); this.validateInput('endDate');
this.reportValidity('endDate');
}, },
hide($event) { hide($event) {
if ($event.target.className.indexOf('c-button icon-x') > -1) { if ($event.target.className.indexOf('c-button icon-x') > -1) {

View File

@ -79,6 +79,7 @@ export default {
const svgWidth = ref(0); const svgWidth = ref(0);
const svgHeight = ref(0); const svgHeight = ref(0);
const axisTransform = ref('translate(0,20)'); const axisTransform = ref('translate(0,20)');
const alignmentOffset = ref(0);
const nowMarkerStyle = reactive({ const nowMarkerStyle = reactive({
height: '0px', height: '0px',
left: '0px' left: '0px'
@ -100,6 +101,7 @@ export default {
svgWidth, svgWidth,
svgHeight, svgHeight,
axisTransform, axisTransform,
alignmentOffset,
nowMarkerStyle, nowMarkerStyle,
openmct openmct
}; };

View File

@ -32,6 +32,7 @@
<script> <script>
import mount from 'utils/mount'; import mount from 'utils/mount';
import { encode_url } from '../../utils/encoding';
import AboutDialog from './AboutDialog.vue'; import AboutDialog from './AboutDialog.vue';
export default { export default {
@ -39,7 +40,7 @@ export default {
mounted() { mounted() {
const branding = this.openmct.branding(); const branding = this.openmct.branding();
if (branding.smallLogoImage) { if (branding.smallLogoImage) {
this.$refs.aboutLogo.style.backgroundImage = `url('${branding.smallLogoImage}')`; this.$refs.aboutLogo.style.backgroundImage = `url('${encode_url(branding.smallLogoImage)}')`;
} }
}, },
methods: { methods: {

25
src/utils/encoding.js Normal file
View File

@ -0,0 +1,25 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2024, 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.
*****************************************************************************/
export function encode_url(url) {
return url ? encodeURI(url) : url;
}