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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 701 additions and 434 deletions

View File

@ -21,45 +21,163 @@
*****************************************************************************/
/*
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');
test.describe('Condition Set Operations', () => {
test('Create new button `condition set` creates new condition object', async ({ page }) => {
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
let conditionSetUrl;
let getConditionSetIdentifierFromUrl;
//Click the Create button
await page.click('button:has-text("Create")');
test('Create new Condition Set object and store @localStorage', async ({ page, context }) => {
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
// Click text=Condition Set
await page.click('text=Condition Set');
//Click the Create button
await page.click('button:has-text("Create")');
// Click text=OK
// Click text=Condition Set
await page.click('text=Condition Set');
// Click text=OK
await Promise.all([
page.waitForNavigation(),
page.click('text=OK')
]);
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.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.click('text=OK')
page.reload(),
page.waitForLoadState('networkidle')
]);
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
//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 }) => {
//Go to object created in step one
//Verify the Condition Set properties persist on Save
//Verify the Condition Set properties persist on page.reload()
});
test.fixme('condition set object can be modified', async ({ page }) => {
//Go to object created in step one
test('condition set object can be modified on @localStorage', async ({ page }) => {
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');
//Update the Condition Set properties
//Verify the Condition Set properties persist on Save
//Verify the Condition Set properties persist on page.reload()
// Click Edit Button
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 }) => {
//Go to object created in step one
//Verify that Condition Set object can be deleted
//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
test('condition set object can be deleted by Search Tree Actions menu on @localStorage', async ({ page }) => {
//Navigate to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
//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",
"version": "2.0.3-SNAPSHOT",
"version": "2.0.3",
"description": "The Open MCT core platform",
"devDependencies": {
"@babel/eslint-parser": "7.16.3",

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,3 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* 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.formats = this.openmct.telemetry.getFormatMap(this.metadata);
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.bounds = this.openmct.time.bounds();
this.limitEvaluator = this.openmct
.telemetry
.limitEvaluator(this.domainObject);
this.openmct.time.on('timeSystem', this.updateTimeSystem);
this.openmct.time.on('bounds', this.updateBounds);
this.timestampKey = this.openmct.time.timeSystem().key;
@ -135,72 +132,41 @@ export default {
this.valueKey = this.valueMetadata ? this.valueMetadata.key : undefined;
this.unsubscribe = this.openmct
.telemetry
.subscribe(this.domainObject, this.setLatestValues);
this.requestHistory();
this.telemetryCollection = this.openmct.telemetry.requestCollection(this.domainObject, {
size: 1,
strategy: 'latest'
});
this.telemetryCollection.on('add', this.setLatestValues);
this.telemetryCollection.on('clear', this.resetValues);
this.telemetryCollection.load();
if (this.hasUnits) {
this.setUnit();
}
},
destroyed() {
this.unsubscribe();
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: {
updateView() {
if (!this.updatingView) {
this.updatingView = true;
requestAnimationFrame(() => {
let newTimestamp = this.getParsedTimestamp(this.latestDatum);
if (this.shouldUpdate(newTimestamp)) {
this.timestamp = newTimestamp;
this.datum = this.latestDatum;
}
this.timestamp = this.getParsedTimestamp(this.latestDatum);
this.datum = this.latestDatum;
this.updatingView = false;
});
}
},
setLatestValues(datum) {
this.latestDatum = datum;
setLatestValues(data) {
this.latestDatum = data[data.length - 1];
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) {
this.resetValues();
this.timestampKey = timeSystem.key;
},
updateViewContext() {
@ -241,4 +207,3 @@ export default {
}
};
</script>

View File

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

View File

@ -27,7 +27,7 @@
:href="url"
>
<div class="c-condition-widget__label">
{{ internalDomainObject.conditionalLabel || internalDomainObject.label }}
{{ label }}
</div>
</component>
</template>
@ -39,28 +39,112 @@ export default {
inject: ['openmct', 'domainObject'],
data: function () {
return {
internalDomainObject: this.domainObject
conditionalLabel: '',
conditionSetIdentifier: null,
domainObjectLabel: '',
url: null,
urlDefined: false,
useConditionSetOutputAsLabel: false
};
},
computed: {
urlDefined() {
return this.internalDomainObject.url && this.internalDomainObject.url.length > 0;
},
url() {
return this.urlDefined ? sanitizeUrl(this.internalDomainObject.url) : null;
label() {
return this.useConditionSetOutputAsLabel
? this.conditionalLabel
: this.domainObjectLabel
;
}
},
watch: {
conditionSetIdentifier: {
handler(newValue, oldValue) {
if (!oldValue || !newValue || !this.openmct.objects.areIdsEqual(newValue, oldValue)) {
return;
}
this.listenToConditionSetChanges();
},
deep: true
}
},
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() {
this.conditionSetIdentifier = null;
if (this.unlisten) {
this.unlisten();
}
this.stopListeningToConditionSetChanges();
},
methods: {
updateInternalDomainObject(domainObject) {
this.internalDomainObject = domainObject;
async listenToConditionSetChanges() {
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);
}
this.openmct.time.on("bounds", this.refreshData);
this.status = this.openmct.status.get(this.item.identifier);
this.removeStatusListener = this.openmct.status.observe(this.item.identifier, this.setStatus);
},
beforeDestroy() {
this.removeSubscription();
this.removeStatusListener();
if (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) {
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}`;
},
requestHistoricalData() {
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.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));
setLatestValues(data) {
this.latestDatum = data[data.length - 1];
this.updateView();
},
updateView() {
if (!this.updatingView) {
@ -291,17 +266,10 @@ export default {
});
}
},
removeSubscription() {
if (this.subscription) {
this.subscription();
this.subscription = undefined;
}
},
refreshData(bounds, isTick) {
if (!isTick) {
this.latestDatum = undefined;
this.updateView();
this.requestHistoricalData(this.domainObject);
}
},
setObject(domainObject) {
@ -315,8 +283,13 @@ export default {
const valueMetadata = this.metadata.value(this.item.value);
this.customStringformatter = this.openmct.telemetry.customStringFormatter(valueMetadata, this.item.format);
this.requestHistoricalData();
this.subscribeToObject();
this.telemetryCollection = this.openmct.telemetry.requestCollection(this.domainObject, {
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.unshift(this.domainObject);

View File

@ -53,6 +53,8 @@ describe('Gauge plugin', () => {
openmct = createOpenMct();
openmct.on('start', done);
openmct.install(openmct.plugins.Gauge());
openmct.startHeadless();
});
@ -190,28 +192,27 @@ describe('Gauge plugin', () => {
});
it('renders gauge element', () => {
const gaugeElement = gaugeHolder.querySelectorAll('.c-gauge');
const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper');
expect(gaugeElement.length).toBe(1);
});
it('renders major elements', () => {
const wrapperElement = gaugeHolder.querySelector('.c-gauge__wrapper');
const rangeElement = gaugeHolder.querySelector('.c-gauge__range');
const curveElement = gaugeHolder.querySelector('.c-gauge__curval');
const dialElement = gaugeHolder.querySelector('.c-dial');
const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper');
const rangeElement = gaugeHolder.querySelector('.js-gauge-dial-range');
const valueElement = gaugeHolder.querySelector('.js-dial-current-value');
const hasMajorElements = Boolean(wrapperElement && rangeElement && curveElement && dialElement);
const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement);
expect(hasMajorElements).toBe(true);
});
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) => {
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));
done();
}
@ -326,28 +327,27 @@ describe('Gauge plugin', () => {
});
it('renders gauge element', () => {
const gaugeElement = gaugeHolder.querySelectorAll('.c-gauge');
const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper');
expect(gaugeElement.length).toBe(1);
});
it('renders major elements', () => {
const wrapperElement = gaugeHolder.querySelector('.c-gauge__wrapper');
const rangeElement = gaugeHolder.querySelector('.c-gauge__range');
const curveElement = gaugeHolder.querySelector('.c-gauge__curval');
const dialElement = gaugeHolder.querySelector('.c-dial');
const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper');
const rangeElement = gaugeHolder.querySelector('.js-gauge-dial-range');
const valueElement = gaugeHolder.querySelector('.js-dial-current-value');
const hasMajorElements = Boolean(wrapperElement && rangeElement && curveElement && dialElement);
const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement);
expect(hasMajorElements).toBe(true);
});
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) => {
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));
done();
}
@ -462,28 +462,27 @@ describe('Gauge plugin', () => {
});
it('renders gauge element', () => {
const gaugeElement = gaugeHolder.querySelectorAll('.c-gauge');
const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper');
expect(gaugeElement.length).toBe(1);
});
it('renders major elements', () => {
const wrapperElement = gaugeHolder.querySelector('.c-gauge__wrapper');
const rangeElement = gaugeHolder.querySelector('.c-gauge__range');
const curveElement = gaugeHolder.querySelector('.c-meter');
const dialElement = gaugeHolder.querySelector('.c-meter__bg');
const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper');
const rangeElement = gaugeHolder.querySelector('.js-gauge-meter-range');
const valueElement = gaugeHolder.querySelector('.js-meter-current-value');
const hasMajorElements = Boolean(wrapperElement && rangeElement && curveElement && dialElement);
const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement);
expect(hasMajorElements).toBe(true);
});
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) => {
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));
done();
}
@ -560,17 +559,16 @@ describe('Gauge plugin', () => {
});
it('renders gauge element', () => {
const gaugeElement = gaugeHolder.querySelectorAll('.c-gauge');
const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper');
expect(gaugeElement.length).toBe(1);
});
it('renders major elements', () => {
const wrapperElement = gaugeHolder.querySelector('.c-gauge__wrapper');
const rangeElement = gaugeHolder.querySelector('.c-gauge__range');
const curveElement = gaugeHolder.querySelector('.c-meter');
const dialElement = gaugeHolder.querySelector('.c-meter__bg');
const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper');
const rangeElement = gaugeHolder.querySelector('.js-gauge-meter-range');
const valueElement = gaugeHolder.querySelector('.js-meter-current-value');
const hasMajorElements = Boolean(wrapperElement && rangeElement && curveElement && dialElement);
const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement);
expect(hasMajorElements).toBe(true);
});
@ -643,17 +641,16 @@ describe('Gauge plugin', () => {
});
it('renders gauge element', () => {
const gaugeElement = gaugeHolder.querySelectorAll('.c-gauge');
const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper');
expect(gaugeElement.length).toBe(1);
});
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 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);
});
@ -772,28 +769,27 @@ describe('Gauge plugin', () => {
});
it('renders gauge element', () => {
const gaugeElement = gaugeHolder.querySelectorAll('.c-gauge');
const gaugeElement = gaugeHolder.querySelectorAll('.js-gauge-wrapper');
expect(gaugeElement.length).toBe(1);
});
it('renders major elements', () => {
const wrapperElement = gaugeHolder.querySelector('.c-gauge__wrapper');
const rangeElement = gaugeHolder.querySelector('.c-gauge__range');
const curveElement = gaugeHolder.querySelector('.c-gauge__curval');
const dialElement = gaugeHolder.querySelector('.c-dial');
const wrapperElement = gaugeHolder.querySelector('.js-gauge-wrapper');
const rangeElement = gaugeHolder.querySelector('.js-gauge-dial-range');
const valueElement = gaugeHolder.querySelector('.js-dial-current-value');
const hasMajorElements = Boolean(wrapperElement && rangeElement && curveElement && dialElement);
const hasMajorElements = Boolean(wrapperElement && rangeElement && valueElement);
expect(hasMajorElements).toBe(true);
});
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) => {
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));
done();
}

View File

@ -21,168 +21,259 @@
*****************************************************************************/
<template>
<div
class="c-gauge"
class="c-gauge__wrapper js-gauge-wrapper"
:class="`c-gauge--${gaugeType}`"
>
<div class="c-gauge__wrapper">
<template v-if="typeDial">
<svg
class="c-gauge__range"
viewBox="0 0 512 512"
>
<text
v-if="displayMinMax"
font-size="35"
transform="translate(105 455) rotate(-45)"
>{{ rangeLow }}</text>
<text
v-if="displayMinMax"
font-size="35"
transform="translate(407 455) rotate(45)"
text-anchor="end"
>{{ rangeHigh }}</text>
</svg>
<template v-if="typeDial">
<svg
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"
>
<text
v-if="displayMinMax"
font-size="35"
transform="translate(105 455) rotate(-45)"
>{{ rangeLow }}</text>
<text
v-if="displayMinMax"
font-size="35"
transform="translate(407 455) rotate(45)"
text-anchor="end"
>{{ rangeHigh }}</text>
</svg>
<svg
class="c-dial__current-value-text-wrapper"
viewBox="0 0 512 512"
>
<svg
v-if="displayCurVal"
class="c-gauge__curval"
class="c-dial__current-value-text-sizer"
:viewBox="curValViewBox"
>
<text
class="c-gauge__curval-text"
class="c-dial__current-value-text js-dial-current-value"
lengthAdjust="spacing"
text-anchor="middle"
style="transform: translate(50%, 70%)"
>{{ curVal }}</text>
</svg>
</svg>
<div class="c-dial">
<svg
class="c-dial__bg"
viewBox="0 0 512 512"
>
<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
class="c-dial__bg"
viewBox="0 0 10 10"
>
<svg
v-if="limitHigh && dialHighLimitDeg < 270"
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>
<g
v-if="limitLow !== null && dialLowLimitDeg < getLimitDegree('low', 'max')"
class="c-dial__limit-low"
: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>
<svg
v-if="limitLow && dialLowLimitDeg < 270"
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)`"
/>
</svg>
<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
class="c-dial__value"
viewBox="0 0 512 512"
:class="{
'c-dial-clip--90': degValue < 90 && typeFilledDial,
'c-dial-clip--180': degValue >= 90 && degValue < 180 && typeFilledDial
}"
>
<path
v-if="typeFilledDial && degValue > 0"
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(${degValue}deg)`"
/>
<path
v-if="typeNeedleDial && valueInBounds"
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"
:style="`transform: rotate(${degValue}deg)`"
/>
</svg>
<svg
v-if="typeFilledDial"
class="c-dial__filled-value-wrapper"
viewBox="0 0 10 10"
>
<g
class="c-dial__filled-value"
:style="`transform: rotate(${degValueFilledDial}deg)`"
>
<rect
v-if="degValue >= getLimitDegree('low', 'q1')"
class="c-dial__filled-value__low"
x="5"
y="5"
width="5"
height="5"
/>
<rect
v-if="degValue >= getLimitDegree('low', 'q2')"
class="c-dial__filled-value__mid"
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>
</template>
<template v-if="typeMeter">
<div class="c-meter">
<div
v-if="displayMinMax"
class="c-gauge__range c-meter__range js-gauge-meter-range"
>
<div class="c-meter__range__high">{{ rangeHigh }}</div>
<div class="c-meter__range__low">{{ rangeLow }}</div>
</div>
</template>
<div class="c-meter__bg">
<template v-if="typeMeterVertical">
<div
class="c-meter__value"
:style="`transform: translateY(${meterValueToPerc}%)`"
></div>
<template v-if="typeMeter">
<div class="c-meter">
<div
v-if="displayMinMax"
class="c-gauge__range c-meter__range"
<div
v-if="limitHigh !== null && meterHighLimitPerc > 0"
class="c-meter__limit-high"
:style="`height: ${meterHighLimitPerc}%`"
></div>
<div
v-if="limitLow !== null && meterLowLimitPerc > 0"
class="c-meter__limit-low"
:style="`height: ${meterLowLimitPerc}%`"
></div>
</template>
<template v-if="typeMeterHorizontal">
<div
class="c-meter__value"
:style="`transform: translateX(${meterValueToPerc * -1}%)`"
></div>
<div
v-if="limitHigh !== null && meterHighLimitPerc > 0"
class="c-meter__limit-high"
:style="`width: ${meterHighLimitPerc}%`"
></div>
<div
v-if="limitLow !== null && meterLowLimitPerc > 0"
class="c-meter__limit-low"
:style="`width: ${meterLowLimitPerc}%`"
></div>
</template>
<svg
class="c-meter__current-value-text-wrapper"
viewBox="0 0 512 512"
>
<div class="c-meter__range__high">{{ rangeHigh }}</div>
<div class="c-meter__range__low">{{ rangeLow }}</div>
</div>
<div class="c-meter__bg">
<template v-if="typeMeterVertical">
<div
class="c-meter__value"
:style="`transform: translateY(${meterValueToPerc}%)`"
></div>
<div
v-if="limitHigh && meterHighLimitPerc > 0"
class="c-meter__limit-high"
:style="`height: ${meterHighLimitPerc}%`"
></div>
<div
v-if="limitLow && meterLowLimitPerc > 0"
class="c-meter__limit-low"
:style="`height: ${meterLowLimitPerc}%`"
></div>
</template>
<template v-if="typeMeterHorizontal">
<div
class="c-meter__value"
:style="`transform: translateX(${meterValueToPerc * -1}%)`"
></div>
<div
v-if="limitHigh && meterHighLimitPerc > 0"
class="c-meter__limit-high"
:style="`width: ${meterHighLimitPerc}%`"
></div>
<div
v-if="limitLow && meterLowLimitPerc > 0"
class="c-meter__limit-low"
:style="`width: ${meterLowLimitPerc}%`"
></div>
</template>
<svg
v-if="displayCurVal"
class="c-gauge__curval"
class="c-meter__current-value-text-sizer"
:viewBox="curValViewBox"
preserveAspectRatio="xMidYMid meet"
>
<text
class="c-gauge__curval-text"
text-anchor="middle"
class="c-dial__current-value-text js-meter-current-value"
lengthAdjust="spacing"
text-anchor="middle"
style="transform: translate(50%, 70%)"
>{{ curVal }}</text>
</svg>
</div>
</svg>
</div>
</template>
</div>
</div>
</template>
</div>
</template>
<script>
import { DIAL_VALUE_DEG_OFFSET, getLimitDegree } from '../gauge-limit-util';
const LIMIT_PADDING_IN_PERCENT = 10;
export default {
@ -209,6 +300,13 @@ export default {
degValue() {
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() {
return this.percentToDegrees(this.valToPercent(this.limitHigh));
},
@ -299,6 +397,7 @@ export default {
this.openmct.time.off('timeSystem', this.setTimeSystem);
},
methods: {
getLimitDegree: getLimitDegree,
addTelemetryObjectAndSubscribe(domainObject) {
this.telemetryObject = domainObject;
this.request();
@ -340,7 +439,7 @@ export default {
return this.gaugeType.indexOf(str) !== -1;
},
percentToDegrees(vPercent) {
return this.round((vPercent / 100) * 270, 2);
return this.round(((vPercent / 100) * 270) + DIAL_VALUE_DEG_OFFSET, 2);
},
removeFromComposition(telemetryObject = this.telemetryObject) {
let composition = this.domainObject.composition.filter(id =>
@ -453,7 +552,7 @@ export default {
valToPercent(vValue) {
// Used by dial
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;
}

View File

@ -27,6 +27,7 @@
:class="model.cssClass"
>
<ToggleSwitch
:id="'gaugeToggle'"
:checked="isUseTelemetryLimits"
label="Use telemetry limits for minimum and maximum ranges"
@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 {
overflow: hidden;
}
@ -13,10 +7,8 @@ $limitHighClip180: polygon(100% 0, 100% 100%, 0 100%);
&.invalid,
&.invalid.req { @include validationState($glyph-icon-x, $colorFormInvalid); }
&.valid,
&.valid.req { @include validationState($glyph-icon-check, $colorFormValid); }
&.req { @include validationState($glyph-icon-asterisk, $colorFormRequired); }
}
@ -37,92 +29,47 @@ $limitHighClip180: polygon(100% 0, 100% 100%, 0 100%);
@include abs();
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 */
.c-dial {
// Dial elements
@include abs();
clip-path: $dialClip;
svg[class*='c-dial'] {
max-height: 100%;
max-width: 100%;
position: absolute;
svg,
&__ticks,
&__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;
}
g {
transform-origin: center;
}
}
.c-gauge--dial-needle .c-dial__value {
path {
.c-dial {
&__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;
}
&__current-value-text {
fill: $colorGaugeTextValue;
font-family: $heroFont;
}
}
/********************************************** METER GAUGE */
@ -131,6 +78,13 @@ $limitHighClip180: polygon(100% 0, 100% 100%, 0 100%);
@include abs();
display: flex;
svg {
// current-value-text
position: absolute;
height: 100%;
width: 100%;
}
&__range {
display: flex;
flex: 0 0 auto;

View File

@ -143,13 +143,13 @@
<!-- spacecraft position fresh -->
<div
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>
<!-- camera position fresh -->
<div
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>
</div>
<div class="h-local-controls">
@ -331,6 +331,16 @@ export default {
},
isImageNew() {
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;
return age < cutoff && !this.refreshCSS;
@ -524,6 +534,8 @@ export default {
if (!this.isPaused) {
this.setFocusedImage(imageIndex);
this.scrollToRight();
} else {
this.scrollToFocused();
}
},
deep: true
@ -987,6 +999,7 @@ export default {
this.setSizedImageDimensions();
this.calculateViewHeight();
this.scrollToFocused();
},
setSizedImageDimensions() {
this.focusedImageNaturalAspectRatio = this.$refs.focusedImage.naturalWidth / this.$refs.focusedImage.naturalHeight;

View File

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

View File

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

View File

@ -215,6 +215,10 @@ export default class YAxisModel extends Model {
const _range = this.get('displayRange');
if (!_range) {
return;
}
if (this.get('logMode')) {
_range.min = antisymlog(_range.min, 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;
}
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.
*/

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) {
this.clear();
@ -450,4 +444,3 @@ export default {
}
};
</script>