2.0.3 merge to master (#5157)

* Release 2.0.3

* Fix tick values for plots ticks in log mode and null check (#5119)

* [2297] When there is no display range or range, skip setting the range value when auto scale is turned off.

* If the formatted value is a number and a float, set precision to 2 decimal points.

* Fix value assignment

* Use whole numbers in log mode

* Revert whole numbers fix - need floats for values between 0 and 1.

* Handle scrolling to focused image on resize/new data (#5121)

* Scroll to focused image when view resizes - this will force scrolling to focused image when going to/from view large mode

* Scroll to the right if there is no paused focused image

* [LAD Tables] Use Telemetry Collections (#5127)

* Use telemetry collections to handle bounds checks

* added telemetry collection to alphanumeric telemetry view (#5131)

* Added animation styling for POS and CAM; adjusted cutoff for isNewImage (#5116)

* Added animation styling for POS and CAM; adjusted cutoff for isNewImage

* Remove animation from POS and CAM

* Fix transactions overwriting latest objects with stale objects on save (#5132)

* use object (map) instead of set to track dirty objects
* fix tests due to internals change

Co-authored-by: Nikhil <nikhil.k.mandlik@nasa.gov>

* Gauge edit enabled 2.0.3 (#5133)

* Gauge plugin #4896, add edit mode

* Dynamic dial-type Gauge sizing by height and width (#5129)

* Improve sizing strategy for gauges.
* Do not install gauge by default for now

Co-authored-by: Nikhil <nikhil.k.mandlik@nasa.gov>
Co-authored-by: Jamie Vigliotta <jamie.j.vigliotta@nasa.gov>
Co-authored-by: Andrew Henry <akhenry@gmail.com>

* [Telemetry Collections] Include data with start and end bounds (#5145)

* Reverts forced precision for log plots axis labels (#5147)

* Condition Widgets trigger hundreds of persistence calls (#5146)

Co-authored-by: unlikelyzero <jchill2@gmail.com>

Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
Co-authored-by: Michael Rogers <contact@mhrogers.com>
Co-authored-by: David Tsay <3614296+davetsay@users.noreply.github.com>
Co-authored-by: Nikhil <nikhil.k.mandlik@nasa.gov>
Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com>
Co-authored-by: Andrew Henry <akhenry@gmail.com>
Co-authored-by: unlikelyzero <jchill2@gmail.com>
This commit is contained in:
Shefali Joshi
2022-05-03 11:09:12 -07:00
committed by GitHub
parent b77a4066f2
commit c089a4760d
23 changed files with 701 additions and 434 deletions

View File

@ -21,13 +21,17 @@
*****************************************************************************/ *****************************************************************************/
/* /*
This test suite is dedicated to tests which verify the basic operations surrounding conditionSets. This test suite is dedicated to tests which verify the basic operations surrounding conditionSets. Note: this
suite is sharing state between tests which is considered an anti-pattern. Implimenting in this way to
demonstrate some playwright for test developers. This pattern should not be re-used in other CRUD suites.
*/ */
const { test, expect } = require('@playwright/test'); const { test, expect } = require('@playwright/test');
test.describe('Condition Set Operations', () => { let conditionSetUrl;
test('Create new button `condition set` creates new condition object', async ({ page }) => { let getConditionSetIdentifierFromUrl;
test('Create new Condition Set object and store @localStorage', async ({ page, context }) => {
//Go to baseURL //Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' }); await page.goto('/', { waitUntil: 'networkidle' });
@ -39,27 +43,141 @@ test.describe('Condition Set Operations', () => {
// Click text=OK // Click text=OK
await Promise.all([ await Promise.all([
page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/dab945d4-5a84-480e-8180-222b4aa730fa?tc.mode=fixed&tc.startBound=1639696164435&tc.endBound=1639697964435&tc.timeSystem=utc&view=conditionSet.view' }*/), page.waitForNavigation(),
page.click('text=OK') page.click('text=OK')
]); ]);
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set'); await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
//Save localStorage for future test execution
await context.storageState({ path: './e2e/tests/recycled_storage.json' });
//Set object identifier from url
conditionSetUrl = await page.url();
console.log('conditionSetUrl ' + conditionSetUrl);
getConditionSetIdentifierFromUrl = await conditionSetUrl.split('/').pop().split('?')[0];
console.log('getConditionSetIdentifierFromUrl ' + getConditionSetIdentifierFromUrl);
});
test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
//Load localStorage for subsequent tests
test.use({ storageState: './e2e/tests/recycled_storage.json' });
//Begin suite of tests again localStorage
test('Condition set object properties persist in main view and inspector', async ({ page }) => {
//Navigate to baseURL with injected localStorage
await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
//Assertions on loaded Condition Set in main view
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
//Assertions on loaded Condition Set in Inspector
await expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy;
//Reload Page
await Promise.all([
page.reload(),
page.waitForLoadState('networkidle')
]);
//Re-verify after reload
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
//Assertions on loaded Condition Set in Inspector
await expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy;
}); });
test.fixme('condition set object properties exist', async ({ page }) => { test('condition set object can be modified on @localStorage', async ({ page }) => {
//Go to object created in step one await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
//Verify the Condition Set properties persist on Save
//Verify the Condition Set properties persist on page.reload() //Assertions on loaded Condition Set in main view
}); await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
test.fixme('condition set object can be modified', async ({ page }) => {
//Go to object created in step one
//Update the Condition Set properties //Update the Condition Set properties
//Verify the Condition Set properties persist on Save // Click Edit Button
//Verify the Condition Set properties persist on page.reload() await page.locator('text=Conditions View Snapshot >> button').nth(3).click();
//Edit Condition Set Name from main view
await page.locator('text=Unnamed Condition Set').first().fill('Renamed Condition Set');
await page.locator('text=Renamed Condition Set').first().press('Enter');
// Click Save Button
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
// Click Save and Finish Editing Option
await page.locator('text=Save and Finish Editing').click();
//Verify Main section reflects updated Name Property
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Renamed Condition Set');
// Verify Inspector properties
// Verify Inspector has updated Name property
await expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy();
// Verify Inspector Details has updated Name property
await expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy();
// Verify Tree reflects updated Name proprety
// Expand Tree
await page.locator('text=Open MCT My Items >> span >> nth=3').click();
// Verify Condition Set Object is renamed in Tree
await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
// Verify Search Tree reflects renamed Name property
await page.locator('input[type="search"]').fill('Renamed');
await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
//Reload Page
await Promise.all([
page.reload(),
page.waitForLoadState('networkidle')
]);
//Verify Main section reflects updated Name Property
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Renamed Condition Set');
// Verify Inspector properties
// Verify Inspector has updated Name property
await expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy();
// Verify Inspector Details has updated Name property
await expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy();
// Verify Tree reflects updated Name proprety
// Expand Tree
await page.locator('text=Open MCT My Items >> span >> nth=3').click();
// Verify Condition Set Object is renamed in Tree
await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
// Verify Search Tree reflects renamed Name property
await page.locator('input[type="search"]').fill('Renamed');
await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
}); });
test.fixme('condition set object can be deleted', async ({ page }) => { test('condition set object can be deleted by Search Tree Actions menu on @localStorage', async ({ page }) => {
//Go to object created in step one //Navigate to baseURL
//Verify that Condition Set object can be deleted await page.goto('/', { waitUntil: 'networkidle' });
//Verify the Condition Set object does not exist in Tree
//Verify the Condition Set object does not exist with direct navigation to object's URL //Expect Unnamed Condition Set to be visible in Main View
await expect(page.locator('a:has-text("Unnamed Condition Set Condition Set")')).toBeVisible();
// Search for Unnamed Condition Set
await page.locator('input[type="search"]').fill('Unnamed Condition Set');
// Right Click to Open Actions Menu
await page.locator('a:has-text("Unnamed Condition Set")').click({
button: 'right'
});
// Click Remove Action
await page.locator('text=Remove').click();
await page.locator('text=OK').click();
//Expect Unnamed Condition Set to be removed in Main View
await expect(page.locator('a:has-text("Unnamed Condition Set Condition Set")')).not.toBeVisible();
await page.locator('.c-search__clear-input').click();
// Search for Unnamed Condition Set
await page.locator('input[type="search"]').fill('Unnamed Condition Set');
// Expect Unnamed Condition Set to be removed
await expect(page.locator('a:has-text("Unnamed Condition Set")')).not.toBeVisible();
//Feature?
//Domain Object is still available by direct URL after delete
await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
}); });
}); });

View File

@ -0,0 +1,22 @@
{
"cookies": [],
"origins": [
{
"origin": "http://localhost:8080",
"localStorage": [
{
"name": "tcHistory",
"value": "{\"utc\":[{\"start\":1651513945533,\"end\":1651515745533}]}"
},
{
"name": "mct",
"value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"c0f99e39-85e7-4ef7-99b1-ef52d4ed69b2\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1651515746374,\"modified\":1651515746374},\"c0f99e39-85e7-4ef7-99b1-ef52d4ed69b2\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"c0f99e39-85e7-4ef7-99b1-ef52d4ed69b2\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"e35a066b-eb0e-4b05-a4c9-cc31dc202572\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1651515746373,\"location\":\"mine\",\"persisted\":1651515746373}}"
},
{
"name": "mct-tree-expanded",
"value": "[]"
}
]
}
]
}

View File

@ -1,6 +1,6 @@
{ {
"name": "openmct", "name": "openmct",
"version": "2.0.3-SNAPSHOT", "version": "2.0.3",
"description": "The Open MCT core platform", "description": "The Open MCT core platform",
"devDependencies": { "devDependencies": {
"@babel/eslint-parser": "7.16.3", "@babel/eslint-parser": "7.16.3",

View File

@ -241,8 +241,6 @@ define([
this.branding = BrandingAPI.default; this.branding = BrandingAPI.default;
// Plugins that are installed by default // Plugins that are installed by default
this.install(this.plugins.Gauge());
this.install(this.plugins.Plot()); this.install(this.plugins.Plot());
this.install(this.plugins.Chart()); this.install(this.plugins.Chart());
this.install(this.plugins.TelemetryTable.default()); this.install(this.plugins.TelemetryTable.default());

View File

@ -22,12 +22,14 @@
export default class Transaction { export default class Transaction {
constructor(objectAPI) { constructor(objectAPI) {
this.dirtyObjects = new Set(); this.dirtyObjects = {};
this.objectAPI = objectAPI; this.objectAPI = objectAPI;
} }
add(object) { add(object) {
this.dirtyObjects.add(object); const key = this.objectAPI.makeKeyString(object.identifier);
this.dirtyObjects[key] = object;
} }
cancel() { cancel() {
@ -37,7 +39,8 @@ export default class Transaction {
commit() { commit() {
const promiseArray = []; const promiseArray = [];
const save = this.objectAPI.save.bind(this.objectAPI); const save = this.objectAPI.save.bind(this.objectAPI);
this.dirtyObjects.forEach(object => {
Object.values(this.dirtyObjects).forEach(object => {
promiseArray.push(this.createDirtyObjectPromise(object, save)); promiseArray.push(this.createDirtyObjectPromise(object, save));
}); });
@ -48,7 +51,9 @@ export default class Transaction {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
action(object) action(object)
.then((success) => { .then((success) => {
this.dirtyObjects.delete(object); const key = this.objectAPI.makeKeyString(object.identifier);
delete this.dirtyObjects[key];
resolve(success); resolve(success);
}) })
.catch(reject); .catch(reject);
@ -57,7 +62,8 @@ export default class Transaction {
getDirtyObject(identifier) { getDirtyObject(identifier) {
let dirtyObject; let dirtyObject;
this.dirtyObjects.forEach(object => {
Object.values(this.dirtyObjects).forEach(object => {
const areIdsEqual = this.objectAPI.areIdsEqual(object.identifier, identifier); const areIdsEqual = this.objectAPI.areIdsEqual(object.identifier, identifier);
if (areIdsEqual) { if (areIdsEqual) {
dirtyObject = object; dirtyObject = object;
@ -67,14 +73,11 @@ export default class Transaction {
return dirtyObject; return dirtyObject;
} }
start() {
this.dirtyObjects = new Set();
}
_clear() { _clear() {
const promiseArray = []; const promiseArray = [];
const refresh = this.objectAPI.refresh.bind(this.objectAPI); const refresh = this.objectAPI.refresh.bind(this.objectAPI);
this.dirtyObjects.forEach(object => {
Object.values(this.dirtyObjects).forEach(object => {
promiseArray.push(this.createDirtyObjectPromise(object, refresh)); promiseArray.push(this.createDirtyObjectPromise(object, refresh));
}); });

View File

@ -34,24 +34,24 @@ describe("Transaction Class", () => {
}); });
it('has no dirty objects', () => { it('has no dirty objects', () => {
expect(transaction.dirtyObjects.size).toEqual(0); expect(Object.keys(transaction.dirtyObjects).length).toEqual(0);
}); });
it('add(), adds object to dirtyObjects', () => { it('add(), adds object to dirtyObjects', () => {
const mockDomainObjects = createMockDomainObjects(); const mockDomainObjects = createMockDomainObjects();
transaction.add(mockDomainObjects[0]); transaction.add(mockDomainObjects[0]);
expect(transaction.dirtyObjects.size).toEqual(1); expect(Object.keys(transaction.dirtyObjects).length).toEqual(1);
}); });
it('cancel(), clears all dirtyObjects', (done) => { it('cancel(), clears all dirtyObjects', (done) => {
const mockDomainObjects = createMockDomainObjects(3); const mockDomainObjects = createMockDomainObjects(3);
mockDomainObjects.forEach(transaction.add.bind(transaction)); mockDomainObjects.forEach(transaction.add.bind(transaction));
expect(transaction.dirtyObjects.size).toEqual(3); expect(Object.keys(transaction.dirtyObjects).length).toEqual(3);
transaction.cancel() transaction.cancel()
.then(success => { .then(success => {
expect(transaction.dirtyObjects.size).toEqual(0); expect(Object.keys(transaction.dirtyObjects).length).toEqual(0);
}).finally(done); }).finally(done);
}); });
@ -59,12 +59,12 @@ describe("Transaction Class", () => {
const mockDomainObjects = createMockDomainObjects(3); const mockDomainObjects = createMockDomainObjects(3);
mockDomainObjects.forEach(transaction.add.bind(transaction)); mockDomainObjects.forEach(transaction.add.bind(transaction));
expect(transaction.dirtyObjects.size).toEqual(3); expect(Object.keys(transaction.dirtyObjects).length).toEqual(3);
spyOn(objectAPI, 'save').and.callThrough(); spyOn(objectAPI, 'save').and.callThrough();
transaction.commit() transaction.commit()
.then(success => { .then(success => {
expect(transaction.dirtyObjects.size).toEqual(0); expect(Object.keys(transaction.dirtyObjects).length).toEqual(0);
expect(objectAPI.save.calls.count()).toEqual(3); expect(objectAPI.save.calls.count()).toEqual(3);
}).finally(done); }).finally(done);
}); });
@ -73,7 +73,7 @@ describe("Transaction Class", () => {
const mockDomainObjects = createMockDomainObjects(); const mockDomainObjects = createMockDomainObjects();
transaction.add(mockDomainObjects[0]); transaction.add(mockDomainObjects[0]);
expect(transaction.dirtyObjects.size).toEqual(1); expect(Object.keys(transaction.dirtyObjects).length).toEqual(1);
const dirtyObject = transaction.getDirtyObject(mockDomainObjects[0].identifier); const dirtyObject = transaction.getDirtyObject(mockDomainObjects[0].identifier);
expect(dirtyObject).toEqual(mockDomainObjects[0]); expect(dirtyObject).toEqual(mockDomainObjects[0]);
@ -82,7 +82,7 @@ describe("Transaction Class", () => {
it('getDirtyObject(), returns empty dirtyObject for no active transaction', () => { it('getDirtyObject(), returns empty dirtyObject for no active transaction', () => {
const mockDomainObjects = createMockDomainObjects(); const mockDomainObjects = createMockDomainObjects();
expect(transaction.dirtyObjects.size).toEqual(0); expect(Object.keys(transaction.dirtyObjects).length).toEqual(0);
const dirtyObject = transaction.getDirtyObject(mockDomainObjects[0].identifier); const dirtyObject = transaction.getDirtyObject(mockDomainObjects[0].identifier);
expect(dirtyObject).toEqual(undefined); expect(dirtyObject).toEqual(undefined);

View File

@ -185,8 +185,8 @@ export class TelemetryCollection extends EventEmitter {
for (let datum of data) { for (let datum of data) {
parsedValue = this.parseTime(datum); parsedValue = this.parseTime(datum);
beforeStartOfBounds = parsedValue <= this.lastBounds.start; beforeStartOfBounds = parsedValue < this.lastBounds.start;
afterEndOfBounds = parsedValue >= this.lastBounds.end; afterEndOfBounds = parsedValue > this.lastBounds.end;
if (!afterEndOfBounds && !beforeStartOfBounds) { if (!afterEndOfBounds && !beforeStartOfBounds) {
let isDuplicate = false; let isDuplicate = false;

View File

@ -1,4 +1,3 @@
/***************************************************************************** /*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government * Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space * as represented by the Administrator of the National Aeronautics and Space
@ -114,14 +113,12 @@ export default {
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject); this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
this.formats = this.openmct.telemetry.getFormatMap(this.metadata); this.formats = this.openmct.telemetry.getFormatMap(this.metadata);
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier); this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.bounds = this.openmct.time.bounds();
this.limitEvaluator = this.openmct this.limitEvaluator = this.openmct
.telemetry .telemetry
.limitEvaluator(this.domainObject); .limitEvaluator(this.domainObject);
this.openmct.time.on('timeSystem', this.updateTimeSystem); this.openmct.time.on('timeSystem', this.updateTimeSystem);
this.openmct.time.on('bounds', this.updateBounds);
this.timestampKey = this.openmct.time.timeSystem().key; this.timestampKey = this.openmct.time.timeSystem().key;
@ -135,72 +132,41 @@ export default {
this.valueKey = this.valueMetadata ? this.valueMetadata.key : undefined; this.valueKey = this.valueMetadata ? this.valueMetadata.key : undefined;
this.unsubscribe = this.openmct this.telemetryCollection = this.openmct.telemetry.requestCollection(this.domainObject, {
.telemetry size: 1,
.subscribe(this.domainObject, this.setLatestValues); strategy: 'latest'
});
this.requestHistory(); this.telemetryCollection.on('add', this.setLatestValues);
this.telemetryCollection.on('clear', this.resetValues);
this.telemetryCollection.load();
if (this.hasUnits) { if (this.hasUnits) {
this.setUnit(); this.setUnit();
} }
}, },
destroyed() { destroyed() {
this.unsubscribe();
this.openmct.time.off('timeSystem', this.updateTimeSystem); this.openmct.time.off('timeSystem', this.updateTimeSystem);
this.openmct.time.off('bounds', this.updateBounds); this.telemetryCollection.off('add', this.setLatestValues);
this.telemetryCollection.off('clear', this.resetValues);
this.telemetryCollection.destroy();
}, },
methods: { methods: {
updateView() { updateView() {
if (!this.updatingView) { if (!this.updatingView) {
this.updatingView = true; this.updatingView = true;
requestAnimationFrame(() => { requestAnimationFrame(() => {
let newTimestamp = this.getParsedTimestamp(this.latestDatum); this.timestamp = this.getParsedTimestamp(this.latestDatum);
if (this.shouldUpdate(newTimestamp)) {
this.timestamp = newTimestamp;
this.datum = this.latestDatum; this.datum = this.latestDatum;
}
this.updatingView = false; this.updatingView = false;
}); });
} }
}, },
setLatestValues(datum) { setLatestValues(data) {
this.latestDatum = datum; this.latestDatum = data[data.length - 1];
this.updateView(); this.updateView();
}, },
shouldUpdate(newTimestamp) {
return this.inBounds(newTimestamp)
&& (this.timestamp === undefined || newTimestamp > this.timestamp);
},
requestHistory() {
this.openmct
.telemetry
.request(this.domainObject, {
start: this.bounds.start,
end: this.bounds.end,
size: 1,
strategy: 'latest'
})
.then((array) => this.setLatestValues(array[array.length - 1]))
.catch((error) => {
console.warn('Error fetching data', error);
});
},
updateBounds(bounds, isTick) {
this.bounds = bounds;
if (!isTick) {
this.resetValues();
this.requestHistory();
}
},
inBounds(timestamp) {
return timestamp >= this.bounds.start && timestamp <= this.bounds.end;
},
updateTimeSystem(timeSystem) { updateTimeSystem(timeSystem) {
this.resetValues();
this.timestampKey = timeSystem.key; this.timestampKey = timeSystem.key;
}, },
updateViewContext() { updateViewContext() {
@ -241,4 +207,3 @@ export default {
} }
}; };
</script> </script>

View File

@ -46,6 +46,7 @@ describe("The LAD Table", () => {
let openmct; let openmct;
let ladPlugin; let ladPlugin;
let historicalProvider;
let parent; let parent;
let child; let child;
let telemetryCount = 3; let telemetryCount = 3;
@ -81,6 +82,13 @@ describe("The LAD Table", () => {
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({})); spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({}));
historicalProvider = {
request: () => {
return Promise.resolve([]);
}
};
spyOn(openmct.telemetry, 'findRequestProvider').and.returnValue(historicalProvider);
openmct.time.bounds({ openmct.time.bounds({
start: bounds.start, start: bounds.start,
end: bounds.end end: bounds.end
@ -147,7 +155,7 @@ describe("The LAD Table", () => {
// add another telemetry object as composition in lad table to test multi rows // add another telemetry object as composition in lad table to test multi rows
mockObj.ladTable.composition.push(anotherTelemetryObj.identifier); mockObj.ladTable.composition.push(anotherTelemetryObj.identifier);
beforeEach(async () => { beforeEach(async (done) => {
let telemetryRequestResolve; let telemetryRequestResolve;
let telemetryObjectResolve; let telemetryObjectResolve;
let anotherTelemetryObjectResolve; let anotherTelemetryObjectResolve;
@ -166,11 +174,12 @@ describe("The LAD Table", () => {
callBack(); callBack();
}); });
openmct.telemetry.request.and.callFake(() => { historicalProvider.request = () => {
telemetryRequestResolve(mockTelemetry); telemetryRequestResolve(mockTelemetry);
return telemetryRequestPromise; return telemetryRequestPromise;
}); };
openmct.objects.get.and.callFake((obj) => { openmct.objects.get.and.callFake((obj) => {
if (obj.key === 'telemetry-object') { if (obj.key === 'telemetry-object') {
telemetryObjectResolve(mockObj.telemetry); telemetryObjectResolve(mockObj.telemetry);
@ -195,6 +204,8 @@ describe("The LAD Table", () => {
await Promise.all([telemetryRequestPromise, telemetryObjectPromise, anotherTelemetryObjectPromise]); await Promise.all([telemetryRequestPromise, telemetryObjectPromise, anotherTelemetryObjectPromise]);
await Vue.nextTick(); await Vue.nextTick();
done();
}); });
it("should show one row per object in the composition", () => { it("should show one row per object in the composition", () => {

View File

@ -27,7 +27,7 @@
:href="url" :href="url"
> >
<div class="c-condition-widget__label"> <div class="c-condition-widget__label">
{{ internalDomainObject.conditionalLabel || internalDomainObject.label }} {{ label }}
</div> </div>
</component> </component>
</template> </template>
@ -39,28 +39,112 @@ export default {
inject: ['openmct', 'domainObject'], inject: ['openmct', 'domainObject'],
data: function () { data: function () {
return { return {
internalDomainObject: this.domainObject conditionalLabel: '',
conditionSetIdentifier: null,
domainObjectLabel: '',
url: null,
urlDefined: false,
useConditionSetOutputAsLabel: false
}; };
}, },
computed: { computed: {
urlDefined() { label() {
return this.internalDomainObject.url && this.internalDomainObject.url.length > 0; return this.useConditionSetOutputAsLabel
? this.conditionalLabel
: this.domainObjectLabel
;
}
}, },
url() { watch: {
return this.urlDefined ? sanitizeUrl(this.internalDomainObject.url) : null; conditionSetIdentifier: {
handler(newValue, oldValue) {
if (!oldValue || !newValue || !this.openmct.objects.areIdsEqual(newValue, oldValue)) {
return;
}
this.listenToConditionSetChanges();
},
deep: true
} }
}, },
mounted() { mounted() {
this.unlisten = this.openmct.objects.observe(this.internalDomainObject, '*', this.updateInternalDomainObject); this.unlisten = this.openmct.objects.observe(this.domainObject, '*', this.updateDomainObject);
if (this.domainObject) {
this.updateDomainObject(this.domainObject);
this.listenToConditionSetChanges();
}
}, },
beforeDestroy() { beforeDestroy() {
this.conditionSetIdentifier = null;
if (this.unlisten) { if (this.unlisten) {
this.unlisten(); this.unlisten();
} }
this.stopListeningToConditionSetChanges();
}, },
methods: { methods: {
updateInternalDomainObject(domainObject) { async listenToConditionSetChanges() {
this.internalDomainObject = domainObject; if (!this.conditionSetIdentifier) {
return;
}
const conditionSetDomainObject = await this.openmct.objects.get(this.conditionSetIdentifier);
this.stopListeningToConditionSetChanges();
if (!conditionSetDomainObject) {
this.openmct.notifications.alert('Unable to find condition set');
}
this.telemetryCollection = this.openmct.telemetry.requestCollection(conditionSetDomainObject, {
size: 1,
strategy: 'latest'
});
this.telemetryCollection.on('add', this.updateConditionLabel, this);
this.telemetryCollection.load();
},
stopListeningToConditionSetChanges() {
if (this.telemetryCollection) {
this.telemetryCollection.off('add', this.updateConditionLabel, this);
this.telemetryCollection.destroy();
this.telemetryCollection = null;
}
},
updateConditionLabel([latestDatum]) {
if (!this.conditionSetIdentifier) {
this.stopListeningToConditionSetChanges();
return;
}
this.conditionalLabel = latestDatum.output || '';
},
updateDomainObject(domainObject) {
if (this.domainObjectLabel !== domainObject.label) {
this.domainObjectLabel = domainObject.label;
}
const urlDefined = domainObject.url && domainObject.url.length > 0;
if (this.urlDefined !== urlDefined) {
this.urlDefined = urlDefined;
}
const url = this.urlDefined ? sanitizeUrl(domainObject.url) : null;
if (this.url !== url) {
this.url = url;
}
const conditionSetIdentifier = domainObject.configuration.objectStyles.conditionSetIdentifier;
if (this.conditionSetIdentifier !== conditionSetIdentifier) {
this.conditionSetIdentifier = conditionSetIdentifier;
}
const useConditionSetOutputAsLabel = this.conditionSetIdentifier && domainObject.configuration.useConditionSetOutputAsLabel;
if (this.useConditionSetOutputAsLabel !== useConditionSetOutputAsLabel) {
this.useConditionSetOutputAsLabel = useConditionSetOutputAsLabel;
}
} }
} }
}; };

View File

@ -222,20 +222,20 @@ export default {
.then(this.setObject); .then(this.setObject);
} }
this.openmct.time.on("bounds", this.refreshData);
this.status = this.openmct.status.get(this.item.identifier); this.status = this.openmct.status.get(this.item.identifier);
this.removeStatusListener = this.openmct.status.observe(this.item.identifier, this.setStatus); this.removeStatusListener = this.openmct.status.observe(this.item.identifier, this.setStatus);
}, },
beforeDestroy() { beforeDestroy() {
this.removeSubscription();
this.removeStatusListener(); this.removeStatusListener();
if (this.removeSelectable) { if (this.removeSelectable) {
this.removeSelectable(); this.removeSelectable();
} }
this.openmct.time.off("bounds", this.refreshData); this.telemetryCollection.off('add', this.setLatestValues);
this.telemetryCollection.off('clear', this.refreshData);
this.telemetryCollection.destroy();
if (this.mutablePromise) { if (this.mutablePromise) {
this.mutablePromise.then(() => { this.mutablePromise.then(() => {
@ -253,34 +253,9 @@ export default {
return `At ${timeFormatter.format(this.datum)} ${this.domainObject.name} had a value of ${this.telemetryValue}${unit}`; return `At ${timeFormatter.format(this.datum)} ${this.domainObject.name} had a value of ${this.telemetryValue}${unit}`;
}, },
requestHistoricalData() { setLatestValues(data) {
let bounds = this.openmct.time.bounds();
let options = {
start: bounds.start,
end: bounds.end,
size: 1,
strategy: 'latest'
};
this.openmct.telemetry.request(this.domainObject, options)
.then(data => {
if (data.length > 0) {
this.latestDatum = data[data.length - 1]; this.latestDatum = data[data.length - 1];
this.updateView(); this.updateView();
}
});
},
subscribeToObject() {
this.subscription = this.openmct.telemetry.subscribe(this.domainObject, function (datum) {
const key = this.openmct.time.timeSystem().key;
const datumTimeStamp = datum[key];
if (this.openmct.time.clock() !== undefined
|| (datumTimeStamp
&& (this.openmct.time.bounds().end >= datumTimeStamp))
) {
this.latestDatum = datum;
this.updateView();
}
}.bind(this));
}, },
updateView() { updateView() {
if (!this.updatingView) { if (!this.updatingView) {
@ -291,17 +266,10 @@ export default {
}); });
} }
}, },
removeSubscription() {
if (this.subscription) {
this.subscription();
this.subscription = undefined;
}
},
refreshData(bounds, isTick) { refreshData(bounds, isTick) {
if (!isTick) { if (!isTick) {
this.latestDatum = undefined; this.latestDatum = undefined;
this.updateView(); this.updateView();
this.requestHistoricalData(this.domainObject);
} }
}, },
setObject(domainObject) { setObject(domainObject) {
@ -315,8 +283,13 @@ export default {
const valueMetadata = this.metadata.value(this.item.value); const valueMetadata = this.metadata.value(this.item.value);
this.customStringformatter = this.openmct.telemetry.customStringFormatter(valueMetadata, this.item.format); this.customStringformatter = this.openmct.telemetry.customStringFormatter(valueMetadata, this.item.format);
this.requestHistoricalData(); this.telemetryCollection = this.openmct.telemetry.requestCollection(this.domainObject, {
this.subscribeToObject(); size: 1,
strategy: 'latest'
});
this.telemetryCollection.on('add', this.setLatestValues);
this.telemetryCollection.on('clear', this.refreshData);
this.telemetryCollection.load();
this.currentObjectPath = this.objectPath.slice(); this.currentObjectPath = this.objectPath.slice();
this.currentObjectPath.unshift(this.domainObject); this.currentObjectPath.unshift(this.domainObject);

View File

@ -53,6 +53,8 @@ describe('Gauge plugin', () => {
openmct = createOpenMct(); openmct = createOpenMct();
openmct.on('start', done); openmct.on('start', done);
openmct.install(openmct.plugins.Gauge());
openmct.startHeadless(); openmct.startHeadless();
}); });
@ -190,28 +192,27 @@ describe('Gauge plugin', () => {
}); });
it('renders gauge element', () => { it('renders gauge element', () => {
const gaugeElement = gaugeHolder.querySelectorAll('.c-gauge'); const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper');
expect(gaugeElement.length).toBe(1); expect(gaugeElement.length).toBe(1);
}); });
it('renders major elements', () => { it('renders major elements', () => {
const wrapperElement = gaugeHolder.querySelector('.c-gauge__wrapper'); const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper');
const rangeElement = gaugeHolder.querySelector('.c-gauge__range'); const rangeElement = gaugeHolder.querySelector('.js-gauge-dial-range');
const curveElement = gaugeHolder.querySelector('.c-gauge__curval'); const valueElement = gaugeHolder.querySelector('.js-dial-current-value');
const dialElement = gaugeHolder.querySelector('.c-dial');
const hasMajorElements = Boolean(wrapperElement && rangeElement && curveElement && dialElement); const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement);
expect(hasMajorElements).toBe(true); expect(hasMajorElements).toBe(true);
}); });
it('renders correct min max values', () => { it('renders correct min max values', () => {
expect(gaugeHolder.querySelector('.c-gauge__range').textContent).toEqual(`${minValue} ${maxValue}`); expect(gaugeHolder.querySelector('.js-gauge-dial-range').textContent).toEqual(`${minValue} ${maxValue}`);
}); });
it('renders correct current value', (done) => { it('renders correct current value', (done) => {
function WatchUpdateValue() { function WatchUpdateValue() {
const textElement = gaugeHolder.querySelector('.c-gauge__curval-text'); const textElement = gaugeHolder.querySelector('.js-dial-current-value');
expect(Number(textElement.textContent).toFixed(gaugeViewObject.configuration.gaugeController.precision)).toBe(randomValue.toFixed(gaugeViewObject.configuration.gaugeController.precision)); expect(Number(textElement.textContent).toFixed(gaugeViewObject.configuration.gaugeController.precision)).toBe(randomValue.toFixed(gaugeViewObject.configuration.gaugeController.precision));
done(); done();
} }
@ -326,28 +327,27 @@ describe('Gauge plugin', () => {
}); });
it('renders gauge element', () => { it('renders gauge element', () => {
const gaugeElement = gaugeHolder.querySelectorAll('.c-gauge'); const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper');
expect(gaugeElement.length).toBe(1); expect(gaugeElement.length).toBe(1);
}); });
it('renders major elements', () => { it('renders major elements', () => {
const wrapperElement = gaugeHolder.querySelector('.c-gauge__wrapper'); const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper');
const rangeElement = gaugeHolder.querySelector('.c-gauge__range'); const rangeElement = gaugeHolder.querySelector('.js-gauge-dial-range');
const curveElement = gaugeHolder.querySelector('.c-gauge__curval'); const valueElement = gaugeHolder.querySelector('.js-dial-current-value');
const dialElement = gaugeHolder.querySelector('.c-dial');
const hasMajorElements = Boolean(wrapperElement && rangeElement && curveElement && dialElement); const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement);
expect(hasMajorElements).toBe(true); expect(hasMajorElements).toBe(true);
}); });
it('renders correct min max values', () => { it('renders correct min max values', () => {
expect(gaugeHolder.querySelector('.c-gauge__range').textContent).toEqual(`${minValue} ${maxValue}`); expect(gaugeHolder.querySelector('.js-gauge-dial-range').textContent).toEqual(`${minValue} ${maxValue}`);
}); });
it('renders correct current value', (done) => { it('renders correct current value', (done) => {
function WatchUpdateValue() { function WatchUpdateValue() {
const textElement = gaugeHolder.querySelector('.c-gauge__curval-text'); const textElement = gaugeHolder.querySelector('.js-dial-current-value');
expect(Number(textElement.textContent).toFixed(gaugeViewObject.configuration.gaugeController.precision)).toBe(randomValue.toFixed(gaugeViewObject.configuration.gaugeController.precision)); expect(Number(textElement.textContent).toFixed(gaugeViewObject.configuration.gaugeController.precision)).toBe(randomValue.toFixed(gaugeViewObject.configuration.gaugeController.precision));
done(); done();
} }
@ -462,28 +462,27 @@ describe('Gauge plugin', () => {
}); });
it('renders gauge element', () => { it('renders gauge element', () => {
const gaugeElement = gaugeHolder.querySelectorAll('.c-gauge'); const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper');
expect(gaugeElement.length).toBe(1); expect(gaugeElement.length).toBe(1);
}); });
it('renders major elements', () => { it('renders major elements', () => {
const wrapperElement = gaugeHolder.querySelector('.c-gauge__wrapper'); const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper');
const rangeElement = gaugeHolder.querySelector('.c-gauge__range'); const rangeElement = gaugeHolder.querySelector('.js-gauge-meter-range');
const curveElement = gaugeHolder.querySelector('.c-meter'); const valueElement = gaugeHolder.querySelector('.js-meter-current-value');
const dialElement = gaugeHolder.querySelector('.c-meter__bg');
const hasMajorElements = Boolean(wrapperElement && rangeElement && curveElement && dialElement); const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement);
expect(hasMajorElements).toBe(true); expect(hasMajorElements).toBe(true);
}); });
it('renders correct min max values', () => { it('renders correct min max values', () => {
expect(gaugeHolder.querySelector('.c-gauge__range').textContent).toEqual(`${maxValue} ${minValue}`); expect(gaugeHolder.querySelector('.js-gauge-meter-range').textContent).toEqual(`${maxValue} ${minValue}`);
}); });
it('renders correct current value', (done) => { it('renders correct current value', (done) => {
function WatchUpdateValue() { function WatchUpdateValue() {
const textElement = gaugeHolder.querySelector('.c-gauge__curval-text'); const textElement = gaugeHolder.querySelector('.js-meter-current-value');
expect(Number(textElement.textContent).toFixed(gaugeViewObject.configuration.gaugeController.precision)).toBe(randomValue.toFixed(gaugeViewObject.configuration.gaugeController.precision)); expect(Number(textElement.textContent).toFixed(gaugeViewObject.configuration.gaugeController.precision)).toBe(randomValue.toFixed(gaugeViewObject.configuration.gaugeController.precision));
done(); done();
} }
@ -560,17 +559,16 @@ describe('Gauge plugin', () => {
}); });
it('renders gauge element', () => { it('renders gauge element', () => {
const gaugeElement = gaugeHolder.querySelectorAll('.c-gauge'); const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper');
expect(gaugeElement.length).toBe(1); expect(gaugeElement.length).toBe(1);
}); });
it('renders major elements', () => { it('renders major elements', () => {
const wrapperElement = gaugeHolder.querySelector('.c-gauge__wrapper'); const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper');
const rangeElement = gaugeHolder.querySelector('.c-gauge__range'); const rangeElement = gaugeHolder.querySelector('.js-gauge-meter-range');
const curveElement = gaugeHolder.querySelector('.c-meter'); const valueElement = gaugeHolder.querySelector('.js-meter-current-value');
const dialElement = gaugeHolder.querySelector('.c-meter__bg');
const hasMajorElements = Boolean(wrapperElement && rangeElement && curveElement && dialElement); const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement);
expect(hasMajorElements).toBe(true); expect(hasMajorElements).toBe(true);
}); });
@ -643,17 +641,16 @@ describe('Gauge plugin', () => {
}); });
it('renders gauge element', () => { it('renders gauge element', () => {
const gaugeElement = gaugeHolder.querySelectorAll('.c-gauge'); const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper');
expect(gaugeElement.length).toBe(1); expect(gaugeElement.length).toBe(1);
}); });
it('renders major elements', () => { it('renders major elements', () => {
const wrapperElement = gaugeHolder.querySelector('.c-gauge__wrapper'); const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper');
const rangeElement = gaugeHolder.querySelector('.c-gauge__range'); const rangeElement = gaugeHolder.querySelector('.c-gauge__range');
const curveElement = gaugeHolder.querySelector('.c-meter'); const curveElement = gaugeHolder.querySelector('.c-meter');
const dialElement = gaugeHolder.querySelector('.c-meter__bg');
const hasMajorElements = Boolean(wrapperElement && rangeElement && curveElement && dialElement); const hasMajorElements = Boolean(wrapperElement && rangeElement && curveElement);
expect(hasMajorElements).toBe(true); expect(hasMajorElements).toBe(true);
}); });
@ -772,28 +769,27 @@ describe('Gauge plugin', () => {
}); });
it('renders gauge element', () => { it('renders gauge element', () => {
const gaugeElement = gaugeHolder.querySelectorAll('.c-gauge'); const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper');
expect(gaugeElement.length).toBe(1); expect(gaugeElement.length).toBe(1);
}); });
it('renders major elements', () => { it('renders major elements', () => {
const wrapperElement = gaugeHolder.querySelector('.c-gauge__wrapper'); const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper');
const rangeElement = gaugeHolder.querySelector('.c-gauge__range'); const rangeElement = gaugeHolder.querySelector('.js-gauge-dial-range');
const curveElement = gaugeHolder.querySelector('.c-gauge__curval'); const valueElement = gaugeHolder.querySelector('.js-dial-current-value');
const dialElement = gaugeHolder.querySelector('.c-dial');
const hasMajorElements = Boolean(wrapperElement && rangeElement && curveElement && dialElement); const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement);
expect(hasMajorElements).toBe(true); expect(hasMajorElements).toBe(true);
}); });
it('renders correct min max values', () => { it('renders correct min max values', () => {
expect(gaugeHolder.querySelector('.c-gauge__range').textContent).toEqual(`${gaugeViewObject.configuration.gaugeController.min} ${gaugeViewObject.configuration.gaugeController.max}`); expect(gaugeHolder.querySelector('.js-gauge-dial-range').textContent).toEqual(`${gaugeViewObject.configuration.gaugeController.min} ${gaugeViewObject.configuration.gaugeController.max}`);
}); });
it('renders correct current value', (done) => { it('renders correct current value', (done) => {
function WatchUpdateValue() { function WatchUpdateValue() {
const textElement = gaugeHolder.querySelector('.c-gauge__curval-text'); const textElement = gaugeHolder.querySelector('.js-dial-current-value');
expect(Number(textElement.textContent).toFixed(gaugeViewObject.configuration.gaugeController.precision)).toBe(randomValue.toFixed(gaugeViewObject.configuration.gaugeController.precision)); expect(Number(textElement.textContent).toFixed(gaugeViewObject.configuration.gaugeController.precision)).toBe(randomValue.toFixed(gaugeViewObject.configuration.gaugeController.precision));
done(); done();
} }

View File

@ -21,13 +21,33 @@
*****************************************************************************/ *****************************************************************************/
<template> <template>
<div <div
class="c-gauge" class="c-gauge__wrapper js-gauge-wrapper"
:class="`c-gauge--${gaugeType}`" :class="`c-gauge--${gaugeType}`"
> >
<div class="c-gauge__wrapper">
<template v-if="typeDial"> <template v-if="typeDial">
<svg <svg
class="c-gauge__range" width="0"
height="0"
class="c-dial__clip-paths"
>
<defs>
<clipPath
id="gaugeBgMask"
clipPathUnits="objectBoundingBox"
>
<path d="M0.853553 0.853553C0.944036 0.763071 1 0.638071 1 0.5C1 0.223858 0.776142 0 0.5 0C0.223858 0 0 0.223858 0 0.5C0 0.638071 0.0559644 0.763071 0.146447 0.853553L0.285934 0.714066C0.23115 0.659281 0.197266 0.583598 0.197266 0.5C0.197266 0.332804 0.332804 0.197266 0.5 0.197266C0.667196 0.197266 0.802734 0.332804 0.802734 0.5C0.802734 0.583598 0.76885 0.659281 0.714066 0.714066L0.853553 0.853553Z" />
</clipPath>
<clipPath
id="gaugeValueMask"
clipPathUnits="objectBoundingBox"
>
<path d="M0.18926 0.81074C0.109735 0.731215 0.0605469 0.621351 0.0605469 0.5C0.0605469 0.257298 0.257298 0.0605469 0.5 0.0605469C0.742702 0.0605469 0.939453 0.257298 0.939453 0.5C0.939453 0.621351 0.890265 0.731215 0.81074 0.81074L0.714066 0.714066C0.76885 0.659281 0.802734 0.583599 0.802734 0.5C0.802734 0.332804 0.667196 0.197266 0.5 0.197266C0.332804 0.197266 0.197266 0.332804 0.197266 0.5C0.197266 0.583599 0.23115 0.659281 0.285934 0.714066L0.18926 0.81074Z" />
</clipPath>
</defs>
</svg>
<svg
class="c-dial__range c-gauge__range js-gauge-dial-range"
viewBox="0 0 512 512" viewBox="0 0 512 512"
> >
<text <text
@ -43,83 +63,147 @@
>{{ rangeHigh }}</text> >{{ rangeHigh }}</text>
</svg> </svg>
<svg
class="c-dial__current-value-text-wrapper"
viewBox="0 0 512 512"
>
<svg <svg
v-if="displayCurVal" v-if="displayCurVal"
class="c-gauge__curval" class="c-dial__current-value-text-sizer"
:viewBox="curValViewBox" :viewBox="curValViewBox"
> >
<text <text
class="c-gauge__curval-text" class="c-dial__current-value-text js-dial-current-value"
lengthAdjust="spacing" lengthAdjust="spacing"
text-anchor="middle" text-anchor="middle"
style="transform: translate(50%, 70%)"
>{{ curVal }}</text> >{{ curVal }}</text>
</svg> </svg>
</svg>
<div class="c-dial">
<svg <svg
class="c-dial__bg" class="c-dial__bg"
viewBox="0 0 512 512" viewBox="0 0 10 10"
> >
<path d="M256,0C114.6,0,0,114.6,0,256S114.6,512,256,512,512,397.4,512,256,397.4,0,256,0Zm0,412A156,156,0,1,1,412,256,155.9,155.9,0,0,1,256,412Z" />
</svg>
<svg <g
v-if="limitHigh && dialHighLimitDeg < 270" v-if="limitLow !== null && dialLowLimitDeg < getLimitDegree('low', 'max')"
class="c-dial__limit-high"
viewBox="0 0 512 512"
:class="{
'c-high-limit-clip--90': dialHighLimitDeg > 90,
'c-high-limit-clip--180': dialHighLimitDeg >= 180
}"
>
<path
d="M100,256A156,156,0,1,1,366.3,366.3L437,437a255.2,255.2,0,0,0,75-181C512,114.6,397.4,0,256,0S0,114.6,0,256A255.2,255.2,0,0,0,75,437l70.7-70.7A155.5,155.5,0,0,1,100,256Z"
:style="`transform: rotate(${dialHighLimitDeg}deg)`"
/>
</svg>
<svg
v-if="limitLow && dialLowLimitDeg < 270"
class="c-dial__limit-low" class="c-dial__limit-low"
viewBox="0 0 512 512"
:class="{
'c-dial-clip--90': dialLowLimitDeg < 90,
'c-dial-clip--180': dialLowLimitDeg >= 90 && dialLowLimitDeg < 180
}"
>
<path
d="M256,100c86.2,0,156,69.8,156,156s-69.8,156-156,156c-43.1,0-82.1-17.5-110.3-45.7L75,437 c46.3,46.3,110.3,75,181,75c141.4,0,256-114.6,256-256S397.4,0,256,0C185.3,0,121.3,28.7,75,75l70.7,70.7 C173.9,117.5,212.9,100,256,100z"
:style="`transform: rotate(${dialLowLimitDeg}deg)`" :style="`transform: rotate(${dialLowLimitDeg}deg)`"
>
<rect
v-if="dialLowLimitDeg >= getLimitDegree('low', 'q1')"
class="c-dial__low-limit__low"
x="5"
y="5"
width="5"
height="5"
/> />
<rect
v-if="dialLowLimitDeg >= getLimitDegree('low', 'q2')"
class="c-dial__low-limit__mid"
x="5"
y="0"
width="5"
height="5"
/>
<rect
v-if="dialLowLimitDeg >= getLimitDegree('low', 'q3')"
class="c-dial__low-limit__high"
x="0"
y="0"
width="5"
height="5"
/>
</g>
<g
v-if="limitHigh !== null && dialHighLimitDeg < getLimitDegree('high', 'max')"
class="c-dial__limit-high"
:style="`transform: rotate(${dialHighLimitDeg}deg)`"
>
<rect
v-if="dialHighLimitDeg <= getLimitDegree('high', 'max')"
class="c-dial__high-limit__low"
x="0"
y="5"
width="5"
height="5"
/>
<rect
v-if="dialHighLimitDeg <= getLimitDegree('high', 'q2')"
class="c-dial__high-limit__mid"
x="0"
y="0"
width="5"
height="5"
/>
<rect
v-if="dialHighLimitDeg <= getLimitDegree('high', 'q3')"
class="c-dial__high-limit__high"
x="5"
y="0"
width="5"
height="5"
/>
</g>
</svg> </svg>
<svg <svg
class="c-dial__value" v-if="typeFilledDial"
viewBox="0 0 512 512" class="c-dial__filled-value-wrapper"
:class="{ viewBox="0 0 10 10"
'c-dial-clip--90': degValue < 90 && typeFilledDial,
'c-dial-clip--180': degValue >= 90 && degValue < 180 && typeFilledDial
}"
> >
<path <g
v-if="typeFilledDial && degValue > 0" class="c-dial__filled-value"
d="M256,31A224.3,224.3,0,0,0,98.3,95.5l48.4,49.2a156,156,0,1,1-1,221.6L96.9,415.1A224.4,224.4,0,0,0,256,481c124.3,0,225-100.7,225-225S380.3,31,256,31Z" :style="`transform: rotate(${degValueFilledDial}deg)`"
:style="`transform: rotate(${degValue}deg)`" >
<rect
v-if="degValue >= getLimitDegree('low', 'q1')"
class="c-dial__filled-value__low"
x="5"
y="5"
width="5"
height="5"
/> />
<path <rect
v-if="typeNeedleDial && valueInBounds" v-if="degValue >= getLimitDegree('low', 'q2')"
d="M256,86c-93.9,0-170,76.1-170,170c0,43.9,16.6,83.9,43.9,114.1l-38.7,38.7c-3.3,3.3-3.3,8.7,0,12s8.7,3.3,12,0 l38.7-38.7C172.1,409.4,212.1,426,256,426c93.9,0,170-76.1,170-170S349.9,86,256,86z M256,411.7c-86,0-155.7-69.7-155.7-155.7 S170,100.3,256,100.3S411.7,170,411.7,256S342,411.7,256,411.7z" class="c-dial__filled-value__mid"
:style="`transform: rotate(${degValue}deg)`" x="5"
y="0"
width="5"
height="5"
/> />
<rect
v-if="degValue >= getLimitDegree('low', 'q3')"
class="c-dial__filled-value__high"
x="0"
y="0"
width="5"
height="5"
/>
</g>
</svg>
<svg
v-if="valueInBounds && typeNeedleDial"
class="c-dial__needle-value-wrapper"
viewBox="0 0 10 10"
>
<g
class="c-dial__needle-value"
:style="`transform: rotate(${degValue}deg)`"
>
<path d="M4.90234 9.39453L5.09766 9.39453L5.30146 8.20874C6.93993 8.05674 8.22265 6.67817 8.22266 5C8.22266 3.22018 6.77982 1.77734 5 1.77734C3.22018 1.77734 1.77734 3.22018 1.77734 5C1.77734 6.67817 3.06007 8.05674 4.69854 8.20874L4.90234 9.39453Z" />
</g>
</svg> </svg>
</div>
</template> </template>
<template v-if="typeMeter"> <template v-if="typeMeter">
<div class="c-meter"> <div class="c-meter">
<div <div
v-if="displayMinMax" v-if="displayMinMax"
class="c-gauge__range c-meter__range" class="c-gauge__range c-meter__range js-gauge-meter-range"
> >
<div class="c-meter__range__high">{{ rangeHigh }}</div> <div class="c-meter__range__high">{{ rangeHigh }}</div>
<div class="c-meter__range__low">{{ rangeLow }}</div> <div class="c-meter__range__low">{{ rangeLow }}</div>
@ -132,13 +216,13 @@
></div> ></div>
<div <div
v-if="limitHigh && meterHighLimitPerc > 0" v-if="limitHigh !== null && meterHighLimitPerc > 0"
class="c-meter__limit-high" class="c-meter__limit-high"
:style="`height: ${meterHighLimitPerc}%`" :style="`height: ${meterHighLimitPerc}%`"
></div> ></div>
<div <div
v-if="limitLow && meterLowLimitPerc > 0" v-if="limitLow !== null && meterLowLimitPerc > 0"
class="c-meter__limit-low" class="c-meter__limit-low"
:style="`height: ${meterLowLimitPerc}%`" :style="`height: ${meterLowLimitPerc}%`"
></div> ></div>
@ -151,38 +235,45 @@
></div> ></div>
<div <div
v-if="limitHigh && meterHighLimitPerc > 0" v-if="limitHigh !== null && meterHighLimitPerc > 0"
class="c-meter__limit-high" class="c-meter__limit-high"
:style="`width: ${meterHighLimitPerc}%`" :style="`width: ${meterHighLimitPerc}%`"
></div> ></div>
<div <div
v-if="limitLow && meterLowLimitPerc > 0" v-if="limitLow !== null && meterLowLimitPerc > 0"
class="c-meter__limit-low" class="c-meter__limit-low"
:style="`width: ${meterLowLimitPerc}%`" :style="`width: ${meterLowLimitPerc}%`"
></div> ></div>
</template> </template>
<svg
class="c-meter__current-value-text-wrapper"
viewBox="0 0 512 512"
>
<svg <svg
v-if="displayCurVal" v-if="displayCurVal"
class="c-gauge__curval" class="c-meter__current-value-text-sizer"
:viewBox="curValViewBox" :viewBox="curValViewBox"
preserveAspectRatio="xMidYMid meet" preserveAspectRatio="xMidYMid meet"
> >
<text <text
class="c-gauge__curval-text" class="c-dial__current-value-text js-meter-current-value"
text-anchor="middle"
lengthAdjust="spacing" lengthAdjust="spacing"
text-anchor="middle"
style="transform: translate(50%, 70%)"
>{{ curVal }}</text> >{{ curVal }}</text>
</svg> </svg>
</svg>
</div> </div>
</div> </div>
</template> </template>
</div>
</div> </div>
</template> </template>
<script> <script>
import { DIAL_VALUE_DEG_OFFSET, getLimitDegree } from '../gauge-limit-util';
const LIMIT_PADDING_IN_PERCENT = 10; const LIMIT_PADDING_IN_PERCENT = 10;
export default { export default {
@ -209,6 +300,13 @@ export default {
degValue() { degValue() {
return this.percentToDegrees(this.valToPercent(this.curVal)); return this.percentToDegrees(this.valToPercent(this.curVal));
}, },
degValueFilledDial() {
if (this.curVal > this.rangeHigh) {
return this.percentToDegrees(100);
}
return this.percentToDegrees(this.valToPercent(this.curVal));
},
dialHighLimitDeg() { dialHighLimitDeg() {
return this.percentToDegrees(this.valToPercent(this.limitHigh)); return this.percentToDegrees(this.valToPercent(this.limitHigh));
}, },
@ -299,6 +397,7 @@ export default {
this.openmct.time.off('timeSystem', this.setTimeSystem); this.openmct.time.off('timeSystem', this.setTimeSystem);
}, },
methods: { methods: {
getLimitDegree: getLimitDegree,
addTelemetryObjectAndSubscribe(domainObject) { addTelemetryObjectAndSubscribe(domainObject) {
this.telemetryObject = domainObject; this.telemetryObject = domainObject;
this.request(); this.request();
@ -340,7 +439,7 @@ export default {
return this.gaugeType.indexOf(str) !== -1; return this.gaugeType.indexOf(str) !== -1;
}, },
percentToDegrees(vPercent) { percentToDegrees(vPercent) {
return this.round((vPercent / 100) * 270, 2); return this.round(((vPercent / 100) * 270) + DIAL_VALUE_DEG_OFFSET, 2);
}, },
removeFromComposition(telemetryObject = this.telemetryObject) { removeFromComposition(telemetryObject = this.telemetryObject) {
let composition = this.domainObject.composition.filter(id => let composition = this.domainObject.composition.filter(id =>
@ -453,7 +552,7 @@ export default {
valToPercent(vValue) { valToPercent(vValue) {
// Used by dial // Used by dial
if (vValue >= this.rangeHigh && this.typeFilledDial) { if (vValue >= this.rangeHigh && this.typeFilledDial) {
// Don't peg at 100% if the gaugeType isn't a filled shape // For filled dial, clip values over the high range to prevent over-rotation
return 100; return 100;
} }

View File

@ -27,6 +27,7 @@
:class="model.cssClass" :class="model.cssClass"
> >
<ToggleSwitch <ToggleSwitch
:id="'gaugeToggle'"
:checked="isUseTelemetryLimits" :checked="isUseTelemetryLimits"
label="Use telemetry limits for minimum and maximum ranges" label="Use telemetry limits for minimum and maximum ranges"
@change="toggleUseTelemetryLimits" @change="toggleUseTelemetryLimits"

View File

@ -0,0 +1,39 @@
const GAUGE_LIMITS = {
q1: 0,
q2: 90,
q3: 180,
q4: 270
};
export const DIAL_VALUE_DEG_OFFSET = 45;
// type: low, high
// quadrant: low, mid, high, max
export function getLimitDegree(type, quadrant) {
if (quadrant === 'max') {
return GAUGE_LIMITS.q4 + DIAL_VALUE_DEG_OFFSET;
}
return type === 'low'
? getLowLimitDegree(quadrant)
: getHighLimitDegree(quadrant)
;
}
function getLowLimitDegree(quadrant) {
return GAUGE_LIMITS[quadrant] + DIAL_VALUE_DEG_OFFSET;
}
function getHighLimitDegree(quadrant) {
if (quadrant === 'q1') {
return GAUGE_LIMITS.q4 + DIAL_VALUE_DEG_OFFSET;
}
if (quadrant === 'q2') {
return GAUGE_LIMITS.q3 + DIAL_VALUE_DEG_OFFSET;
}
if (quadrant === 'q3') {
return GAUGE_LIMITS.q2 + DIAL_VALUE_DEG_OFFSET;
}
}

View File

@ -1,9 +1,3 @@
$dialClip: polygon(0 0, 100% 0, 100% 100%, 50% 50%, 0 100%);
$dialClip90: polygon(0 0, 50% 50%, 0 100%);
$dialClip180: polygon(0 0, 100% 0, 0 100%);
$limitHighClip90: polygon(0 0, 100% 0, 100% 100%);
$limitHighClip180: polygon(100% 0, 100% 100%, 0 100%);
.is-object-type-gauge { .is-object-type-gauge {
overflow: hidden; overflow: hidden;
} }
@ -13,10 +7,8 @@ $limitHighClip180: polygon(100% 0, 100% 100%, 0 100%);
&.invalid, &.invalid,
&.invalid.req { @include validationState($glyph-icon-x, $colorFormInvalid); } &.invalid.req { @include validationState($glyph-icon-x, $colorFormInvalid); }
&.valid, &.valid,
&.valid.req { @include validationState($glyph-icon-check, $colorFormValid); } &.valid.req { @include validationState($glyph-icon-check, $colorFormValid); }
&.req { @include validationState($glyph-icon-asterisk, $colorFormRequired); } &.req { @include validationState($glyph-icon-asterisk, $colorFormRequired); }
} }
@ -37,92 +29,47 @@ $limitHighClip180: polygon(100% 0, 100% 100%, 0 100%);
@include abs(); @include abs();
overflow: hidden; overflow: hidden;
} }
svg {
path {
transform-origin: center;
}
&.c-gauge__curval {
@include abs();
fill: $colorGaugeTextValue;
position: absolute;
width: 100%;
height: 100%;
z-index: 2;
.c-gauge__curval-text {
font-family: $heroFont;
transform: translate(50%, 75%);
}
}
}
&[class*='dial'] {
// Square aspect ratio
width: 100%;
padding-bottom: 100%;
}
&[class*='meter'] {
@include abs();
}
} }
/********************************************** DIAL GAUGE */ /********************************************** DIAL GAUGE */
.c-dial { svg[class*='c-dial'] {
// Dial elements max-height: 100%;
@include abs(); max-width: 100%;
clip-path: $dialClip; position: absolute;
svg, g {
&__ticks, transform-origin: center;
&__bg,
&[class*='__limit'],
&__value {
@include abs();
}
.c-high-limit-clip--90 {
clip-path: $limitHighClip90;
}
.c-high-limit-clip--180 {
clip-path: $limitHighClip180;
}
&__limit-high path { fill: $colorGaugeLimitHigh; }
&__limit-low path { fill: $colorGaugeLimitLow; }
&__value,
&__limit-low {
&.c-dial-clip--90 {
clip-path: $dialClip90;
}
&.c-dial-clip--180 {
clip-path: $dialClip180;
}
}
&__value {
path,
polygon {
fill: $colorGaugeValue;
}
}
&__bg {
path {
fill: $colorGaugeBg;
}
} }
} }
.c-gauge--dial-needle .c-dial__value { .c-dial {
path { &__bg {
background: $colorGaugeBg;
clip-path: url(#gaugeBgMask);
}
&__limit-high rect { fill: $colorGaugeLimitHigh; }
&__limit-low rect { fill: $colorGaugeLimitLow; }
&__filled-value-wrapper {
clip-path: url(#gaugeValueMask);
}
&__needle-value-wrapper {
clip-path: url(#gaugeValueMask);
}
&__filled-value { fill: $colorGaugeValue; }
&__needle-value {
fill: $colorGaugeValue;
transition: transform $transitionTimeGauge; transition: transform $transitionTimeGauge;
} }
&__current-value-text {
fill: $colorGaugeTextValue;
font-family: $heroFont;
}
} }
/********************************************** METER GAUGE */ /********************************************** METER GAUGE */
@ -131,6 +78,13 @@ $limitHighClip180: polygon(100% 0, 100% 100%, 0 100%);
@include abs(); @include abs();
display: flex; display: flex;
svg {
// current-value-text
position: absolute;
height: 100%;
width: 100%;
}
&__range { &__range {
display: flex; display: flex;
flex: 0 0 auto; flex: 0 0 auto;

View File

@ -143,13 +143,13 @@
<!-- spacecraft position fresh --> <!-- spacecraft position fresh -->
<div <div
v-if="relatedTelemetry.hasRelatedTelemetry && isSpacecraftPositionFresh" v-if="relatedTelemetry.hasRelatedTelemetry && isSpacecraftPositionFresh"
class="c-imagery__age icon-check c-imagery--new" class="c-imagery__age icon-check c-imagery--new no-animation"
>POS</div> >POS</div>
<!-- camera position fresh --> <!-- camera position fresh -->
<div <div
v-if="relatedTelemetry.hasRelatedTelemetry && isCameraPositionFresh" v-if="relatedTelemetry.hasRelatedTelemetry && isCameraPositionFresh"
class="c-imagery__age icon-check c-imagery--new" class="c-imagery__age icon-check c-imagery--new no-animation"
>CAM</div> >CAM</div>
</div> </div>
<div class="h-local-controls"> <div class="h-local-controls">
@ -331,6 +331,16 @@ export default {
}, },
isImageNew() { isImageNew() {
let cutoff = FIVE_MINUTES; let cutoff = FIVE_MINUTES;
if (this.imageFreshnessOptions) {
const { fadeOutDelayTime, fadeOutDurationTime} = this.imageFreshnessOptions;
// convert css duration to IS8601 format for parsing
const isoFormattedDuration = 'PT' + fadeOutDurationTime.toUpperCase();
const isoFormattedDelay = 'PT' + fadeOutDelayTime.toUpperCase();
const parsedDuration = moment.duration(isoFormattedDuration).asMilliseconds();
const parsedDelay = moment.duration(isoFormattedDelay).asMilliseconds();
cutoff = parsedDuration + parsedDelay;
}
let age = this.numericDuration; let age = this.numericDuration;
return age < cutoff && !this.refreshCSS; return age < cutoff && !this.refreshCSS;
@ -524,6 +534,8 @@ export default {
if (!this.isPaused) { if (!this.isPaused) {
this.setFocusedImage(imageIndex); this.setFocusedImage(imageIndex);
this.scrollToRight(); this.scrollToRight();
} else {
this.scrollToFocused();
} }
}, },
deep: true deep: true
@ -987,6 +999,7 @@ export default {
this.setSizedImageDimensions(); this.setSizedImageDimensions();
this.calculateViewHeight(); this.calculateViewHeight();
this.scrollToFocused();
}, },
setSizedImageDimensions() { setSizedImageDimensions() {
this.focusedImageNaturalAspectRatio = this.$refs.focusedImage.naturalWidth / this.$refs.focusedImage.naturalHeight; this.focusedImageNaturalAspectRatio = this.$refs.focusedImage.naturalWidth / this.$refs.focusedImage.naturalHeight;

View File

@ -1,3 +1,5 @@
@use 'sass:math';
@keyframes fade-out { @keyframes fade-out {
from { from {
background-color: rgba($colorOk, 0.5); background-color: rgba($colorOk, 0.5);
@ -138,6 +140,9 @@
animation-timing-function: ease-in; animation-timing-function: ease-in;
animation-iteration-count: 1; animation-iteration-count: 1;
animation-fill-mode: forwards; animation-fill-mode: forwards;
&.no-animation {
animation: none;
}
} }
@ -211,7 +216,7 @@
} }
.c-thumb { .c-thumb {
$w: $imageThumbsD / 2; $w: math.div($imageThumbsD, 2);
min-width: $w; min-width: $w;
width: $w; width: $w;

View File

@ -192,7 +192,6 @@ export default {
if (this.axisType === 'yAxis' && this.axis.get('logMode')) { if (this.axisType === 'yAxis' && this.axis.get('logMode')) {
return getLogTicks(range.min, range.max, number, 4); return getLogTicks(range.min, range.max, number, 4);
// return getLogTicks2(range.min, range.max, number);
} else { } else {
return ticks(range.min, range.max, number); return ticks(range.min, range.max, number);
} }
@ -204,6 +203,7 @@ export default {
updateTicks(forceRegeneration = false) { updateTicks(forceRegeneration = false) {
const range = this.axis.get('displayRange'); const range = this.axis.get('displayRange');
if (!range) { if (!range) {
delete this.min; delete this.min;
delete this.max; delete this.max;

View File

@ -215,6 +215,10 @@ export default class YAxisModel extends Model {
const _range = this.get('displayRange'); const _range = this.get('displayRange');
if (!_range) {
return;
}
if (this.get('logMode')) { if (this.get('logMode')) {
_range.min = antisymlog(_range.min, 10); _range.min = antisymlog(_range.min, 10);
_range.max = antisymlog(_range.max, 10); _range.max = antisymlog(_range.max, 10);

View File

@ -132,12 +132,6 @@ export default {
} }
} }
}); });
if (this.object && this.object.type === 'conditionWidget' && keys.includes('output')) {
this.openmct.objects.mutate(this.object, 'conditionalLabel', styleObj.output);
} else {
this.openmct.objects.mutate(this.object, 'conditionalLabel', '');
}
} }
} }
}; };

View File

@ -78,11 +78,6 @@ export function getLogTicks(start, stop, mainTickCount = 8, secondaryTickCount =
return result; return result;
} }
export function getLogTicks2(start, stop, count = 8) {
return ticks(antisymlog(start, 10), antisymlog(stop, 10), count)
.map(n => symlog(n, 10));
}
/** /**
* Linear tick generation from d3-array. * Linear tick generation from d3-array.
*/ */

View File

@ -213,12 +213,6 @@ export default {
} }
} }
}); });
if (this.domainObject && this.domainObject.type === 'conditionWidget' && keys.includes('output')) {
this.openmct.objects.mutate(this.domainObject, 'conditionalLabel', styleObj.output);
} else {
this.openmct.objects.mutate(this.domainObject, 'conditionalLabel', '');
}
}, },
updateView(immediatelySelect) { updateView(immediatelySelect) {
this.clear(); this.clear();
@ -450,4 +444,3 @@ export default {
} }
}; };
</script> </script>