Compare commits

...

9 Commits

Author SHA1 Message Date
Shefali
5092ba8763 Return awaited promise 2024-03-20 12:33:54 -07:00
dependabot[bot]
1d5ddc545e chore(deps-dev): bump @types/lodash from 4.14.192 to 4.17.0 (#7610)
Bumps [@types/lodash](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/lodash) from 4.14.192 to 4.17.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/lodash)

---
updated-dependencies:
- dependency-name: "@types/lodash"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-19 22:57:38 -07:00
John Hill
42085a4b70 [CI] Parallelize visual test runs (#7618)
* rename suite and add parallelism

* use test sharding

* expect 2 parallel runs

---------

Co-authored-by: Scott Bell <scott@traclabs.com>
2024-03-19 20:50:14 -07:00
Scott Bell
b2b0837592 Handle empty namespaces in import (#7619)
* handle blank namespaces in import

---------

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2024-03-19 13:05:14 -07:00
Scott Bell
e305b46d88 Ensure a request for telemetry happens in Condition Sets (#7592)
* request telemetry when subscribing to data in case we have cached subscription

* change back to >=

* revert

* update tests

* fixing tests

* add metadata

* fix test

* another mock required

* one more function needed

* attempt to fix some afterall errors

* add fixme for e2e test

* fail fast on request

---------

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2024-03-19 10:44:50 -07:00
Jamie V
fb396ac194 [Telemetry Table] Telemetry mode bug fixes (#7601)
* source maps

* any tables without configuration will default to either default options or configured options

* prevent double unsubscribese

* remove source maps

* update coment

* moving defaults to plugin level

* whoops

* missed a spot, updated omment

* adding config values

* lint

* typos

* fixing broken ref

* fixing broken ref

* actually fixing ref

* setting rowLimit so initial change does not trigger a resubscribe of telemetry that was not subscribed yet
2024-03-19 02:34:00 +00:00
Shefali Joshi
a01f21017f For the setTimeConductorMode, use the close time popup button rather than the submit button to dismiss time popup (#7613)
Use the close time popup button rather than the submit button as the submit button triggers network requests.
2024-03-18 23:48:33 +00:00
Rukmini Bose (Ruki)
b7b9ccbe65 [TC Popup] Fix Calendar so it is not cutoff (#7596) 2024-03-18 23:28:09 +00:00
Scott Bell
f189a4d602 Resize plans properly (#7597)
* resize firing

* ensure watcher fires

* remove unneeded const

* add small visual test resizing plan

* use browser with null viewport

* lint
2024-03-18 15:13:19 -07:00
27 changed files with 394 additions and 129 deletions

View File

@@ -10,6 +10,7 @@ executors:
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_PARALLEL_TOTAL: 2
ubuntu:
machine:
image: ubuntu-2204:current
@@ -220,15 +221,20 @@ 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
- run: npm run test:e2e:visual:<<parameters.suite>>
node-version: lts/iron
- run:
command: |
mkdir test-results
TESTFILES=$(circleci tests glob "e2e/**/*.spec.js")
echo "$TESTFILES" | circleci tests run --command="xargs npm run test:e2e:visual:<<parameters.suite>>" --verbose --split-by=timings
- store_test_results:
path: test-results/results.xml
- store_artifacts:
@@ -254,8 +260,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,8 +280,8 @@ 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:

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

@@ -457,4 +457,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)',

8
package-lock.json generated
View File

@@ -21,7 +21,7 @@
"@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",
@@ -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": {

View File

@@ -17,7 +17,7 @@
"@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",

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);