Merge branch 'master' into eval-source-maps

This commit is contained in:
David Tsay 2024-03-25 11:12:17 -07:00
commit cc58dbd5e7
32 changed files with 426 additions and 170 deletions

View File

@ -5,18 +5,19 @@ orbs:
executors:
pw-focal-development:
docker:
- image: mcr.microsoft.com/playwright:v1.39.0-focal
- image: mcr.microsoft.com/playwright:v1.42.1-focal
environment:
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_LOGLEVEL: "debug" # Enable DEBUG level logging for Percy (Issue: https://github.com/nasa/openmct/issues/5742)
PERCY_POSTINSTALL_BROWSER: 'true' # Needed to store the percy browser in cache deps
PERCY_LOGLEVEL: 'debug' # Enable DEBUG level logging for Percy (Issue: https://github.com/nasa/openmct/issues/5742)
PERCY_PARALLEL_TOTAL: 2
ubuntu:
machine:
image: ubuntu-2204:current
docker_layer_caching: true
commands:
build_and_install:
description: "All steps used to build and install."
description: 'All steps used to build and install.'
parameters:
node-version:
type: string
@ -26,7 +27,7 @@ commands:
node-version: << parameters.node-version >>
- node/install-packages
generate_and_store_version_and_filesystem_artifacts:
description: "Track important packages and files"
description: 'Track important packages and files'
steps:
- run: |
[[ $EUID -ne 0 ]] && (sudo mkdir -p /tmp/artifacts && sudo chmod 777 /tmp/artifacts) || (mkdir -p /tmp/artifacts && chmod 777 /tmp/artifacts)
@ -37,7 +38,7 @@ commands:
- store_artifacts:
path: /tmp/artifacts/
generate_e2e_code_cov_report:
description: "Generate e2e code coverage artifacts and publish to codecov.io. Needed to that we can ignore the exit code status of the npm run test"
description: 'Generate e2e code coverage artifacts and publish to codecov.io. Needed to that we can ignore the exit code status of the npm run test'
parameters:
suite:
type: string
@ -101,7 +102,7 @@ jobs:
node-version: lts/hydrogen
- when: #Only install chrome-beta when running the 'full' suite to save $$$
condition:
equal: ["full", <<parameters.suite>>]
equal: ['full', <<parameters.suite>>]
steps:
- run: npx playwright install chrome-beta
- run:
@ -158,7 +159,7 @@ jobs:
steps:
- build_and_install:
node-version: lts/hydrogen
- run: npx playwright@1.39.0 install #Necessary for bare ubuntu machine
- run: npx playwright@1.42.1 install #Necessary for bare ubuntu machine
- run: |
export $(cat src/plugins/persistence/couch/.env.ci | xargs)
docker-compose -f src/plugins/persistence/couch/couchdb-compose.yaml up --detach
@ -220,14 +221,15 @@ jobs:
equal: [42, 42] # Always run codecov reports regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2
steps:
- generate_and_store_version_and_filesystem_artifacts
visual-a11y-tests:
visual-a11y:
parameters:
suite:
type: string # ci or full
executor: pw-focal-development
parallelism: 2
steps:
- build_and_install:
node-version: lts/hydrogen
node-version: lts/iron
- run: npm run test:e2e:visual:<<parameters.suite>>
- store_test_results:
path: test-results/results.xml
@ -254,8 +256,8 @@ workflows:
name: e2e-stable
suite: stable
- e2e-mobile
- visual-a11y-tests:
name: visual-a11y-test-ci
- visual-a11y:
name: visual-a11y-ci
suite: ci
the-nightly: #These jobs do not run on PRs, but against master at night
@ -274,13 +276,13 @@ workflows:
- e2e-mobile
- perf-test
- mem-test
- visual-a11y-tests:
name: visual-a11y-test-nightly
- visual-a11y:
name: visual-a11y-nightly
suite: full
- e2e-couchdb
triggers:
- schedule:
cron: "0 0 * * *"
cron: '0 0 * * *'
filters:
branches:
only:

View File

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

View File

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

View File

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

View File

@ -33,7 +33,7 @@ jobs:
restore-keys: |
${{ runner.os }}-node-
- run: npx playwright@1.39.0 install
- run: npx playwright@1.42.1 install
- run: npx playwright install chrome-beta
- run: npm ci --no-audit --progress=false
- run: npm run test:e2e:full -- --max-failures=40

View File

@ -16,8 +16,6 @@ The [CodeQL GitHub Actions workflow](https://github.com/nasa/openmct/blob/master
CodeQL is run for every pull-request in GitHub Actions.
The project is also monitored by [LGTM](https://lgtm.com/projects/g/nasa/openmct/) and is available to public.
### ESLint
Static analysis is run for every push on the master branch and every pull request on all branches in Github Actions.

View File

@ -392,7 +392,8 @@ async function setTimeConductorMode(page, isFixedTimespan = true) {
await page.getByRole('menuitem', { name: /Real-Time/ }).click();
await page.waitForURL(/tc\.mode=local/);
}
await page.getByLabel('Submit time offsets').or(page.getByLabel('Submit time bounds')).click();
//dismiss the time conductor popup
await page.getByLabel('Discard changes and close time popup').click();
}
/**

View File

@ -292,6 +292,16 @@ test.describe('Basic Condition Set Use', () => {
await expect(page.getByRole('menuitem', { name: /Conditions View/ })).toBeVisible();
await expect(page.getByRole('menuitem', { name: /Plot/ })).toBeVisible();
await expect(page.getByRole('menuitem', { name: /Telemetry Table/ })).toBeVisible();
await page.getByLabel('Plot').click();
await expect(
page.getByLabel('Plot Legend Collapsed').getByText('Test Condition Set')
).toBeVisible();
await page.getByLabel('Open the View Switcher Menu').click();
await page.getByLabel('Telemetry Table').click();
await expect(page.getByRole('searchbox', { name: 'output filter input' })).toBeVisible();
await page.getByLabel('Open the View Switcher Menu').click();
await page.getByLabel('Conditions View').click();
await expect(page.getByText('Current Output')).toBeVisible();
});
test('ConditionSet has correct outputs when telemetry is and is not available', async ({
page
@ -457,4 +467,11 @@ test.describe('Basic Condition Set Use', () => {
await page.goto(exampleTelemetry.url);
});
test.fixme('Ensure condition sets work with telemetry like operator status', ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/7484'
});
});
});

View File

@ -0,0 +1,64 @@
/*****************************************************************************
* 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.
*****************************************************************************/
import {
createDomainObjectWithDefaults,
setIndependentTimeConductorBounds
} from '../../../../appActions.js';
import { expect, test } from '../../../../pluginFixtures.js';
const FIXED_TIME =
'./#/browse/mine?tc.mode=fixed&tc.startBound=1693592063607&tc.endBound=1693593893607&tc.timeSystem=utc&view=grid&hideInspector=true&hideTree=true';
test.describe('Datepicker operations', () => {
test.beforeEach(async ({ page }) => {
await page.goto(FIXED_TIME);
});
test('Verify that user can use the datepicker in the TC', async ({ page }) => {
await page.getByLabel('Time Conductor Mode').click();
// Click on the date picker that is left-most on the screen
await page.getByLabel('Global Time Conductor').locator('a').first().click();
await expect(page.getByRole('dialog')).toBeVisible();
// Click on the first cell
await page.getByText('27 239').click();
// Expect datepicker to close and time conductor date setting to be changed
await expect(page.getByRole('dialog')).toHaveCount(0);
});
test('Verify that user can use the datepicker in the ITC', async ({ page }) => {
const createdTimeList = await createDomainObjectWithDefaults(page, { type: 'Time List' });
await page.goto(createdTimeList.url, { waitUntil: 'domcontentloaded' });
await setIndependentTimeConductorBounds(page, {
start: '2024-11-12 19:11:11.000Z',
end: '2024-11-12 20:11:11.000Z'
});
// Open ITC
await page.getByLabel('Start bounds').nth(0).click();
// Click on the datepicker icon
await page.locator('form a').first().click();
await expect(page.getByRole('dialog')).toBeVisible();
// Click on the first cell
await page.getByText('7 342').click();
// Expect datepicker to close and time conductor date setting to be changed
await expect(page.getByRole('dialog')).toHaveCount(0);
});
});

View File

@ -72,11 +72,29 @@ test.describe('Visual - Planning', () => {
name: 'Plan Visual Test',
json: examplePlanSmall2
});
await setBoundsToSpanAllActivities(page, examplePlanSmall2, plan.url);
await percySnapshot(page, `Plan View (theme: ${theme})`);
});
test('Resize Plan View @2p', async ({ browser, theme }) => {
// need to set viewport to null to allow for resizing
const newContext = await browser.newContext({
viewport: null
});
const newPage = await newContext.newPage();
await newPage.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
const plan = await createPlanFromJSON(newPage, {
name: 'Plan Visual Test',
json: examplePlanSmall2
});
await setBoundsToSpanAllActivities(newPage, examplePlanSmall2, plan.url);
// resize the window
await newPage.setViewportSize({ width: 800, height: 600 });
await percySnapshot(newPage, `Plan View resized (theme: ${theme})`);
});
test('Plan View w/ draft status', async ({ page, theme }) => {
const plan = await createPlanFromJSON(page, {
name: 'Plan Visual Test (Draft)',

45
package-lock.json generated
View File

@ -14,14 +14,14 @@
"@braintree/sanitize-url": "6.0.4",
"@percy/cli": "1.27.4",
"@percy/playwright": "1.0.4",
"@playwright/test": "1.39.0",
"@playwright/test": "1.42.1",
"@types/d3-axis": "3.0.6",
"@types/d3-scale": "4.0.8",
"@types/d3-selection": "3.0.10",
"@types/d3-shape": "3.0.0",
"@types/eventemitter3": "1.2.0",
"@types/jasmine": "5.1.2",
"@types/lodash": "4.14.192",
"@types/lodash": "4.17.0",
"@vue/compiler-sfc": "3.4.3",
"babel-loader": "9.1.0",
"babel-plugin-istanbul": "6.1.1",
@ -1548,12 +1548,12 @@
}
},
"node_modules/@playwright/test": {
"version": "1.39.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.39.0.tgz",
"integrity": "sha512-3u1iFqgzl7zr004bGPYiN/5EZpRUSFddQBra8Rqll5N0/vfpqlP9I9EXqAoGacuAbX6c9Ulg/Cjqglp5VkK6UQ==",
"version": "1.42.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.42.1.tgz",
"integrity": "sha512-Gq9rmS54mjBL/7/MvBaNOBwbfnh7beHvS6oS4srqXFcQHpQCV1+c8JXWE8VLPyRDhgS3H8x8A7hztqI9VnwrAQ==",
"dev": true,
"dependencies": {
"playwright": "1.39.0"
"playwright": "1.42.1"
},
"bin": {
"playwright": "cli.js"
@ -1821,9 +1821,9 @@
"dev": true
},
"node_modules/@types/lodash": {
"version": "4.14.192",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.192.tgz",
"integrity": "sha512-km+Vyn3BYm5ytMO13k9KTp27O75rbQ0NFw+U//g+PX7VZyjCioXaRFisqSIJRECljcTv73G3i6BpglNGHgUQ5A==",
"version": "4.17.0",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz",
"integrity": "sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==",
"dev": true
},
"node_modules/@types/mime": {
@ -6027,9 +6027,9 @@
"dev": true
},
"node_modules/follow-redirects": {
"version": "1.15.5",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
"integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
"version": "1.15.6",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"dev": true,
"funding": [
{
@ -9026,12 +9026,12 @@
}
},
"node_modules/playwright": {
"version": "1.39.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.39.0.tgz",
"integrity": "sha512-naE5QT11uC/Oiq0BwZ50gDmy8c8WLPRTEWuSSFVG2egBka/1qMoSqYQcROMT9zLwJ86oPofcTH2jBY/5wWOgIw==",
"version": "1.42.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.42.1.tgz",
"integrity": "sha512-PgwB03s2DZBcNRoW+1w9E+VkLBxweib6KTXM0M3tkiT4jVxKSi6PmVJ591J+0u10LUrgxB7dLRbiJqO5s2QPMg==",
"dev": true,
"dependencies": {
"playwright-core": "1.39.0"
"playwright-core": "1.42.1"
},
"bin": {
"playwright": "cli.js"
@ -9048,7 +9048,6 @@
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.42.1.tgz",
"integrity": "sha512-mxz6zclokgrke9p1vtdy/COWBH+eOZgYUVVU34C73M+4j4HLlQJHtfcqiqqxpP0o8HhMkflvfbquLX5dg6wlfA==",
"dev": true,
"peer": true,
"bin": {
"playwright-core": "cli.js"
},
@ -9070,18 +9069,6 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/playwright/node_modules/playwright-core": {
"version": "1.39.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.39.0.tgz",
"integrity": "sha512-+k4pdZgs1qiM+OUkSjx96YiKsXsmb59evFoqv8SKO067qBA+Z2s/dCzJij/ZhdQcs2zlTAgRKfeiiLm8PQ2qvw==",
"dev": true,
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=16"
}
},
"node_modules/plotly.js-basic-dist-min": {
"version": "2.29.1",
"resolved": "https://registry.npmjs.org/plotly.js-basic-dist-min/-/plotly.js-basic-dist-min-2.29.1.tgz",

View File

@ -10,14 +10,14 @@
"@braintree/sanitize-url": "6.0.4",
"@percy/cli": "1.27.4",
"@percy/playwright": "1.0.4",
"@playwright/test": "1.39.0",
"@playwright/test": "1.42.1",
"@types/d3-axis": "3.0.6",
"@types/d3-shape": "3.0.0",
"@types/d3-scale": "4.0.8",
"@types/d3-selection": "3.0.10",
"@types/eventemitter3": "1.2.0",
"@types/jasmine": "5.1.2",
"@types/lodash": "4.14.192",
"@types/lodash": "4.17.0",
"@vue/compiler-sfc": "3.4.3",
"babel-loader": "9.1.0",
"babel-plugin-istanbul": "6.1.1",
@ -156,4 +156,4 @@
"keywords": [
"nasa"
]
}
}

View File

@ -56,20 +56,38 @@ export default class ConditionManager extends EventEmitter {
);
}
subscribeToTelemetry(endpoint) {
const id = this.openmct.objects.makeKeyString(endpoint.identifier);
if (this.subscriptions[id]) {
console.log('subscription already exists');
async requestLatestValue(endpoint) {
const options = {
size: 1,
strategy: 'latest'
};
const latestData = await this.openmct.telemetry.request(endpoint, options);
if (!latestData) {
throw new Error('Telemetry request failed by returning a falsy response');
}
if (latestData.length === 0) {
return;
}
this.telemetryReceived(endpoint, latestData[0]);
}
subscribeToTelemetry(endpoint) {
const telemetryKeyString = this.openmct.objects.makeKeyString(endpoint.identifier);
if (this.subscriptions[telemetryKeyString]) {
return;
}
const metadata = this.openmct.telemetry.getMetadata(endpoint);
this.telemetryObjects[id] = Object.assign({}, endpoint, {
this.telemetryObjects[telemetryKeyString] = Object.assign({}, endpoint, {
telemetryMetaData: metadata ? metadata.valueMetadatas : []
});
this.subscriptions[id] = this.openmct.telemetry.subscribe(
// get latest telemetry value (in case subscription is cached and no new data is coming in)
this.requestLatestValue(endpoint);
this.subscriptions[telemetryKeyString] = this.openmct.telemetry.subscribe(
endpoint,
this.telemetryReceived.bind(this, endpoint)
);
@ -91,7 +109,7 @@ export default class ConditionManager extends EventEmitter {
//force re-computation of condition set result as we might be in a state where
// there is no telemetry datum coming in for a while or at all.
let latestTimestamp = getLatestTimestamp(
const latestTimestamp = getLatestTimestamp(
{},
{},
this.timeSystems,
@ -334,57 +352,54 @@ export default class ConditionManager extends EventEmitter {
return currentCondition;
}
requestLADConditionSetOutput(options) {
async requestLADConditionSetOutput(options) {
if (!this.conditions.length) {
return Promise.resolve([]);
return [];
}
return this.compositionLoad.then(() => {
let latestTimestamp;
let conditionResults = {};
let nextLegOptions = { ...options };
delete nextLegOptions.onPartialResponse;
await this.compositionLoad;
const conditionRequests = this.conditions.map((condition) =>
condition.requestLADConditionResult(nextLegOptions)
let latestTimestamp;
let conditionResults = {};
let nextLegOptions = { ...options };
delete nextLegOptions.onPartialResponse;
const results = await Promise.all(
this.conditions.map((condition) => condition.requestLADConditionResult(nextLegOptions))
);
results.forEach((resultObj) => {
const {
id,
data,
data: { result }
} = resultObj;
if (this.findConditionById(id)) {
conditionResults[id] = Boolean(result);
}
latestTimestamp = getLatestTimestamp(
latestTimestamp,
data,
this.timeSystems,
this.openmct.time.timeSystem()
);
return Promise.all(conditionRequests).then((results) => {
results.forEach((resultObj) => {
const {
id,
data,
data: { result }
} = resultObj;
if (this.findConditionById(id)) {
conditionResults[id] = Boolean(result);
}
latestTimestamp = getLatestTimestamp(
latestTimestamp,
data,
this.timeSystems,
this.openmct.time.timeSystem()
);
});
if (!Object.values(latestTimestamp).some((timeSystem) => timeSystem)) {
return [];
}
const currentCondition = this.getCurrentConditionLAD(conditionResults);
const currentOutput = Object.assign(
{
output: currentCondition.configuration.output,
id: this.conditionSetDomainObject.identifier,
conditionId: currentCondition.id
},
latestTimestamp
);
return [currentOutput];
});
});
if (!Object.values(latestTimestamp).some((timeSystem) => timeSystem)) {
return [];
}
const currentCondition = this.getCurrentConditionLAD(conditionResults);
const currentOutput = {
output: currentCondition.configuration.output,
id: this.conditionSetDomainObject.identifier,
conditionId: currentCondition.id,
...latestTimestamp
};
return [currentOutput];
}
isTelemetryUsed(endpoint) {
@ -409,7 +424,7 @@ export default class ConditionManager extends EventEmitter {
}
const normalizedDatum = this.createNormalizedDatum(datum, endpoint);
const timeSystemKey = this.openmct.time.timeSystem().key;
const timeSystemKey = this.openmct.time.getTimeSystem().key;
let timestamp = {};
const currentTimestamp = normalizedDatum[timeSystemKey];
timestamp[timeSystemKey] = currentTimestamp;

View File

@ -40,12 +40,10 @@ export default class ConditionSetTelemetryProvider {
return domainObject.type === 'conditionSet';
}
request(domainObject, options) {
async request(domainObject, options) {
let conditionManager = this.getConditionManager(domainObject);
return conditionManager.requestLADConditionSetOutput(options).then((latestOutput) => {
return latestOutput;
});
let latestOutput = await conditionManager.requestLADConditionSetOutput(options);
return latestOutput;
}
subscribe(domainObject, callback) {

View File

@ -66,7 +66,8 @@ describe('the plugin', function () {
format: 'utc',
hints: {
domain: 1
}
},
source: 'utc'
},
{
key: 'testSource',
@ -720,6 +721,23 @@ describe('the plugin', function () {
});
it('should evaluate as old when telemetry is not received in the allotted time', (done) => {
openmct.telemetry = jasmine.createSpyObj('telemetry', [
'subscribe',
'getMetadata',
'request',
'getValueFormatter',
'abortAllRequests'
]);
openmct.telemetry.getMetadata.and.returnValue({
...testTelemetryObject.telemetry,
valueMetadatas: []
});
openmct.telemetry.request.and.returnValue(Promise.resolve([]));
openmct.telemetry.getValueFormatter.and.returnValue({
parse: function (value) {
return value;
}
});
let conditionMgr = new ConditionManager(conditionSetDomainObject, openmct);
conditionMgr.on('conditionSetResultUpdated', mockListener);
conditionMgr.telemetryObjects = {
@ -741,6 +759,20 @@ describe('the plugin', function () {
});
it('should not evaluate as old when telemetry is received in the allotted time', (done) => {
openmct.telemetry.getMetadata = jasmine.createSpy('getMetadata');
openmct.telemetry.getMetadata.and.returnValue({
...testTelemetryObject.telemetry,
valueMetadatas: testTelemetryObject.telemetry.values
});
const testDatum = {
'some-key2': '',
utc: 1,
testSource: '',
'some-key': null,
id: 'test-object'
};
openmct.telemetry.request = jasmine.createSpy('request');
openmct.telemetry.request.and.returnValue(Promise.resolve([testDatum]));
const date = 1;
conditionSetDomainObject.configuration.conditionCollection[0].configuration.criteria[0].input =
['0.4'];
@ -750,9 +782,7 @@ describe('the plugin', function () {
'test-object': testTelemetryObject
};
conditionMgr.updateConditionTelemetryObjects();
conditionMgr.telemetryReceived(testTelemetryObject, {
utc: date
});
conditionMgr.telemetryReceived(testTelemetryObject, testDatum);
setTimeout(() => {
expect(mockListener).toHaveBeenCalledWith({
output: 'Default',
@ -868,6 +898,12 @@ describe('the plugin', function () {
it('should stop evaluating conditions when a condition evaluates to true', () => {
const date = Date.now();
let conditionMgr = new ConditionManager(conditionSetDomainObject, openmct);
openmct.telemetry.getMetadata = jasmine.createSpy('getMetadata');
openmct.telemetry.getMetadata.and.returnValue({
...testTelemetryObject.telemetry,
valueMetadatas: []
});
conditionMgr.on('conditionSetResultUpdated', mockListener);
conditionMgr.telemetryObjects = {
'test-object': testTelemetryObject

View File

@ -150,16 +150,15 @@ export default class ImportAsJSONAction {
* @param {string} namespace
* @returns {object}
*/
_generateNewIdentifiers(tree, namespace) {
_generateNewIdentifiers(tree, newNamespace) {
// For each domain object in the file, generate new ID, replace in tree
Object.keys(tree.openmct).forEach((domainObjectId) => {
const newId = {
namespace,
key: uuid()
};
const oldId = parseKeyString(domainObjectId);
const newId = {
namespace: newNamespace,
key: uuid()
};
tree = this._rewriteId(oldId, newId, tree);
}, this);
@ -228,22 +227,32 @@ export default class ImportAsJSONAction {
_rewriteId(oldId, newId, tree) {
let newIdKeyString = this.openmct.objects.makeKeyString(newId);
let oldIdKeyString = this.openmct.objects.makeKeyString(oldId);
tree = JSON.stringify(tree).replace(new RegExp(oldIdKeyString, 'g'), newIdKeyString);
return JSON.parse(tree, (key, value) => {
const newTreeString = JSON.stringify(tree).replace(
new RegExp(oldIdKeyString, 'g'),
newIdKeyString
);
const newTree = JSON.parse(newTreeString, (key, value) => {
if (
value !== undefined &&
value !== null &&
Object.prototype.hasOwnProperty.call(value, 'key') &&
Object.prototype.hasOwnProperty.call(value, 'namespace') &&
value.key === oldId.key &&
value.namespace === oldId.namespace
Object.prototype.hasOwnProperty.call(value, 'namespace')
) {
return newId;
} else {
return value;
// first check if key is messed up from regex and contains a colon
// if it does, repair it
if (value.key.includes(':')) {
const splitKey = value.key.split(':');
value.key = splitKey[1];
value.namespace = splitKey[0];
}
// now check if we need to replace the id
if (value.key === oldId.key && value.namespace === oldId.namespace) {
return newId;
}
}
return value;
});
return newTree;
}
/**
* @private

View File

@ -135,11 +135,75 @@ describe('The import JSON action', function () {
selectFile: {
name: 'imported object',
// eslint-disable-next-line prettier/prettier
body: "{\"openmct\":{\"c28d230d-e909-4a3e-9840-d9ef469dda70\":{\"identifier\":{\"key\":\"c28d230d-e909-4a3e-9840-d9ef469dda70\",\"namespace\":\"\"},\"name\":\"Unnamed Overlay Plot\",\"type\":\"telemetry.plot.overlay\",\"composition\":[],\"configuration\":{\"series\":[]},\"modified\":1695837546833,\"location\":\"mine\",\"created\":1695837546833,\"persisted\":1695837546833,\"__proto__\":{\"toString\":\"foobar\"}}},\"rootId\":\"c28d230d-e909-4a3e-9840-d9ef469dda70\"}"
body: '{"openmct":{"c28d230d-e909-4a3e-9840-d9ef469dda70":{"identifier":{"key":"c28d230d-e909-4a3e-9840-d9ef469dda70","namespace":""},"name":"Unnamed Overlay Plot","type":"telemetry.plot.overlay","composition":[],"configuration":{"series":[]},"modified":1695837546833,"location":"mine","created":1695837546833,"persisted":1695837546833,"__proto__":{"toString":"foobar"}}},"rootId":"c28d230d-e909-4a3e-9840-d9ef469dda70"}'
}
};
return Promise.resolve(pollutedResponse);
}
});
it('preserves the integrity of the namespace and key during import', async () => {
const incomingObject = {
openmct: {
'7323f02a-06ac-438d-bd58-6d6e33b8741e': {
name: 'Some Folder',
type: 'folder',
composition: [
{
key: '9f6c2d21-5ec8-434c-9fe8-31614ae6d7e6',
namespace: ''
}
],
modified: 1710843256162,
location: 'mine',
created: 1710843243471,
persisted: 1710843256162,
identifier: {
namespace: '',
key: '7323f02a-06ac-438d-bd58-6d6e33b8741e'
}
},
'9f6c2d21-5ec8-434c-9fe8-31614ae6d7e6': {
name: 'Some Clock',
type: 'clock',
configuration: {
baseFormat: 'YYYY/MM/DD hh:mm:ss',
use24: 'clock12',
timezone: 'UTC'
},
modified: 1710843256152,
location: '7323f02a-06ac-438d-bd58-6d6e33b8741e',
created: 1710843256152,
persisted: 1710843256152,
identifier: {
namespace: '',
key: '9f6c2d21-5ec8-434c-9fe8-31614ae6d7e6'
}
}
},
rootId: '7323f02a-06ac-438d-bd58-6d6e33b8741e'
};
const targetDomainObject = {
identifier: {
namespace: 'starJones',
key: '84438cda-a071-48d1-b9bf-d77bd53e59ba'
},
type: 'folder'
};
spyOn(openmct.objects, 'save').and.callFake((model) => Promise.resolve(model));
try {
await importFromJSONAction.onSave(targetDomainObject, {
selectFile: { body: JSON.stringify(incomingObject) }
});
for (const callArgs of openmct.objects.save.calls.allArgs()) {
const savedObject = callArgs[0]; // Assuming the first argument is the object being saved.
expect(savedObject.identifier.key.includes(':')).toBeFalse(); // Ensure no colon in the key.
expect(savedObject.identifier.namespace).toBe(targetDomainObject.identifier.namespace);
}
} catch (error) {
fail(error);
}
});
});

View File

@ -169,6 +169,8 @@ describe('Notebook plugin:', () => {
openmct.editor = {};
openmct.editor.isEditing = () => false;
openmct.editor.on = () => {};
openmct.editor.off = () => {};
const applicableViews = openmct.objectViews.get(notebookViewObject, [notebookViewObject]);
notebookViewProvider = applicableViews.find(

View File

@ -25,7 +25,7 @@ import mount from 'utils/mount';
import TableConfigurationComponent from './components/TableConfiguration.vue';
import TelemetryTableConfiguration from './TelemetryTableConfiguration.js';
export default function TableConfigurationViewProvider(openmct) {
export default function TableConfigurationViewProvider(openmct, options) {
return {
key: 'table-configuration',
name: 'Config',
@ -45,7 +45,7 @@ export default function TableConfigurationViewProvider(openmct) {
return {
show: function (element) {
tableConfiguration = new TelemetryTableConfiguration(domainObject, openmct);
tableConfiguration = new TelemetryTableConfiguration(domainObject, openmct, options);
const { destroy } = mount(
{
el: element,

View File

@ -32,14 +32,14 @@ import TelemetryTableRow from './TelemetryTableRow.js';
import TelemetryTableUnitColumn from './TelemetryTableUnitColumn.js';
export default class TelemetryTable extends EventEmitter {
constructor(domainObject, openmct) {
constructor(domainObject, openmct, options) {
super();
this.domainObject = domainObject;
this.openmct = openmct;
this.tableComposition = undefined;
this.datumCache = [];
this.configuration = new TelemetryTableConfiguration(domainObject, openmct);
this.configuration = new TelemetryTableConfiguration(domainObject, openmct, options);
this.telemetryMode = this.configuration.getTelemetryMode();
this.rowLimit = this.configuration.getRowLimit();
this.paused = false;

View File

@ -24,11 +24,12 @@ import EventEmitter from 'EventEmitter';
import _ from 'lodash';
export default class TelemetryTableConfiguration extends EventEmitter {
constructor(domainObject, openmct) {
constructor(domainObject, openmct, options) {
super();
this.domainObject = domainObject;
this.openmct = openmct;
this.defaultOptions = options;
this.columns = {};
this.removeColumnsForObject = this.removeColumnsForObject.bind(this);
@ -48,10 +49,12 @@ export default class TelemetryTableConfiguration extends EventEmitter {
configuration.columnOrder = configuration.columnOrder || [];
configuration.cellFormat = configuration.cellFormat || {};
configuration.autosize = configuration.autosize === undefined ? true : configuration.autosize;
// anything that doesn't have a telemetryMode existed before the change and should stay as it was for consistency
configuration.telemetryMode = configuration.telemetryMode ?? 'unlimited';
configuration.persistModeChange = configuration.persistModeChange ?? true;
configuration.rowLimit = configuration.rowLimit ?? 50;
// anything that doesn't have a telemetryMode existed before the change and should
// take the properties of any passed in defaults or the defaults from the plugin
configuration.telemetryMode = configuration.telemetryMode ?? this.defaultOptions.telemetryMode;
configuration.persistModeChange =
configuration.persistModeChange ?? this.defaultOptions.persistModeChange;
configuration.rowLimit = configuration.rowLimit ?? this.defaultOptions.rowLimit;
return configuration;
}

View File

@ -20,8 +20,8 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
export default function getTelemetryTableType(options = {}) {
const { telemetryMode = 'performance', persistModeChange = true, rowLimit = 50 } = options;
export default function getTelemetryTableType(options) {
let { telemetryMode, persistModeChange, rowLimit } = options;
return {
name: 'Telemetry Table',

View File

@ -33,7 +33,7 @@ export default class TelemetryTableView {
this.component = null;
Object.defineProperty(this, 'table', {
value: new TelemetryTable(domainObject, openmct),
value: new TelemetryTable(domainObject, openmct, options),
enumerable: false,
configurable: false
});

View File

@ -398,15 +398,17 @@ export default {
totalNumberOfRows: 0,
rowContext: {},
telemetryMode: configuration.telemetryMode,
rowLimit: configuration.rowLimit,
persistModeChange: configuration.persistModeChange,
afterLoadActions: []
afterLoadActions: [],
existingConfiguration: configuration
};
},
computed: {
dropTargetStyle() {
return {
top: this.$refs.headersTable.offsetTop + 'px',
height: this.totalHeight + this.$refs.headersTable.offsetHeight + 'px',
top: this.$refs.headersHolderEl.offsetTop + 'px',
height: this.totalHeight + this.$refs.headersHolderEl.offsetHeight + 'px',
left: this.dropOffsetLeft && this.dropOffsetLeft + 'px'
};
},
@ -595,23 +597,35 @@ export default {
},
handleConfigurationChanges(changes) {
const { rowLimit, telemetryMode, persistModeChange } = changes;
const telemetryModeChanged = this.existingConfiguration.telemetryMode !== telemetryMode;
let rowLimitChanged = false;
this.persistModeChange = persistModeChange;
// both rowLimit changes and telemetryMode changes
// require a re-request of telemetry
if (this.rowLimit !== rowLimit) {
rowLimitChanged = true;
this.rowLimit = rowLimit;
this.table.updateRowLimit(rowLimit);
if (this.telemetryMode !== telemetryMode) {
// need to clear and resubscribe, if different, handled below
this.table.clearAndResubscribe();
}
}
if (this.telemetryMode !== telemetryMode) {
// check for telemetry mode change, because you could technically have persist mode changes
// set to false, which could create a state where the configuration saved telemetry mode is
// different from the currently set telemetry mode
if (telemetryModeChanged && this.telemetryMode !== telemetryMode) {
this.telemetryMode = telemetryMode;
// this method also re-requests telemetry
this.table.updateTelemetryMode(telemetryMode);
}
if (rowLimitChanged && !telemetryModeChanged) {
this.table.clearAndResubscribe();
}
this.existingConfiguration = changes;
},
updateVisibleRows() {
if (!this.updatingView) {

View File

@ -25,10 +25,12 @@ import getTelemetryTableType from './TelemetryTableType.js';
import TelemetryTableViewProvider from './TelemetryTableViewProvider.js';
import TelemetryTableViewActions from './ViewActions.js';
export default function plugin(options) {
export default function plugin(
options = { telemetryMode: 'performance', persistModeChange: true, rowLimit: 50 }
) {
return function install(openmct) {
openmct.objectViews.addProvider(new TelemetryTableViewProvider(openmct, options));
openmct.inspectorViews.addProvider(new TableConfigurationViewProvider(openmct));
openmct.inspectorViews.addProvider(new TableConfigurationViewProvider(openmct, options));
openmct.types.addType('table', getTelemetryTableType(options));
openmct.composition.addPolicy((parent, child) => {
if (parent.type === 'table') {

View File

@ -195,7 +195,10 @@ describe('the plugin', () => {
utc: false,
'some-key': false,
'some-other-key': false
}
},
persistModeChange: true,
rowLimit: 50,
telemetryMode: 'performance'
}
};
const testTelemetry = [

View File

@ -29,7 +29,7 @@
}"
>
<a class="c-icon-button icon-calendar" @click="toggle"></a>
<div v-if="open" class="c-menu c-menu--mobile-modal c-datetime-picker">
<div v-if="open" role="dialog" class="c-menu c-menu--mobile-modal c-datetime-picker">
<div class="c-datetime-picker__close-button">
<button class="c-click-icon icon-x-in-circle" @click="toggle"></button>
</div>

View File

@ -21,7 +21,7 @@
/>
<date-picker
v-if="isUTCBased"
class="c-ctrl-wrapper--menus-left"
class="c-ctrl-wrapper--menus-right"
:default-date-time="formattedBounds.start"
:formatter="timeFormatter"
@date-selected="startDateSelected"
@ -87,7 +87,7 @@
></button>
<button
class="c-button icon-x"
aria-label="Discard time bounds"
aria-label="Discard changes and close time popup"
@click.prevent="hide"
></button>
</div>

View File

@ -132,7 +132,7 @@
></button>
<button
class="c-button icon-x"
aria-label="Discard time offsets"
aria-label="Discard changes and close time popup"
@click.prevent="hide"
></button>
</div>

View File

@ -454,6 +454,12 @@
color: $colorTimeRealtimeFg;
}
}
.c-ctrl-wrapper--menus-up{ // A bit hacky, but we are rewriting the CSS class here for ITC such that the calendar opens at the bottom to avoid cutoff
.c-menu {
top: auto;
bottom: revert !important;
};
}
}
}

View File

@ -711,6 +711,20 @@
}
}
&[class*='--menus-down'] {
.c-menu {
top: auto;
bottom: 100%;
}
}
&[class*='--menus-right'] {
.c-menu {
left: 0;
right: auto;
}
}
&[class*='--menus-left'],
&[class*='menus-to-left'] {
.c-menu {

View File

@ -77,12 +77,13 @@ export default {
},
setup() {
const axisHolder = ref(null);
const { size, startObserving } = useResizeObserver();
const { size: containerSize, startObserving } = useResizeObserver();
onMounted(() => {
startObserving(axisHolder.value);
});
return {
containerSize: size
axisHolder,
containerSize
};
},
watch: {
@ -95,8 +96,11 @@ export default {
contentHeight() {
this.updateNowMarker();
},
containerSize() {
this.resize();
containerSize: {
handler() {
this.resize();
},
deep: true
}
},
mounted() {
@ -104,7 +108,7 @@ export default {
this.useSVG = true;
}
this.container = select(this.$refs.axisHolder);
this.container = select(this.axisHolder);
this.svgElement = this.container.append('svg:svg');
// draw x axis with labels. CSS is used to position them.
this.axisElement = this.svgElement
@ -122,7 +126,7 @@ export default {
},
methods: {
resize() {
if (this.$refs.axisHolder.clientWidth !== this.width) {
if (this.axisHolder.clientWidth !== this.width) {
this.setDimensions();
this.drawAxis(this.bounds, this.timeSystem);
this.updateNowMarker();
@ -139,11 +143,10 @@ export default {
}
},
setDimensions() {
const axisHolder = this.$refs.axisHolder;
this.width = axisHolder.clientWidth;
this.width = this.axisHolder.clientWidth;
this.offsetWidth = this.width - this.offset;
this.height = Math.round(axisHolder.getBoundingClientRect().height);
this.height = Math.round(this.axisHolder.getBoundingClientRect().height);
if (this.useSVG) {
this.svgElement.attr('width', this.width);