From ccf7ed91afe489fe7e14b39541ef221ff6c41dc6 Mon Sep 17 00:00:00 2001 From: Jesse Mazzella Date: Tue, 15 Oct 2024 16:26:57 -0700 Subject: [PATCH 01/11] fix(vipergc-574): Use selected shelve duration for fault management (#7890) * refactor: `Indefinite` -> `Unlimited` * refactor: remove unused const * fix: use the selected shelveDuration * fix: set and use default if none selected * fix: bad optional chaining * fix: handle the case of no provider * fix: don't assign defaults if no provider --- src/api/faultmanagement/FaultManagementAPI.js | 12 ++++++++---- .../faultManagement/FaultManagementView.vue | 3 ++- src/plugins/faultManagement/constants.js | 18 ------------------ 3 files changed, 10 insertions(+), 23 deletions(-) diff --git a/src/api/faultmanagement/FaultManagementAPI.js b/src/api/faultmanagement/FaultManagementAPI.js index 2eb3673a61..652c158e94 100644 --- a/src/api/faultmanagement/FaultManagementAPI.js +++ b/src/api/faultmanagement/FaultManagementAPI.js @@ -35,7 +35,7 @@ export const DEFAULT_SHELVE_DURATIONS = [ value: 900000 }, { - name: 'Indefinite', + name: 'Unlimited', value: null } ]; @@ -136,17 +136,21 @@ export default class FaultManagementAPI { /** * Retrieves the available shelve durations from the provider, or the default durations if the * provider does not provide any. - * @returns {ShelveDuration[]} + * @returns {ShelveDuration[] | undefined} */ getShelveDurations() { - return this.provider?.getShelveDurations() ?? DEFAULT_SHELVE_DURATIONS; + if (!this.provider) { + return; + } + + return this.provider.getShelveDurations?.() ?? DEFAULT_SHELVE_DURATIONS; } } /** * @typedef {Object} ShelveDuration * @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 */ /** diff --git a/src/plugins/faultManagement/FaultManagementView.vue b/src/plugins/faultManagement/FaultManagementView.vue index eae7b03ef9..8f9f3b8cb3 100644 --- a/src/plugins/faultManagement/FaultManagementView.vue +++ b/src/plugins/faultManagement/FaultManagementView.vue @@ -330,7 +330,8 @@ export default { } shelveData.comment = data.comment || ''; - shelveData.shelveDuration = data.shelveDuration ?? this.shelveDurations[0].value; + shelveData.shelveDuration = + data.shelveDuration === undefined ? this.shelveDurations[0].value : data.shelveDuration; } else { shelveData = { shelved: false diff --git a/src/plugins/faultManagement/constants.js b/src/plugins/faultManagement/constants.js index 583c6607b5..d4fe458eb4 100644 --- a/src/plugins/faultManagement/constants.js +++ b/src/plugins/faultManagement/constants.js @@ -42,24 +42,6 @@ export const FAULT_MANAGEMENT_TYPE = 'faultManagement'; export const FAULT_MANAGEMENT_INSPECTOR = 'faultManagementInspector'; export const FAULT_MANAGEMENT_ALARMS = 'alarms'; 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_NAMESPACE = 'faults.taxonomy'; export const FILTER_ITEMS = ['Standard View', 'Acknowledged', 'Unacknowledged', 'Shelved']; From d8c5095ebb0df923fb289a50695def30a28fc510 Mon Sep 17 00:00:00 2001 From: John Hill Date: Wed, 16 Oct 2024 09:08:34 -0700 Subject: [PATCH 02/11] add environment variable check --- src/plugins/persistence/couch/scripts/lockObjects.mjs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/plugins/persistence/couch/scripts/lockObjects.mjs b/src/plugins/persistence/couch/scripts/lockObjects.mjs index f7edf69459..1a7305ca53 100644 --- a/src/plugins/persistence/couch/scripts/lockObjects.mjs +++ b/src/plugins/persistence/couch/scripts/lockObjects.mjs @@ -2,8 +2,15 @@ import http from 'http'; import nano from 'nano'; import { parseArgs } from 'util'; -const COUCH_URL = process.env.OPENMCT_COUCH_URL || 'http://127.0.0.1:5984'; -const COUCH_DB_NAME = process.env.OPENMCT_DATABASE_NAME || 'openmct'; +const COUCH_URL = process.env.OPENMCT_COUCH_URL; +const COUCH_DB_NAME = process.env.OPENMCT_DATABASE_NAME; + +if (!COUCH_URL || !COUCH_DB_NAME) { + console.error( + 'Error: Environment variables OPENMCT_COUCH_URL and OPENMCT_DATABASE_NAME must be set.' + ); + process.exit(1); +} const { values: { couchUrl, database, lock, unlock, startObjectKeystring, user, pass } From 890ddcac4ee92a62561058cb947a33e7af920886 Mon Sep 17 00:00:00 2001 From: Andrew Henry Date: Wed, 16 Oct 2024 09:57:24 -0700 Subject: [PATCH 03/11] Revert d8c5095ebb0df923fb289a50695def30a28fc510 (#7894) --- src/plugins/persistence/couch/scripts/lockObjects.mjs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/plugins/persistence/couch/scripts/lockObjects.mjs b/src/plugins/persistence/couch/scripts/lockObjects.mjs index 1a7305ca53..f7edf69459 100644 --- a/src/plugins/persistence/couch/scripts/lockObjects.mjs +++ b/src/plugins/persistence/couch/scripts/lockObjects.mjs @@ -2,15 +2,8 @@ import http from 'http'; import nano from 'nano'; import { parseArgs } from 'util'; -const COUCH_URL = process.env.OPENMCT_COUCH_URL; -const COUCH_DB_NAME = process.env.OPENMCT_DATABASE_NAME; - -if (!COUCH_URL || !COUCH_DB_NAME) { - console.error( - 'Error: Environment variables OPENMCT_COUCH_URL and OPENMCT_DATABASE_NAME must be set.' - ); - process.exit(1); -} +const COUCH_URL = process.env.OPENMCT_COUCH_URL || 'http://127.0.0.1:5984'; +const COUCH_DB_NAME = process.env.OPENMCT_DATABASE_NAME || 'openmct'; const { values: { couchUrl, database, lock, unlock, startObjectKeystring, user, pass } From 7c2bb16bfd2b63322d3ea9d22c1464ef319e3a2a Mon Sep 17 00:00:00 2001 From: David Tsay <3614296+davetsay@users.noreply.github.com> Date: Wed, 16 Oct 2024 18:57:56 -0700 Subject: [PATCH 04/11] Bugfix/7873 time conductor input validation (#7886) * validate on change because input is too aggressive * validate logical bounds on submit * perfection --- e2e/appActions.js | 4 + .../telemetryTable/telemetryTable.e2e.spec.js | 3 +- .../timeConductor/timeConductor.e2e.spec.js | 296 +++++++++++------- src/plugins/timeConductor/TimePopupFixed.vue | 192 ++++++------ 4 files changed, 294 insertions(+), 201 deletions(-) diff --git a/e2e/appActions.js b/e2e/appActions.js index 6cd901fb4d..99ee63cdb5 100644 --- a/e2e/appActions.js +++ b/e2e/appActions.js @@ -510,6 +510,10 @@ async function setTimeConductorBounds(page, { submitChanges = true, ...bounds }) // 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); + if (startDate) { await page.getByLabel('Start date').fill(startDate); } diff --git a/e2e/tests/functional/plugins/telemetryTable/telemetryTable.e2e.spec.js b/e2e/tests/functional/plugins/telemetryTable/telemetryTable.e2e.spec.js index 9aea26f395..e98e694b00 100644 --- a/e2e/tests/functional/plugins/telemetryTable/telemetryTable.e2e.spec.js +++ b/e2e/tests/functional/plugins/telemetryTable/telemetryTable.e2e.spec.js @@ -117,7 +117,8 @@ test.describe('Telemetry Table', () => { endTimeStamp.setUTCMinutes(endTimeStamp.getUTCMinutes() - 5); 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 }); diff --git a/e2e/tests/functional/plugins/timeConductor/timeConductor.e2e.spec.js b/e2e/tests/functional/plugins/timeConductor/timeConductor.e2e.spec.js index 79346f382e..9fe41e588f 100644 --- a/e2e/tests/functional/plugins/timeConductor/timeConductor.e2e.spec.js +++ b/e2e/tests/functional/plugins/timeConductor/timeConductor.e2e.spec.js @@ -24,65 +24,210 @@ import { setEndOffset, setFixedTimeMode, setRealTimeMode, - setStartOffset, - setTimeConductorBounds + setStartOffset } from '../../../../appActions.js'; import { expect, test } from '../../../../pluginFixtures.js'; 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 await page.goto('./', { waitUntil: 'domcontentloaded' }); - const year = new Date().getFullYear(); + }); - // Set initial valid time bounds - const startDate = `${year}-01-01`; - const startTime = '01:00:00'; - const endDate = `${year}-01-01`; - const endTime = '02:00:00'; - await setTimeConductorBounds(page, { startDate, startTime, endDate, endTime }); + test('validate date and time inputs are validated on input event', async ({ page }) => { + const submitButtonLocator = page.getByLabel('Submit time bounds'); // Open the time conductor popup await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click(); - // Test invalid start date - const invalidStartDate = `${year}-01-02`; - await page.getByLabel('Start date').fill(invalidStartDate); - await expect(page.getByLabel('Submit time bounds')).toBeDisabled(); - await page.getByLabel('Start date').fill(startDate); - await expect(page.getByLabel('Submit time bounds')).toBeEnabled(); + await test.step('invalid start date disables submit button', async () => { + const initialStartDate = await page.getByLabel('Start date').inputValue(); + const invalidStartDate = `${initialStartDate.substring(0, 5)}${initialStartDate.substring(6)}`; - // Test invalid end date - const invalidEndDate = `${year - 1}-12-31`; - await page.getByLabel('End date').fill(invalidEndDate); - await expect(page.getByLabel('Submit time bounds')).toBeDisabled(); - await page.getByLabel('End date').fill(endDate); - await expect(page.getByLabel('Submit time bounds')).toBeEnabled(); + await page.getByLabel('Start date').fill(invalidStartDate); + await expect(submitButtonLocator).toBeDisabled(); + await page.getByLabel('Start date').fill(initialStartDate); + await expect(submitButtonLocator).toBeEnabled(); + }); - // Test invalid start time - const invalidStartTime = '42:00:00'; - await page.getByLabel('Start time').fill(invalidStartTime); - await expect(page.getByLabel('Submit time bounds')).toBeDisabled(); - await page.getByLabel('Start time').fill(startTime); - await expect(page.getByLabel('Submit time bounds')).toBeEnabled(); + await test.step('invalid start time disables submit button', async () => { + const initialStartTime = await page.getByLabel('Start time').inputValue(); + const invalidStartTime = `${initialStartTime.substring(0, 5)}${initialStartTime.substring(6)}`; - // Test invalid end time - const invalidEndTime = '43:00:00'; - await page.getByLabel('End time').fill(invalidEndTime); - await expect(page.getByLabel('Submit time bounds')).toBeDisabled(); - await page.getByLabel('End time').fill(endTime); - await expect(page.getByLabel('Submit time bounds')).toBeEnabled(); + await page.getByLabel('Start time').fill(invalidStartTime); + await expect(submitButtonLocator).toBeDisabled(); + await page.getByLabel('Start time').fill(initialStartTime); + await expect(submitButtonLocator).toBeEnabled(); + }); - // Submit valid time bounds + 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)}`; + + await page.getByLabel('End time').fill(invalidEndTime); + await expect(page.getByLabel('End time')).not.toHaveAttribute('title', 'Invalid Time'); + await page.getByLabel('End time').press('Tab'); + 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'); + }); + }); + + 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(); - // Verify the submitted time bounds - await expect(page.getByLabel('Start bounds')).toHaveText( - new RegExp(`${startDate} ${startTime}.000Z`) + await expect(page.getByLabel('Start date')).toHaveAttribute( + 'title', + 'Specified start date exceeds end bound' ); - await expect(page.getByLabel('End bounds')).toHaveText( - new RegExp(`${endDate} ${endTime}.000Z`) + await expect(page.getByLabel('Start bounds')).not.toHaveText(`${DAY} ${TWO_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); + 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(); }); - 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 * between fixed timespan and real-time mode. diff --git a/src/plugins/timeConductor/TimePopupFixed.vue b/src/plugins/timeConductor/TimePopupFixed.vue index 7c098e4462..652df767e3 100644 --- a/src/plugins/timeConductor/TimePopupFixed.vue +++ b/src/plugins/timeConductor/TimePopupFixed.vue @@ -11,18 +11,19 @@ > @@ -37,7 +38,8 @@ autocorrect="off" spellcheck="false" aria-label="Start time" - @input="validateAllBounds('startDate')" + @input="validateInput('startTime')" + @change="reportValidity('startTime')" /> @@ -48,18 +50,19 @@ > @@ -74,14 +77,15 @@ autocorrect="off" spellcheck="false" aria-label="End time" - @input="validateAllBounds('endDate')" + @input="validateInput('endTime')" + @change="reportValidity('endTime')" />
@@ -125,6 +129,7 @@ export default { return { timeFormatter: this.getFormatter(timeSystem.timeFormat), durationFormatter: this.getFormatter(timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER), + timeSystemKey: timeSystem.key, bounds: { start: bounds.start, end: bounds.end @@ -136,9 +141,29 @@ export default { endTime: '' }, 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: { inputBounds: { handler(newBounds) { @@ -168,25 +193,17 @@ export default { this.setBounds(bounds); this.setViewFromBounds(bounds); }, - clearAllValidation() { - [this.$refs.startDate, this.$refs.endDate].forEach(this.clearValidationForInput); - }, - clearValidationForInput(input) { - if (input) { - input.setCustomValidity(''); - input.title = ''; - } - }, setBounds(bounds) { this.bounds = bounds; }, setViewFromBounds(bounds) { - this.formattedBounds.start = this.timeFormatter.format(bounds.start).split(' ')[0]; - this.formattedBounds.end = this.timeFormatter.format(bounds.end).split(' ')[0]; + this.formattedBounds.startDate = this.timeFormatter.format(bounds.start).split(' ')[0]; + this.formattedBounds.endDate = this.timeFormatter.format(bounds.end).split(' ')[0]; this.formattedBounds.startTime = this.durationFormatter.format(Math.abs(bounds.start)); this.formattedBounds.endTime = this.durationFormatter.format(Math.abs(bounds.end)); }, setTimeSystem(timeSystem) { + this.timeSystemKey = timeSystem.key; this.timeFormatter = this.getFormatter(timeSystem.timeFormat); this.durationFormatter = this.getFormatter( timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER @@ -201,10 +218,10 @@ export default { setBoundsFromView(dismiss) { if (this.$refs.fixedDeltaInput.checkValidity()) { let start = this.timeFormatter.parse( - `${this.formattedBounds.start} ${this.formattedBounds.startTime}` + `${this.formattedBounds.startDate} ${this.formattedBounds.startTime}` ); let end = this.timeFormatter.parse( - `${this.formattedBounds.end} ${this.formattedBounds.endTime}` + `${this.formattedBounds.endDate} ${this.formattedBounds.endTime}` ); this.$emit('update', { start, end }); @@ -215,96 +232,93 @@ export default { return false; } }, - handleFormSubmission(shouldDismiss) { - this.validateAllBounds('startDate'); - this.validateAllBounds('endDate'); + clearAllValidation() { + Object.keys(this.inputValidityMap).forEach(this.clearValidation); + }, + 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); } }, - validateAllBounds(ref) { - this.isDisabled = false; + validateInput(refName) { + this.clearAllValidation(); - if (!this.areBoundsFormatsValid()) { - this.isDisabled = true; - return false; - } + const inputType = refName.includes('Date') ? 'Date' : 'Time'; + const formatter = inputType === 'Date' ? this.timeFormatter : this.durationFormatter; + const validationResult = formatter.validate(this.formattedBounds[refName]) + ? { valid: true } + : { valid: false, message: `Invalid ${inputType}` }; - let validationResult = { valid: true }; - const currentInput = this.$refs[ref]; + this.inputValidityMap[refName] = validationResult; + }, + validateBounds() { + const bounds = { + start: this.timeFormatter.parse( + `${this.formattedBounds.startDate} ${this.formattedBounds.startTime}` + ), + end: this.timeFormatter.parse( + `${this.formattedBounds.endDate} ${this.formattedBounds.endTime}` + ) + }; - return [this.$refs.startDate, this.$refs.endDate].every((input) => { - let boundsValues = { - start: this.timeFormatter.parse( - `${this.formattedBounds.start} ${this.formattedBounds.startTime}` - ), - end: this.timeFormatter.parse( - `${this.formattedBounds.end} ${this.formattedBounds.endTime}` - ) + this.logicalValidityMap.bounds = this.openmct.time.validateBounds(bounds); + }, + 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, + message: 'Start and end difference exceeds allowable limit' }; - //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) { - if (input === currentInput) { - validationResult = { - valid: false, - message: 'Start and end difference exceeds allowable limit' - }; - } - } else if (input === currentInput) { - validationResult = this.openmct.time.validateBounds(boundsValues); - } - - return this.handleValidationResults(input, validationResult); - }); + } else { + this.logicalValidityMap.limit = { valid: true }; + } }, - areBoundsFormatsValid() { - return [this.$refs.startDate, this.$refs.endDate].every((input) => { - const formattedDate = - input === this.$refs.startDate - ? `${this.formattedBounds.start} ${this.formattedBounds.startTime}` - : `${this.formattedBounds.end} ${this.formattedBounds.endTime}`; + reportValidity(refName) { + const input = this.getInput(refName); + const validationResult = this.inputValidityMap[refName] ?? this.logicalValidityMap[refName]; - 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) { input.setCustomValidity(validationResult.message); input.title = validationResult.message; - this.isDisabled = true; + this.hasLogicalValidationErrors = true; } else { input.setCustomValidity(''); input.title = ''; } 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) { - this.formattedBounds.start = this.timeFormatter.format(date).split(' ')[0]; - this.validateAllBounds('startDate'); + this.formattedBounds.startDate = this.timeFormatter.format(date).split(' ')[0]; + this.validateInput('startDate'); + this.reportValidity('startDate'); }, endDateSelected(date) { - this.formattedBounds.end = this.timeFormatter.format(date).split(' ')[0]; - this.validateAllBounds('endDate'); + this.formattedBounds.endDate = this.timeFormatter.format(date).split(' ')[0]; + this.validateInput('endDate'); + this.reportValidity('endDate'); }, hide($event) { if ($event.target.className.indexOf('c-button icon-x') > -1) { From 7f8b5e09e5a750702e7252748203287ae26635b9 Mon Sep 17 00:00:00 2001 From: Shefali Joshi Date: Thu, 17 Oct 2024 10:28:55 -0700 Subject: [PATCH 05/11] Fix gantt chart swimlane order (#7895) * Use the planObject to get ordered swimlane names * If there is no plan object, don't try to render the chart --------- Co-authored-by: Jamie V. --- src/plugins/plan/components/PlanView.vue | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/plugins/plan/components/PlanView.vue b/src/plugins/plan/components/PlanView.vue index 86a9f3cdf1..f54082b895 100644 --- a/src/plugins/plan/components/PlanView.vue +++ b/src/plugins/plan/components/PlanView.vue @@ -243,8 +243,8 @@ export default { if (this.planObject) { this.showReplacePlanDialog(domainObject); } else { - this.swimlaneVisibility = this.configuration.swimlaneVisibility; this.setupPlan(domainObject); + this.swimlaneVisibility = this.configuration.swimlaneVisibility; } }, handleConfigurationChange(newConfiguration) { @@ -423,7 +423,10 @@ export default { return currentRow || SWIMLANE_PADDING; }, generateActivities() { - const groupNames = getValidatedGroups(this.domainObject, this.planData); + if (!this.planObject) { + return; + } + const groupNames = getValidatedGroups(this.planObject, this.planData); if (!groupNames.length) { return; From 3e23dceb64b04c348ca2585e4b4fdd5faed75e4b Mon Sep 17 00:00:00 2001 From: Shefali Joshi Date: Thu, 17 Oct 2024 14:24:26 -0700 Subject: [PATCH 06/11] fix(#7892): restore "now" (marcus bains) line to planning views (#7898) * Initialize alignment offset to 0. (it was undefined). Also handle a small bug with swimlane configuration not getting replaced when the plan used by a gantt chart was changed * lint: fix * test: update visual tests to mock clock and show now line --------- Co-authored-by: Mazzella, Jesse D. (ARC-TI)[KBR Wyle Services, LLC] --- e2e/helper/planningUtils.js | 3 +++ e2e/tests/visual-a11y/planning-gantt.visual.spec.js | 13 ++++++++++++- .../visual-a11y/planning-timestrip.visual.spec.js | 9 ++++++++- e2e/tests/visual-a11y/planning-view.visual.spec.js | 6 ++++++ src/plugins/plan/components/PlanView.vue | 1 + src/ui/components/TimeSystemAxis.vue | 2 ++ 6 files changed, 32 insertions(+), 2 deletions(-) diff --git a/e2e/helper/planningUtils.js b/e2e/helper/planningUtils.js index 43988532f3..2e1d8a8a70 100644 --- a/e2e/helper/planningUtils.js +++ b/e2e/helper/planningUtils.js @@ -129,6 +129,7 @@ export async function setBoundsToSpanAllActivities(page, planJson, planObjectUrl */ export function getEarliestStartTime(planJson) { const activities = Object.values(planJson).flat(); + return Math.min(...activities.map((activity) => activity.start)); } @@ -139,6 +140,7 @@ export function getEarliestStartTime(planJson) { */ export function getLatestEndTime(planJson) { const activities = Object.values(planJson).flat(); + return Math.max(...activities.map((activity) => activity.end)); } @@ -151,6 +153,7 @@ export function getFirstActivity(planJson) { const groups = Object.keys(planJson); const firstGroupKey = groups[0]; const firstGroupItems = planJson[firstGroupKey]; + return firstGroupItems[0]; } diff --git a/e2e/tests/visual-a11y/planning-gantt.visual.spec.js b/e2e/tests/visual-a11y/planning-gantt.visual.spec.js index f41cfa23fd..7af9942404 100644 --- a/e2e/tests/visual-a11y/planning-gantt.visual.spec.js +++ b/e2e/tests/visual-a11y/planning-gantt.visual.spec.js @@ -26,14 +26,25 @@ import fs from 'fs'; import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../appActions.js'; import { scanForA11yViolations, test } from '../../avpFixtures.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( 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.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' }); }); test('Gantt Chart View', async ({ page, theme }) => { diff --git a/e2e/tests/visual-a11y/planning-timestrip.visual.spec.js b/e2e/tests/visual-a11y/planning-timestrip.visual.spec.js index f0a6c9a90c..2ea1e25478 100644 --- a/e2e/tests/visual-a11y/planning-timestrip.visual.spec.js +++ b/e2e/tests/visual-a11y/planning-timestrip.visual.spec.js @@ -27,14 +27,21 @@ import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../appAct import { scanForA11yViolations, test } from '../../avpFixtures.js'; import { waitForAnimations } from '../../baseFixtures.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( 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.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' }); }); test('Time Strip View', async ({ page, theme }) => { diff --git a/e2e/tests/visual-a11y/planning-view.visual.spec.js b/e2e/tests/visual-a11y/planning-view.visual.spec.js index 7ca1200c03..6386f0cca4 100644 --- a/e2e/tests/visual-a11y/planning-view.visual.spec.js +++ b/e2e/tests/visual-a11y/planning-view.visual.spec.js @@ -42,6 +42,7 @@ const examplePlanSmall2 = JSON.parse( ); const FIRST_ACTIVITY_SMALL_1 = getFirstActivity(examplePlanSmall1); +const FIRST_ACTIVITY_SMALL_2 = getFirstActivity(examplePlanSmall2); test.describe('Visual - Timelist progress bar @clock @a11y', () => { test.beforeEach(async ({ page }) => { @@ -59,6 +60,11 @@ test.describe('Visual - Timelist progress bar @clock @a11y', () => { test.describe('Visual - Plan View @a11y', () => { 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' }); }); diff --git a/src/plugins/plan/components/PlanView.vue b/src/plugins/plan/components/PlanView.vue index f54082b895..fab47328b1 100644 --- a/src/plugins/plan/components/PlanView.vue +++ b/src/plugins/plan/components/PlanView.vue @@ -248,6 +248,7 @@ export default { } }, handleConfigurationChange(newConfiguration) { + this.configuration = this.planViewConfiguration.getConfiguration(); Object.keys(newConfiguration).forEach((key) => { this[key] = newConfiguration[key]; }); diff --git a/src/ui/components/TimeSystemAxis.vue b/src/ui/components/TimeSystemAxis.vue index e86f27f43c..92014e54df 100644 --- a/src/ui/components/TimeSystemAxis.vue +++ b/src/ui/components/TimeSystemAxis.vue @@ -79,6 +79,7 @@ export default { const svgWidth = ref(0); const svgHeight = ref(0); const axisTransform = ref('translate(0,20)'); + const alignmentOffset = ref(0); const nowMarkerStyle = reactive({ height: '0px', left: '0px' @@ -100,6 +101,7 @@ export default { svgWidth, svgHeight, axisTransform, + alignmentOffset, nowMarkerStyle, openmct }; From 518b55cf0f9baa865527fe6334caf37640eb3b6f Mon Sep 17 00:00:00 2001 From: Jesse Mazzella Date: Thu, 17 Oct 2024 15:08:52 -0700 Subject: [PATCH 07/11] fix(vipergc-660): identify axis keys upon adding object to composition (#7897) * fix: identify axis keys upon adding object to composition * fix: set yKey to 'none' if nonArrayValues --- src/plugins/charts/bar/BarGraphView.vue | 30 ++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/plugins/charts/bar/BarGraphView.vue b/src/plugins/charts/bar/BarGraphView.vue index 0bc90e3ebb..07c24932d6 100644 --- a/src/plugins/charts/bar/BarGraphView.vue +++ b/src/plugins/charts/bar/BarGraphView.vue @@ -332,7 +332,11 @@ export default { this.domainObject.configuration.axes.xKey === 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 = []; @@ -431,6 +435,30 @@ export default { subscribeToAll() { const telemetryObjects = Object.values(this.telemetryObjects); 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 }; } } }; From 078cd341a58e361000ef9331b732a2c2d9734196 Mon Sep 17 00:00:00 2001 From: John Hill Date: Fri, 18 Oct 2024 10:07:26 -0700 Subject: [PATCH 08/11] Bump references to support node22 (#7901) * Bump references to support node22 * strings! --------- Co-authored-by: Jesse Mazzella --- .circleci/config.yml | 8 ++++---- package-lock.json | 2 +- package.json | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 3998a1c918..c2bada139f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -286,8 +286,8 @@ workflows: overall-circleci-commit-status: #These jobs run on every commit jobs: - lint: - name: node20-lint - node-version: lts/iron + name: node22-lint + node-version: '22' - unit-test: name: node18-chrome node-version: lts/hydrogen @@ -304,8 +304,8 @@ workflows: the-nightly: #These jobs do not run on PRs, but against master at night jobs: - unit-test: - name: node20-chrome-nightly - node-version: lts/iron + name: node22-chrome-nightly + node-version: '22' - unit-test: name: node18-chrome node-version: lts/hydrogen diff --git a/package-lock.json b/package-lock.json index c82e60d1a7..1fdcfd514c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -93,7 +93,7 @@ "webpack-merge": "5.10.0" }, "engines": { - "node": ">=18.14.2 <22" + "node": ">=18.14.2 <23" } }, "e2e": { diff --git a/package.json b/package.json index b52c369f19..6e96eace12 100644 --- a/package.json +++ b/package.json @@ -139,7 +139,7 @@ "url": "git+https://github.com/nasa/openmct.git" }, "engines": { - "node": ">=18.14.2 <22" + "node": ">=18.14.2 <23" }, "browserslist": [ "Firefox ESR", From 057a5f997c8592cf5c9f404b75edaea64e14866f Mon Sep 17 00:00:00 2001 From: Shefali Joshi Date: Fri, 18 Oct 2024 12:59:02 -0700 Subject: [PATCH 09/11] Encode urls for css background images (#7906) * Add new utility to encode urls. Use the encode urls utility to encode all background images in css * need a commit to pull exampleimagery from * skip and fix on otherside --------- Co-authored-by: John Hill --- e2e/test-data/rick space roll.jpg | Bin 0 -> 10259 bytes .../imagery/exampleImagery.e2e.spec.js | 3 - .../imagery/exampleImageryFile.e2e.spec.js | 93 ++++++++++++++++++ .../components/inspector/StyleEditor.vue | 15 +-- .../displayLayout/components/ImageView.vue | 5 +- .../imagery/components/ImageThumbnail.vue | 7 +- .../imagery/components/ImageryView.vue | 5 +- src/ui/layout/AppLogo.vue | 3 +- src/utils/encoding.js | 25 +++++ 9 files changed, 140 insertions(+), 16 deletions(-) create mode 100644 e2e/test-data/rick space roll.jpg create mode 100644 e2e/tests/functional/plugins/imagery/exampleImageryFile.e2e.spec.js create mode 100644 src/utils/encoding.js diff --git a/e2e/test-data/rick space roll.jpg b/e2e/test-data/rick space roll.jpg new file mode 100644 index 0000000000000000000000000000000000000000..98340f6304bac2e1b8131a26c18a12903a5370e0 GIT binary patch literal 10259 zcmbVydt6gzy6(yqh@glI3#d~-gKJ^2To#CzX#*I{6>|Zqk?BTInngEOwDvYzCxD=? zQi~7~;-xXHBwRbSMn|jNgATD?nhv=@6|Fy|9oo{{wrXd_?iu#`Eu5Ki&R^%8_={xa zw$}H3&-=X3^L&}QK6M9+Ruw7>F&+=Y6z~t5x{IxozeZPKm{yCWVi*>V@p&~MY*|Ga>gjPYml1haWlL)epW zMIs(|<30xe@%RFvNE{X(A(;VhI28?-E)ejA0+9%vhPM-5$Aq&*F-aMD;yL<_Vac_4 zX5E3~;S2LmT!=OJ|7%fJ#kTqg$=oO6;uEAPiwQE7%FbDqyL?5#YI)%rg;HfKE-8I( z-Fj2yrp;9^nCa^6FYkEe)z@BsqhWVr)1JNinASGy!S+KPoi;}o>vXxhdwSnL={a@! zcW2IiaPbdA-b)`{zH&7X42_I_di}=O-SN-A_|wFfUwu7!@5lQO9{%*_zx?$P8W+sx z^swOn>5=^(Mjk8j=FZ3x|N{qr^jU&2dXv?!O#EvKmRi6fRH6aUY*sm}nGPfd+skpdoo zNiZ9G7CWbw$99XBlJYbxg>W#KiE!G5j+Q`GorLyGX2>r2i@efATt{cAWfAhUIJ?+J zbWzxDL&ZCVMEozv)O<%LBg? zTrbCPs~)~*>Eolcoc3lNUtdU?F&2HADbq7TDQ?j5^L0^EXq&AHp!-t8exz z6(UC~gOvrU7!_tB346svbHJbzoK)IW1aEsGO|X1;MlYLTQ<3n5+E#;O-v-TaI=C#% z(cWs2s9>z*cBLF!I=Refe*FHKzIkuuFyfU|C+mz|mU_Wf7BB=Ro+PT814gx^?84P& zsVc|qtfJe*mop6-zwOPBO@aokqKxhAq6{TvrB6^*txUxNTb9;gncK<`(R!skeJ%qR zWoDgr{vpZ=7g;FJVX#-sz8prlGPyZuwue=uw7s|l)%#lKph_hNfI-XK3PmE#E88`sIn=G60 zZ*?L;46`Hsz1TEs3!5Oz&}I&-{_#<1QQ^Lsv@F*dQ@j4#FR6Qpmr_+JuWR!+XU|Lj zC25^g`}d8``?ChtFy!^6&(=C4cUGs

<0)DAyL07Bs9M96vUnVZD`wOn>X65qsYJ zlAh>ctM$JP75(i9h|yNPJY7OKEE1CnfCHc=5K`o603gG78y-${A#laPH~@LI({P0A zB}`5|ttOd`bDmTx<(XUJ-2x3|56+fSep`VIqrMHjd{=g*&Mnz% zykL)5QL>j^$n@CWIAWAlH`q)}W_Ztw#RYM6RGKs+0;(ulFUK)Mz%a}!;vX);u`^w` z2g(jcEWpn|k;JiHxHy;)SV$;Axgb){XaWz3_@i>?#Odfsl}a-#tYEz`^kSv#ps!~8&Sdm$Vc zx0$c7g14k8be8NRy;3QYRIe~9Wg;o8ic*eS2uEw(n??B8R(qI1Z`0VYe7gC%>+;ad z)Re`s)=pMW)-wG+D733@ji%WSEcfMCm&VTSU7g`Ej5iw>(RRc5k-0!6yT)GIZ%Ov9 zjLl`<`IGUz*b_?Mh94*A=9AaM@2?eis#XL9MFQ{o-qU|ez0$Y0BdC;1*C+jBve1Ni z?rNQQ_)gr~X%|sY_1F%4tPDn>x2fPE)GaVhRr4?(CU(LiA$;+6e(NEs)1IeR+RBD` z3VOT39Cwv))IB*B3h<9A$QL(?mf@2%i3=D6PLhT?bF{Ww=E7X^TNqP;OiXxj4`wj< z6%_`Z*)C$yVIXxyOAy#~8j>wT90%!@zzm@Tg0c)GUMqfKV8ap{Bg1@~v(_CvBC!O{ zeQ{6o&HC%-_7>7P#b?|Vy+bWR-^!~!^mxbKRlWKJ>$>o7lhf;2xCr{X&|T5m-XTuS z(N(JoWLGt3ToTWC-+>0z#iuve&K9TrG5feSqB;5ST59b5)m;FX4#1Qy5izq9x89Y_ z0LZ9iQQoBl<){;r8AT4uJSL-vrLc}J{H)Uln5j5wBY1E$DS|HJUBO^m$Z&Tc0jnZw zRb&YJSSNp`Wms5j7-z7lksv;4c9&j^4oIRBf!YBPP{%3^?x+o*9%Bufc{HMGzZ{RG z9SzV{JXRe)JM#!_#n93L10+gRgnc6iBastoEv4Fd;jh*H-scuGpA;S-{_Gwod?U0^ z$E4s1ge!^6a|MEY8BJS)Ut0_TT@;oJ1-&C(QlOTnC)JktOx67(iR!T10~zawXC2OV zEO+gz)JjRVZ*?QC*YIHp^9}Gh-ecTCt?>;^0o0lYHNB>z3Nz3W*FxdytR5>o4@U|) z%!S!qVIv=46)nt;BzyQ?*1IZb7EFx>a1TKF^rDS+V>ccN)6`)X`cmlYEM?)QBCC+D z1(H6?x@T+cBF7=T!V(#%fy;aSAMxzsGbF6S(avV-+)hqbWQsMyFZV9w{ zZyYO#GM<@V5-eF7cFndgVPWq6YPrkyMsZ{t067@pf@T<8;ct+|G`X94$B#r|>s$|x z7tupT-tl{DV~V4+K5YFha=VUa;Az5+9bC7++Piw!!=$-)HInyd;h~>B-x{L>W_zH- zAxC`|KwOU6d!k!XL)aro+IoJNZxc!94-39>-B&lrX5_IG4K_@#)^yo~E0`*6JcWHO zUxrBJf%P_>Y38d`ThNnti@NWx^zKogXeH}&ZVu2*gdC*?T+2EWn=lW#o| zFz{;CTHjJcJZ2w8vaBx!zRbA2P>Zc-J#5PYrq=l4SwXjJqS+=+as{BgNhnT`9b_bG z0&|a$p1ZPHj?Ol9n?jf^ymq#_nvRd_+1!=X6tz56`JA}x`BS1Jw2?yd(xx3)jJroDxcNjr2v!lQ4*QAR>EEY1awvb4Oty%kn795Zp#?Bd{*S zN#UeuDb0hjVL)VHAg8hl&Dc#fqY2j8h2A)KRf`SV%hco|hz*Z+;Z@C}7w4&!YT1lJ z!iQT(&!nmxN)&n}kOCt^bPsBb$xzFo0idF`Y(cL^W)zZi#JDAn_5haka;ZQ`h z7P@GQo-}QN3iN==i_pSp1(sWK6aRQ~$^OvL<*%I7L9W)INgFU%AYhEP-a{~c1^N<> zs3t)Vr$s#~&d%ub?Beh68YgJQ96u7LXISquhk>f52U05sJe+a!lttnWU@Xus$;s%d zcvJonZxa5fU_hn0n#k3wRBrSfMwW`t7Qqd7kKH#Y}Fm4^Mkd*l}J-6#-=G7HB zanI0YAMQF_tN@ZpmjGxvY8k@0@-7QZG#kb(bEv9jod~pQnV}3 zMO!Cp(y%lfYApz4%rkRBi@|C@9f{JUh_wkS5~zyc(F1C2HN0TerL#!(7^dHW-Cf)5 zea5mtF2$RqVunaq`0}mPuGJ&E_NSW2ETRWLSfz)#sc!-)ZNS)12-gwd=)XAciwZn} z8j44660^RN)jyPCF2PqU%*HQQj2sX2T}t(4uCsb7+D>IeQdU=MM?FKhxJ5v^tbV$h zaf8H!6{tv3j;&~KF$8#Y6rNC?b5sjrn<0s4PY~VSvYO|9L+)y zW}&JrySu)p-Ktlqv@IVh_$-FFB10bxlWiYw?VAN82 z_b`$>kt7lW1Pc@n1i9616g|^2!xjMsr^)P-id)3=xwGuzSnNMOOgdimP08NAm zq4$T-L`tPAnsw>oqoXZpt0Y)Stdv46OYf7a8a~Wa7MB-WFBJJQ zQ;;6etpcboIa-gv>X@B&aR8dAkvABaXoC`-sHaH+^MO07o1q3nvOwEKjbX1jT>`c3 zvGg%8TYybhfK05i2sG|F8kmw$@Ma>!E9lyh#V{_!|HUv+o4TLsly`Ox8jn&Z(iAi>;Ta$j#G&8K45Vf#oyAIgV=a zN0=rs2hosbSerE7rzQjg*i&zN^JZUbD;d)S?JwCJe@gyhPja8_pf8`EiyMGOfY}+W zCE>Mhd2WKRh^)@0zW;TQH?px%^jz$wmtBcYyI~||zTXF=_XnklCig;{;*2A-6PXCH zZ^sN@Jmv9Vw`^HOEL2OGUoE@zJ+*~819fpLFmVVk*Cjziz7Br{@b@ox2aa_*tdcUF zfZkr@eHx~L&N4)k=>)H{D;pKnYC1Kaw1!WOi~x>WPetFACqk=XND_0k7z_wdgk9va znP_`CNjutGW$%WIzo>sbs3zLupS+Qf+MUrSH=zYGbNKD zYy3=U%g|CyOU<8YQ9Q7vYsWI&z`MbMbH-ko%p}1~|IVm%wBeB&zZ|zfU}3M&E7nOG zo&19zRaD5j$7HE}e=IQ>s<(*T?*w{1Lx1j`F-OZj@ryFs7PA)@UE~_`FSVIg!|Z|h zTb};T347g{zJ>#(O~nhvOuPJ}?6~8OpoGT3ma@ms2fc9)OW4firjZ5m5m#2nVXFe>M z=yhJ{Idg68FH7l&?tPTC{_{8C0towa^VDiNpJS$)6|I(-IyuhUBK|UVXVeP?qrdHO zSq?VcE!lg)YcgtI4zrzb<;j<3s13g8G1V(%h9P5~GxONrNNdc+_Eu)otmjWXU%RJo z#;D`$&!M3bR23?cCNX2fY^|g2*6BmS9eq73PE`h)mIONH*&B-bJH8M9#uSmhvFMH# z85vD}MYFNnE*?LG0}N}NH$nLR+5-nY`bC)!ea+6*`3hYn}|icRUF4kn*rCm{jbBHJ6C|ED&L!1UVkHIgCFf2SsT@_(91t|}E zzR4NcUu&Xm(iJFYKpsZ~asV8aWCs`jQ2}EQQr28r*z)9*-%m+0 zWU+Q20wx{$Ct0hI3nV79ljRkfp>%~{%aHU34GQz7s7O#nNwL`HOUF_#^#9z%UNUZP z(v6qMbW7BYVl=@hPz6HAzi2H%uNsr?Xdne;R zFr3VMXn#soS@J`1)z2T{pOatf=GY*+)XR3W00p!zk&mxK@YLY-ji&MUKF_VbQE*Uf zc~Xv-aBBwI*#jP>3PMGbirl*s8m?t-8)Y!WfxWrluIkwHcvZ*1tf72?A+uypP6Yx^ z@ZBaZr6Qx!s6d>f-3B%q@FYl}Js?l=kz$H7BtZ8NB3Y6DVoH^CZ{PjXPcR>6Keef_ zvxpjNfr6_Oaak*L7LO6sZpgrpdpUE73=oebLsrb%yOkzs-nOhW#kbdIE93Sb%)C0@ z6KNf+ewOqq7E|}Oe&|h4o;P|VzIS70!mh7;iA&n4ly&z@YvY}~U+aViy7l_2mWMq& zc31Im$;VB1#yi*k<~(g30&L;CQHSL^8;B;Z(F;O%Mn7ImEWt|$PF$_9r1_QVVotlM zLD=Jfap_W$yl9Dps9?vmBLQ2p6dK|aD9OS!hKi#~RKf_Wj0V6am97Kc&rXH>?yAnB0U@q0gk)fh%us|OfhcmciQB*rv-o_l z&>`z~?Sy4+i{B^3q}lsL&m6b@x2xsjD{FlvK4sE`qo-VaSS?SAa>7*3(85BXX~A5! z9aNAff2PX~v-dbA;X_n{0#VD{ck{1?WfvW@wAINHHU9W1J@?((T$jb~U{Wol8I%*OC8#U7JiEaFVv0TsQ-r1tAn$|NCkWPP zTE4?Iqn2t?ft8m1W@;qhHp*9cP%@v?C>6JI#s%{vPvvz*(t(>|2 zBQu(=cvCVP9(I5IgFXZts8{3%wJTbN1yE2VW?&!!2}9vAZ80|H)XDY{EN+CjAIU&S z1kwGS_&-#2xp}gL{LS86#X)_=qT*vlBma{J)W)cijLYQj&zr;>GMtGeVNCa?7UQbX zPol7Yo7L`JlXa)OxI}PbF^*l_4hb>_*hPt*3Q;3Qz$t)mOfsTx^uy}C1M~FWj6+|s z&R%Y>0p`MlWfVe5&4H3?=St0lAs4L~=2x^qqGuQ0RG)5duZy~=JpGOmS|K{>h~$nG zv!+IaxW}(9MODiOzD8{Tm^+X_M)Bj!c!CH;1!$wF)spZL|K#T7DZjnE^_1<)0d;)I z2SM_?(N2XB;M}sukU)ShK~7o+*}_aPonxp}J7(dN<It%YRfEQS)Eu9MXaA}`v?}Hf4i0aX88B()$EYDz)HL zOezRE>nnd8KWTgSR>B{*8!qL4Mtj&`sth~<3-9#HW`I^ebPwwa!9BEn9Y&6Kxf?VE z-H@=MI`~JqMO5x|4Ma?`<#Q)~8~!Pd9ow07B;obl#ZN~G_>hG~7k4BD7VU4A`JcFCzi}}EFj}e2+=3`&aqki*R>D8V7b=_NE+&oifERcfG??#gl zW`HfJN^o0;sBrMZ;H;S~^mgF)6nQ)zPC}vi1PdT+Ls=Z;_&)vuIhbMKC-xS(OoB}f z2VnE)y_j}JC8PX#z}A3T%o8kf*|_Y00~FLt*Y?Jg7JTtGt-jLMzUE4I>bfDyI;qN^ zrz~;>IF_7VM3_Ym?P#r_W-+y#O4y;RhYJOWlNMl5tXGk}N%3%9$zOh|iT_4+`TUnZ zD5|norrIo*T$Y7&HC1jpa<5L4*TjlWmTVX>WtV&~EP%D=+B|BVoK2B(4;YnR7PHuN zefOV4mA^HOHtfcSui+l+DcSUDfR@yQl9Zw?7F1i2s8PvwBSnd_EBK$=DMj&k+Vn0< zKo>UU4?rdK^Bd&SL@uPe$U${?@U8U6ID--^QSZ5UpmbkIS)QOE z?d0QNqY}ANLkR<#7A|E#?1y@mkf0vzKSEj_LA$qj42B@rIo$95J#_WN91(=FGZf^x z+x_iE&B|KE=87*Kx}P=`3aGwO%Tps&-;}1F+hG^OG=dOm?Esyp0G9yrol(JF>|JFM zK4?p7Gx?i|{-1YT8AuBBMD9rA2oCrbh6OAKbvI3SuvBP!G7p3#mneb7eH@4&8@y&J zfU1b?<=t?#A${z01%@-wCn0%A*od}&5#X&lp&>_9dG%(;sP((X?EN35IGbrO00&^- z8ay5sL_KC6RP!7m}KOC6x-uCZ` zqttQ07z)6@3qg_3ZPnLEAyDAP4|>%CXW>c)91{_Dhkh8{!*eBt^l}|1(F}Dq6~TXL zNwQ;;cjd2nny0i$dqt|F)ft900Em*(Z%Wt3_YRCA6~*liajJ-h)CqbW1CaJ&7m`3<${`A0XO&cb2W70eCcXouY= zSk9@?@8!~1*-DUbN%k=07<>Yd&Q?gHv`%VF2Gn81VDV*!bMdj(7pw2SnpXMVL|fcd zRhc2-fCwlPDexhv$BGt63IE%uu)_jQOH5o3(m`(!rnqNI_};RF9NW3~B>A41-UNNA zO0wG$m9fqQCv+vj%5Y?>%3$`LfJ~Ug?!sJj#U}5tZ~AU{1ZL#0vt)}WoQp(}(pG>t z5IXS+z#L-&<4n5VflcN5{)>&a<0V%H6qSQMDs$+?I<%S0p>Wz!A)(H3;lHK?rIO%n zrk~&=!7yuTRTWGECN~?3Nh&}*aqUmm-DS@M0_6!BpFDWJ>gon=Z%iEmG~1C z=^5&^H`VXA1Qxn%@02^Q6Y6E6gz3_mW+3RBs9^s0JNO%}tnk69(X$9I_!#VD!_{!Mbclu1AgqB=YdNA5y?$8o zBKda8K-uz7Quk#)B}_RCmJ=ksN`}ZKiAJ1+v@|S=RhMa=yqvv0bL{j)P6csnpdbEU(@3W>pJq5ppkpc9r|12_ez?FaM;#CRl$J@TQi8raX zjYKK_R-M|8%!UP)g@oTvfUQCS1{e~U?5%L65r%$A|EZ1Uv=|*w_3;Py^g%D=Of==$ zZRE29#aH@TiEMpSlrY0Hwu2g0Jj|Bnr%vY { 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 ({ page, browserName diff --git a/e2e/tests/functional/plugins/imagery/exampleImageryFile.e2e.spec.js b/e2e/tests/functional/plugins/imagery/exampleImageryFile.e2e.spec.js new file mode 100644 index 0000000000..a030e43f78 --- /dev/null +++ b/e2e/tests/functional/plugins/imagery/exampleImageryFile.e2e.spec.js @@ -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(); + }); +}); diff --git a/src/plugins/condition/components/inspector/StyleEditor.vue b/src/plugins/condition/components/inspector/StyleEditor.vue index 512c75cc03..076e16889e 100644 --- a/src/plugins/condition/components/inspector/StyleEditor.vue +++ b/src/plugins/condition/components/inspector/StyleEditor.vue @@ -28,11 +28,7 @@ { 'is-style-invisible': styleItem.style && styleItem.style.isStyleInvisible }, { 'c-style-thumb--mixed': mixedStyles.indexOf('backgroundColor') > -1 } ]" - :style="[ - styleItem.style.imageUrl - ? { backgroundImage: 'url(' + styleItem.style.imageUrl + ')' } - : itemStyle - ]" + :style="[encodedImageUrl ? { backgroundImage: 'url(' + encodedImageUrl + ')' } : itemStyle]" class="c-style-thumb" > -1 }; }, + encodedImageUrl() { + return encode_url(this.styleItem.style.imageUrl); + }, isStyleInvisibleOption() { return { value: this.styleItem.style.isStyleInvisible, diff --git a/src/plugins/displayLayout/components/ImageView.vue b/src/plugins/displayLayout/components/ImageView.vue index b22b8d87aa..d653f3afcf 100644 --- a/src/plugins/displayLayout/components/ImageView.vue +++ b/src/plugins/displayLayout/components/ImageView.vue @@ -35,6 +35,7 @@