Compare commits
5 Commits
user-attri
...
v2.1.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db495ea12e | ||
|
|
9022d5d859 | ||
|
|
8fed722f72 | ||
|
|
9a64220db4 | ||
|
|
eeba4703dd |
1
.github/codeql/codeql-config.yml
vendored
1
.github/codeql/codeql-config.yml
vendored
@@ -1 +0,0 @@
|
||||
name: 'Custom CodeQL config'
|
||||
12
.github/dependabot.yml
vendored
12
.github/dependabot.yml
vendored
@@ -13,12 +13,14 @@ updates:
|
||||
- "pr:daveit"
|
||||
- "pr:platform"
|
||||
ignore:
|
||||
- dependency-name: "@playwright/test" #We have to source the playwright container which is not detected by Dependabot
|
||||
- dependency-name: "playwright-core" #We have to source the playwright container which is not detected by Dependabot
|
||||
- dependency-name: "@babel/eslint-parser" #Lots of noise in these type patch releases.
|
||||
update-types: ["version-update:semver-patch"]
|
||||
- dependency-name: "eslint-plugin-vue" #Lots of noise in these type patch releases.
|
||||
#We have to source the container which is not detected by Dependabot
|
||||
- dependency-name: "@playwright/test"
|
||||
#Lots of noise in these type patch releases.
|
||||
- dependency-name: "@babel/eslint-parser"
|
||||
update-types: ["version-update:semver-patch"]
|
||||
- dependency-name: "eslint-plugin-vue"
|
||||
update-types: ["version-update:semver-patch"]
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
|
||||
31
.github/workflows/codeql-analysis.yml
vendored
31
.github/workflows/codeql-analysis.yml
vendored
@@ -1,10 +1,11 @@
|
||||
name: 'CodeQL'
|
||||
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master, 'release/*']
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [master, 'release/*']
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
- '**/*Spec.js'
|
||||
- '**/*.md'
|
||||
@@ -26,19 +27,17 @@ jobs:
|
||||
security-events: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
config-file: ./.github/codeql/codeql-config.yml
|
||||
languages: javascript
|
||||
queries: security-and-quality
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: javascript
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
|
||||
98
.github/workflows/lighthouse.yml
vendored
Normal file
98
.github/workflows/lighthouse.yml
vendored
Normal file
@@ -0,0 +1,98 @@
|
||||
name: lighthouse
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Which branch do you want to test?' # Limited to branch for now
|
||||
required: false
|
||||
default: 'master'
|
||||
pull_request:
|
||||
types:
|
||||
- labeled
|
||||
jobs:
|
||||
lighthouse-pr:
|
||||
if: ${{ github.event.label.name == 'pr:lighthouse' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Master for Baseline
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: master #explicitly checkout master for baseline
|
||||
- name: Install Node 16
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v2
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
|
||||
- name: npm install with lighthouse cli
|
||||
run: npm install && npm install -g @lhci/cli
|
||||
- name: Run lhci against master to generate baseline and ignore exit codes
|
||||
run: lhci autorun || true
|
||||
- name: Perform clean checkout of PR
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: true
|
||||
- name: Install Node version which is compatible with PR
|
||||
uses: actions/setup-node@v3
|
||||
- name: npm install with lighthouse cli
|
||||
run: npm install && npm install -g @lhci/cli
|
||||
- name: Run lhci with PR
|
||||
run: lhci autorun
|
||||
env:
|
||||
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
|
||||
lighthouse-nightly:
|
||||
if: ${{ github.event.schedule }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Node 16
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v2
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
|
||||
- name: npm install with lighthouse cli
|
||||
run: npm install && npm install -g @lhci/cli
|
||||
- name: Run lhci against master to generate baseline
|
||||
run: lhci autorun
|
||||
env:
|
||||
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
|
||||
lighthouse-dispatch:
|
||||
if: ${{ github.event.workflow_dispatch }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.inputs.version }}
|
||||
- name: Install Node 14
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v3
|
||||
env:
|
||||
cache-name: cache-node-modules
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
|
||||
- name: npm install with lighthouse cli
|
||||
run: npm install && npm install -g @lhci/cli
|
||||
- name: Run lhci against master to generate baseline
|
||||
run: lhci autorun
|
||||
|
||||
@@ -100,7 +100,7 @@ To run the performance tests:
|
||||
The test suite is configured to all tests localed in `e2e/tests/` ending in `*.e2e.spec.js`. For more about the e2e test suite, please see the [README](./e2e/README.md)
|
||||
|
||||
### Security Tests
|
||||
Each commit is analyzed for known security vulnerabilities using [CodeQL](https://codeql.github.com/docs/codeql-language-guides/codeql-library-for-javascript/) and our overall security report is available on [LGTM](https://lgtm.com/projects/g/nasa/openmct/). The list of CWE coverage items is avaiable in the [CodeQL docs](https://codeql.github.com/codeql-query-help/javascript-cwe/). The CodeQL workflow is specified in the [CodeQL analysis file](./.github/workflows/codeql-analysis.yml) and the custom [CodeQL config](./.github/codeql/codeql-config.yml).
|
||||
Each commit is analyzed for known security vulnerabilities using [CodeQL](https://codeql.github.com/docs/codeql-language-guides/codeql-library-for-javascript/) and our overall security report is available on [LGTM](https://lgtm.com/projects/g/nasa/openmct/)
|
||||
|
||||
### Test Reporting and Code Coverage
|
||||
|
||||
|
||||
@@ -225,14 +225,15 @@ async function getHashUrlToDomainObject(page, uuid) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Utilizes the OpenMCT API to detect if the UI is in Edit mode.
|
||||
* Utilizes the OpenMCT API to detect if the given object has an active transaction (is in Edit mode).
|
||||
* @private
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @return {Promise<boolean>} true if the Open MCT is in Edit Mode
|
||||
* @param {string | import('../src/api/objects/ObjectAPI').Identifier} identifier
|
||||
* @return {Promise<boolean>} true if the object has an active transaction, false otherwise
|
||||
*/
|
||||
async function _isInEditMode(page, identifier) {
|
||||
// eslint-disable-next-line no-return-await
|
||||
return await page.evaluate(() => window.openmct.editor.isEditing());
|
||||
return await page.evaluate((objectIdentifier) => window.openmct.objects.isTransactionActive(objectIdentifier), identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
File diff suppressed because one or more lines are too long
15
package.json
15
package.json
@@ -1,15 +1,18 @@
|
||||
{
|
||||
"name": "openmct",
|
||||
"version": "2.1.2-SNAPSHOT",
|
||||
"version": "2.1.1",
|
||||
"description": "The Open MCT core platform",
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "7.18.9",
|
||||
"@braintree/sanitize-url": "6.0.0",
|
||||
"@percy/cli": "1.11.0",
|
||||
"@percy/cli": "1.10.3",
|
||||
"@percy/playwright": "1.0.4",
|
||||
"@playwright/test": "1.25.2",
|
||||
"@types/jasmine": "4.3.0",
|
||||
"@types/lodash": "4.14.186",
|
||||
"@types/eventemitter3": "^1.0.0",
|
||||
"@types/jasmine": "^4.0.1",
|
||||
"@types/karma": "^6.3.2",
|
||||
"@types/lodash": "^4.14.178",
|
||||
"@types/mocha": "^9.1.0",
|
||||
"babel-loader": "8.2.5",
|
||||
"babel-plugin-istanbul": "6.1.1",
|
||||
"codecov": "3.8.3",
|
||||
@@ -19,10 +22,10 @@
|
||||
"d3-axis": "3.0.0",
|
||||
"d3-scale": "3.3.0",
|
||||
"d3-selection": "3.0.0",
|
||||
"eslint": "8.25.0",
|
||||
"eslint": "8.23.1",
|
||||
"eslint-plugin-compat": "4.0.2",
|
||||
"eslint-plugin-playwright": "0.11.2",
|
||||
"eslint-plugin-vue": "9.6.0",
|
||||
"eslint-plugin-vue": "9.3.0",
|
||||
"eslint-plugin-you-dont-need-lodash-underscore": "6.12.0",
|
||||
"eventemitter3": "1.2.0",
|
||||
"file-saver": "2.0.5",
|
||||
|
||||
@@ -94,6 +94,7 @@ describe("The Annotation API", () => {
|
||||
openmct.startHeadless();
|
||||
});
|
||||
afterEach(async () => {
|
||||
openmct.objects.providers = {};
|
||||
await resetApplicationState(openmct);
|
||||
});
|
||||
it("is defined", () => {
|
||||
|
||||
@@ -96,7 +96,7 @@ export default class ObjectAPI {
|
||||
this.cache = {};
|
||||
this.interceptorRegistry = new InterceptorRegistry();
|
||||
|
||||
this.SYNCHRONIZED_OBJECT_TYPES = ['notebook', 'restricted-notebook', 'plan', 'annotation'];
|
||||
this.SYNCHRONIZED_OBJECT_TYPES = ['notebook', 'plan', 'annotation'];
|
||||
|
||||
this.errors = {
|
||||
Conflict: ConflictError
|
||||
@@ -204,13 +204,13 @@ export default class ObjectAPI {
|
||||
}
|
||||
|
||||
identifier = utils.parseKeyString(identifier);
|
||||
|
||||
let dirtyObject;
|
||||
if (this.isTransactionActive()) {
|
||||
let dirtyObject = this.transaction.getDirtyObject(identifier);
|
||||
dirtyObject = this.transaction.getDirtyObject(identifier);
|
||||
}
|
||||
|
||||
if (dirtyObject) {
|
||||
return Promise.resolve(dirtyObject);
|
||||
}
|
||||
if (dirtyObject) {
|
||||
return Promise.resolve(dirtyObject);
|
||||
}
|
||||
|
||||
const provider = this.getProvider(identifier);
|
||||
@@ -354,8 +354,10 @@ export default class ObjectAPI {
|
||||
* @returns {Promise} a promise which will resolve when the domain object
|
||||
* has been saved, or be rejected if it cannot be saved
|
||||
*/
|
||||
async save(domainObject) {
|
||||
const provider = this.getProvider(domainObject.identifier);
|
||||
save(domainObject) {
|
||||
let provider = this.getProvider(domainObject.identifier);
|
||||
let savedResolve;
|
||||
let savedReject;
|
||||
let result;
|
||||
|
||||
if (!this.isPersistable(domainObject.identifier)) {
|
||||
@@ -364,37 +366,27 @@ export default class ObjectAPI {
|
||||
result = Promise.resolve(true);
|
||||
} else {
|
||||
const persistedTime = Date.now();
|
||||
const username = await this.#getCurrentUsername();
|
||||
const isNewObject = domainObject.persisted === undefined;
|
||||
let savedResolve;
|
||||
let savedReject;
|
||||
let savedObjectPromise;
|
||||
|
||||
result = new Promise((resolve, reject) => {
|
||||
savedResolve = resolve;
|
||||
savedReject = reject;
|
||||
});
|
||||
|
||||
this.#mutate(domainObject, 'persisted', persistedTime);
|
||||
this.#mutate(domainObject, 'modifiedBy', username);
|
||||
|
||||
if (isNewObject) {
|
||||
this.#mutate(domainObject, 'created', persistedTime);
|
||||
this.#mutate(domainObject, 'createdBy', username);
|
||||
|
||||
savedObjectPromise = provider.create(domainObject);
|
||||
} else {
|
||||
savedObjectPromise = provider.update(domainObject);
|
||||
}
|
||||
|
||||
if (savedObjectPromise) {
|
||||
savedObjectPromise.then(response => {
|
||||
savedResolve(response);
|
||||
}).catch((error) => {
|
||||
savedReject(error);
|
||||
if (domainObject.persisted === undefined) {
|
||||
result = new Promise((resolve, reject) => {
|
||||
savedResolve = resolve;
|
||||
savedReject = reject;
|
||||
});
|
||||
domainObject.persisted = persistedTime;
|
||||
const newObjectPromise = provider.create(domainObject);
|
||||
if (newObjectPromise) {
|
||||
newObjectPromise.then(response => {
|
||||
this.mutate(domainObject, 'persisted', persistedTime);
|
||||
savedResolve(response);
|
||||
}).catch((error) => {
|
||||
savedReject(error);
|
||||
});
|
||||
} else {
|
||||
result = Promise.reject(`[ObjectAPI][save] Object provider returned ${newObjectPromise} when creating new object.`);
|
||||
}
|
||||
} else {
|
||||
result = Promise.reject(`[ObjectAPI][save] Object provider returned ${savedObjectPromise} when ${isNewObject ? 'creating new' : 'updating'} object.`);
|
||||
domainObject.persisted = persistedTime;
|
||||
this.mutate(domainObject, 'persisted', persistedTime);
|
||||
result = provider.update(domainObject);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -407,21 +399,8 @@ export default class ObjectAPI {
|
||||
});
|
||||
}
|
||||
|
||||
async #getCurrentUsername() {
|
||||
const user = await this.openmct.user.getCurrentUser();
|
||||
let username;
|
||||
|
||||
if (user !== undefined) {
|
||||
username = user.getName();
|
||||
}
|
||||
|
||||
return username;
|
||||
}
|
||||
|
||||
/**
|
||||
* After entering into edit mode, creates a new instance of Transaction to keep track of changes in Objects
|
||||
*
|
||||
* @returns {Transaction} a new Transaction that was just created
|
||||
*/
|
||||
startTransaction() {
|
||||
if (this.isTransactionActive()) {
|
||||
@@ -429,8 +408,6 @@ export default class ObjectAPI {
|
||||
}
|
||||
|
||||
this.transaction = new Transaction(this);
|
||||
|
||||
return this.transaction;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -503,16 +480,14 @@ export default class ObjectAPI {
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify a domain object. Internal to ObjectAPI, won't call save after.
|
||||
* @private
|
||||
*
|
||||
* Modify a domain object.
|
||||
* @param {module:openmct.DomainObject} object the object to mutate
|
||||
* @param {string} path the property to modify
|
||||
* @param {*} value the new value for this property
|
||||
* @method mutate
|
||||
* @memberof module:openmct.ObjectAPI#
|
||||
*/
|
||||
#mutate(domainObject, path, value) {
|
||||
mutate(domainObject, path, value) {
|
||||
if (!this.supportsMutation(domainObject.identifier)) {
|
||||
throw `Error: Attempted to mutate immutable object ${domainObject.name}`;
|
||||
}
|
||||
@@ -533,18 +508,6 @@ export default class ObjectAPI {
|
||||
//Destroy temporary mutable object
|
||||
this.destroyMutable(mutableDomainObject);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify a domain object and save.
|
||||
* @param {module:openmct.DomainObject} object the object to mutate
|
||||
* @param {string} path the property to modify
|
||||
* @param {*} value the new value for this property
|
||||
* @method mutate
|
||||
* @memberof module:openmct.ObjectAPI#
|
||||
*/
|
||||
mutate(domainObject, path, value) {
|
||||
this.#mutate(domainObject, path, value);
|
||||
|
||||
if (this.isTransactionActive()) {
|
||||
this.transaction.add(domainObject);
|
||||
@@ -721,7 +684,7 @@ export default class ObjectAPI {
|
||||
}
|
||||
|
||||
isTransactionActive() {
|
||||
return this.transaction !== undefined && this.transaction !== null;
|
||||
return Boolean(this.transaction && this.openmct.editor.isEditing());
|
||||
}
|
||||
|
||||
#hasAlreadyBeenPersisted(domainObject) {
|
||||
|
||||
@@ -8,27 +8,13 @@ describe("The Object API", () => {
|
||||
let mockDomainObject;
|
||||
const TEST_NAMESPACE = "test-namespace";
|
||||
const TEST_KEY = "test-key";
|
||||
const USERNAME = 'Joan Q Public';
|
||||
const FIFTEEN_MINUTES = 15 * 60 * 1000;
|
||||
|
||||
beforeEach((done) => {
|
||||
typeRegistry = jasmine.createSpyObj('typeRegistry', [
|
||||
'get'
|
||||
]);
|
||||
const userProvider = {
|
||||
isLoggedIn() {
|
||||
return true;
|
||||
},
|
||||
getCurrentUser() {
|
||||
return Promise.resolve({
|
||||
getName() {
|
||||
return USERNAME;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
openmct = createOpenMct();
|
||||
openmct.user.setProvider(userProvider);
|
||||
objectAPI = openmct.objects;
|
||||
|
||||
openmct.editor = {};
|
||||
@@ -77,34 +63,19 @@ describe("The Object API", () => {
|
||||
mockProvider.update.and.returnValue(Promise.resolve(true));
|
||||
objectAPI.addProvider(TEST_NAMESPACE, mockProvider);
|
||||
});
|
||||
it("Adds a 'created' timestamp to new objects", async () => {
|
||||
await objectAPI.save(mockDomainObject);
|
||||
expect(mockDomainObject.created).not.toBeUndefined();
|
||||
});
|
||||
it("Calls 'create' on provider if object is new", async () => {
|
||||
await objectAPI.save(mockDomainObject);
|
||||
it("Calls 'create' on provider if object is new", () => {
|
||||
objectAPI.save(mockDomainObject);
|
||||
expect(mockProvider.create).toHaveBeenCalled();
|
||||
expect(mockProvider.update).not.toHaveBeenCalled();
|
||||
});
|
||||
it("Calls 'update' on provider if object is not new", async () => {
|
||||
it("Calls 'update' on provider if object is not new", () => {
|
||||
mockDomainObject.persisted = Date.now() - FIFTEEN_MINUTES;
|
||||
mockDomainObject.modified = Date.now();
|
||||
|
||||
await objectAPI.save(mockDomainObject);
|
||||
objectAPI.save(mockDomainObject);
|
||||
expect(mockProvider.create).not.toHaveBeenCalled();
|
||||
expect(mockProvider.update).toHaveBeenCalled();
|
||||
});
|
||||
it("Sets the current user for 'createdBy' on new objects", async () => {
|
||||
await objectAPI.save(mockDomainObject);
|
||||
expect(mockDomainObject.createdBy).toBe(USERNAME);
|
||||
});
|
||||
it("Sets the current user for 'modifedBy' on existing objects", async () => {
|
||||
mockDomainObject.persisted = Date.now() - FIFTEEN_MINUTES;
|
||||
mockDomainObject.modified = Date.now();
|
||||
|
||||
await objectAPI.save(mockDomainObject);
|
||||
expect(mockDomainObject.modifiedBy).toBe(USERNAME);
|
||||
});
|
||||
|
||||
it("Does not persist if the object is unchanged", () => {
|
||||
mockDomainObject.persisted =
|
||||
|
||||
@@ -27,6 +27,7 @@ import TelemetryMetadataManager from './TelemetryMetadataManager';
|
||||
import TelemetryValueFormatter from './TelemetryValueFormatter';
|
||||
import DefaultMetadataProvider from './DefaultMetadataProvider';
|
||||
import objectUtils from 'objectUtils';
|
||||
import _ from 'lodash';
|
||||
|
||||
export default class TelemetryAPI {
|
||||
|
||||
@@ -72,7 +73,7 @@ export default class TelemetryAPI {
|
||||
* @returns {boolean} true if the object is a telemetry object.
|
||||
*/
|
||||
isTelemetryObject(domainObject) {
|
||||
return Boolean(this.#findMetadataProvider(domainObject));
|
||||
return Boolean(this.findMetadataProvider(domainObject));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -86,7 +87,7 @@ export default class TelemetryAPI {
|
||||
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
|
||||
*/
|
||||
canProvideTelemetry(domainObject) {
|
||||
return Boolean(this.#findSubscriptionProvider(domainObject))
|
||||
return Boolean(this.findSubscriptionProvider(domainObject))
|
||||
|| Boolean(this.findRequestProvider(domainObject));
|
||||
}
|
||||
|
||||
@@ -119,7 +120,7 @@ export default class TelemetryAPI {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
#findSubscriptionProvider() {
|
||||
findSubscriptionProvider() {
|
||||
const args = Array.prototype.slice.apply(arguments);
|
||||
function supportsDomainObject(provider) {
|
||||
return provider.supportsSubscribe.apply(provider, args);
|
||||
@@ -129,10 +130,9 @@ export default class TelemetryAPI {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a telemetry request provider that supports
|
||||
* a given domain object and options.
|
||||
* @private
|
||||
*/
|
||||
findRequestProvider() {
|
||||
findRequestProvider(domainObject) {
|
||||
const args = Array.prototype.slice.apply(arguments);
|
||||
function supportsDomainObject(provider) {
|
||||
return provider.supportsRequest.apply(provider, args);
|
||||
@@ -144,7 +144,7 @@ export default class TelemetryAPI {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
#findMetadataProvider(domainObject) {
|
||||
findMetadataProvider(domainObject) {
|
||||
return this.metadataProviders.filter(function (p) {
|
||||
return p.supportsMetadata(domainObject);
|
||||
})[0];
|
||||
@@ -153,7 +153,7 @@ export default class TelemetryAPI {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
#findLimitEvaluator(domainObject) {
|
||||
findLimitEvaluator(domainObject) {
|
||||
return this.limitProviders.filter(function (p) {
|
||||
return p.supportsLimits(domainObject);
|
||||
})[0];
|
||||
@@ -161,7 +161,6 @@ export default class TelemetryAPI {
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Though used in TelemetryCollection as well
|
||||
*/
|
||||
standardizeRequestOptions(options) {
|
||||
if (!Object.prototype.hasOwnProperty.call(options, 'start')) {
|
||||
@@ -175,10 +174,6 @@ export default class TelemetryAPI {
|
||||
if (!Object.prototype.hasOwnProperty.call(options, 'domain')) {
|
||||
options.domain = this.openmct.time.timeSystem().key;
|
||||
}
|
||||
|
||||
if (!Object.prototype.hasOwnProperty.call(options, 'timeContext')) {
|
||||
options.timeContext = this.openmct.time;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -246,7 +241,7 @@ export default class TelemetryAPI {
|
||||
/**
|
||||
* Request historical telemetry for a domain object.
|
||||
* The `options` argument allows you to specify filters
|
||||
* (start, end, etc.), sort order, time context, and strategies for retrieving
|
||||
* (start, end, etc.), sort order, and strategies for retrieving
|
||||
* telemetry (aggregation, latest available, etc.).
|
||||
*
|
||||
* @method request
|
||||
@@ -260,7 +255,7 @@ export default class TelemetryAPI {
|
||||
*/
|
||||
async request(domainObject) {
|
||||
if (this.noRequestProviderForAllObjects) {
|
||||
return [];
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
if (arguments.length === 1) {
|
||||
@@ -278,24 +273,22 @@ export default class TelemetryAPI {
|
||||
if (!provider) {
|
||||
this.requestAbortControllers.delete(abortController);
|
||||
|
||||
return this.#handleMissingRequestProvider(domainObject);
|
||||
return this.handleMissingRequestProvider(domainObject);
|
||||
}
|
||||
|
||||
arguments[1] = await this.applyRequestInterceptors(domainObject, arguments[1]);
|
||||
try {
|
||||
const telemetry = await provider.request(...arguments);
|
||||
|
||||
return telemetry;
|
||||
} catch (error) {
|
||||
if (error.name !== 'AbortError') {
|
||||
this.openmct.notifications.error('Error requesting telemetry data, see console for details');
|
||||
console.error(error);
|
||||
}
|
||||
return provider.request.apply(provider, arguments)
|
||||
.catch((rejected) => {
|
||||
if (rejected.name !== 'AbortError') {
|
||||
this.openmct.notifications.error('Error requesting telemetry data, see console for details');
|
||||
console.error(rejected);
|
||||
}
|
||||
|
||||
throw new Error(error);
|
||||
} finally {
|
||||
this.requestAbortControllers.delete(abortController);
|
||||
}
|
||||
return Promise.reject(rejected);
|
||||
}).finally(() => {
|
||||
this.requestAbortControllers.delete(abortController);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -313,7 +306,7 @@ export default class TelemetryAPI {
|
||||
* the subscription
|
||||
*/
|
||||
subscribe(domainObject, callback, options) {
|
||||
const provider = this.#findSubscriptionProvider(domainObject);
|
||||
const provider = this.findSubscriptionProvider(domainObject);
|
||||
|
||||
if (!this.subscribeCache) {
|
||||
this.subscribeCache = {};
|
||||
@@ -360,7 +353,7 @@ export default class TelemetryAPI {
|
||||
*/
|
||||
getMetadata(domainObject) {
|
||||
if (!this.metadataCache.has(domainObject)) {
|
||||
const metadataProvider = this.#findMetadataProvider(domainObject);
|
||||
const metadataProvider = this.findMetadataProvider(domainObject);
|
||||
if (!metadataProvider) {
|
||||
return;
|
||||
}
|
||||
@@ -376,6 +369,33 @@ export default class TelemetryAPI {
|
||||
return this.metadataCache.get(domainObject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array of valueMetadatas that are common to all supplied
|
||||
* telemetry objects and match the requested hints.
|
||||
*
|
||||
*/
|
||||
commonValuesForHints(metadatas, hints) {
|
||||
const options = metadatas.map(function (metadata) {
|
||||
const values = metadata.valuesForHints(hints);
|
||||
|
||||
return _.keyBy(values, 'key');
|
||||
}).reduce(function (a, b) {
|
||||
const results = {};
|
||||
Object.keys(a).forEach(function (key) {
|
||||
if (Object.prototype.hasOwnProperty.call(b, key)) {
|
||||
results[key] = a[key];
|
||||
}
|
||||
});
|
||||
|
||||
return results;
|
||||
});
|
||||
const sortKeys = hints.map(function (h) {
|
||||
return 'hints.' + h;
|
||||
});
|
||||
|
||||
return _.sortBy(options, sortKeys);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a value formatter for a given valueMetadata.
|
||||
*
|
||||
@@ -430,7 +450,7 @@ export default class TelemetryAPI {
|
||||
*
|
||||
* @returns Promise
|
||||
*/
|
||||
#handleMissingRequestProvider(domainObject) {
|
||||
handleMissingRequestProvider(domainObject) {
|
||||
this.noRequestProviderForAllObjects = this.requestProviders.every(requestProvider => {
|
||||
const supportsRequest = requestProvider.supportsRequest.apply(requestProvider, arguments);
|
||||
const hasRequestProvider = Object.prototype.hasOwnProperty.call(requestProvider, 'request') && typeof requestProvider.request === 'function';
|
||||
@@ -520,7 +540,7 @@ export default class TelemetryAPI {
|
||||
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
|
||||
*/
|
||||
getLimitEvaluator(domainObject) {
|
||||
const provider = this.#findLimitEvaluator(domainObject);
|
||||
const provider = this.findLimitEvaluator(domainObject);
|
||||
if (!provider) {
|
||||
return {
|
||||
evaluate: function () {}
|
||||
@@ -558,7 +578,7 @@ export default class TelemetryAPI {
|
||||
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
|
||||
*/
|
||||
getLimits(domainObject) {
|
||||
const provider = this.#findLimitEvaluator(domainObject);
|
||||
const provider = this.findLimitEvaluator(domainObject);
|
||||
if (!provider || !provider.getLimits) {
|
||||
return {
|
||||
limits: function () {
|
||||
|
||||
@@ -23,11 +23,11 @@ import { createOpenMct, resetApplicationState } from 'utils/testing';
|
||||
import TelemetryAPI from './TelemetryAPI';
|
||||
import TelemetryCollection from './TelemetryCollection';
|
||||
|
||||
describe('Telemetry API', () => {
|
||||
describe('Telemetry API', function () {
|
||||
let openmct;
|
||||
let telemetryAPI;
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(function () {
|
||||
openmct = {
|
||||
time: jasmine.createSpyObj('timeAPI', [
|
||||
'timeSystem',
|
||||
@@ -47,11 +47,11 @@ describe('Telemetry API', () => {
|
||||
|
||||
});
|
||||
|
||||
describe('telemetry providers', () => {
|
||||
describe('telemetry providers', function () {
|
||||
let telemetryProvider;
|
||||
let domainObject;
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(function () {
|
||||
telemetryProvider = jasmine.createSpyObj('telemetryProvider', [
|
||||
'supportsSubscribe',
|
||||
'subscribe',
|
||||
@@ -73,16 +73,19 @@ describe('Telemetry API', () => {
|
||||
};
|
||||
});
|
||||
|
||||
it('provides consistent results without providers', async () => {
|
||||
it('provides consistent results without providers', function (done) {
|
||||
const unsubscribe = telemetryAPI.subscribe(domainObject);
|
||||
|
||||
expect(unsubscribe).toEqual(jasmine.any(Function));
|
||||
|
||||
const data = await telemetryAPI.request(domainObject);
|
||||
expect(data).toEqual([]);
|
||||
telemetryAPI.request(domainObject)
|
||||
.then((data) => {
|
||||
expect(data).toEqual([]);
|
||||
})
|
||||
.finally(done);
|
||||
});
|
||||
|
||||
it('skips providers that do not match', async () => {
|
||||
it('skips providers that do not match', function (done) {
|
||||
telemetryProvider.supportsSubscribe.and.returnValue(false);
|
||||
telemetryProvider.supportsRequest.and.returnValue(false);
|
||||
telemetryProvider.request.and.returnValue(Promise.resolve([]));
|
||||
@@ -95,13 +98,14 @@ describe('Telemetry API', () => {
|
||||
expect(telemetryProvider.subscribe).not.toHaveBeenCalled();
|
||||
expect(unsubscribe).toEqual(jasmine.any(Function));
|
||||
|
||||
await telemetryAPI.request(domainObject);
|
||||
expect(telemetryProvider.supportsRequest)
|
||||
.toHaveBeenCalledWith(domainObject, jasmine.any(Object));
|
||||
expect(telemetryProvider.request).not.toHaveBeenCalled();
|
||||
telemetryAPI.request(domainObject).then((response) => {
|
||||
expect(telemetryProvider.supportsRequest)
|
||||
.toHaveBeenCalledWith(domainObject, jasmine.any(Object));
|
||||
expect(telemetryProvider.request).not.toHaveBeenCalled();
|
||||
}).finally(done);
|
||||
});
|
||||
|
||||
it('sends subscribe calls to matching providers', () => {
|
||||
it('sends subscribe calls to matching providers', function () {
|
||||
const unsubFunc = jasmine.createSpy('unsubscribe');
|
||||
telemetryProvider.subscribe.and.returnValue(unsubFunc);
|
||||
telemetryProvider.supportsSubscribe.and.returnValue(true);
|
||||
@@ -129,7 +133,7 @@ describe('Telemetry API', () => {
|
||||
expect(callback).not.toHaveBeenCalledWith('otherValue');
|
||||
});
|
||||
|
||||
it('subscribes once per object', () => {
|
||||
it('subscribes once per object', function () {
|
||||
const unsubFunc = jasmine.createSpy('unsubscribe');
|
||||
telemetryProvider.subscribe.and.returnValue(unsubFunc);
|
||||
telemetryProvider.supportsSubscribe.and.returnValue(true);
|
||||
@@ -160,7 +164,7 @@ describe('Telemetry API', () => {
|
||||
expect(callbacktwo).not.toHaveBeenCalledWith('anotherValue');
|
||||
});
|
||||
|
||||
it('only deletes subscription cache when there are no more subscribers', () => {
|
||||
it('only deletes subscription cache when there are no more subscribers', function () {
|
||||
const unsubFunc = jasmine.createSpy('unsubscribe');
|
||||
telemetryProvider.subscribe.and.returnValue(unsubFunc);
|
||||
telemetryProvider.supportsSubscribe.and.returnValue(true);
|
||||
@@ -183,7 +187,7 @@ describe('Telemetry API', () => {
|
||||
unsubscribeThree();
|
||||
});
|
||||
|
||||
it('does subscribe/unsubscribe', () => {
|
||||
it('does subscribe/unsubscribe', function () {
|
||||
const unsubFunc = jasmine.createSpy('unsubscribe');
|
||||
telemetryProvider.subscribe.and.returnValue(unsubFunc);
|
||||
telemetryProvider.supportsSubscribe.and.returnValue(true);
|
||||
@@ -199,7 +203,7 @@ describe('Telemetry API', () => {
|
||||
unsubscribe();
|
||||
});
|
||||
|
||||
it('subscribes for different object', () => {
|
||||
it('subscribes for different object', function () {
|
||||
const unsubFuncs = [];
|
||||
const notifiers = [];
|
||||
telemetryProvider.supportsSubscribe.and.returnValue(true);
|
||||
@@ -239,120 +243,120 @@ describe('Telemetry API', () => {
|
||||
expect(unsubFuncs[1]).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('sends requests to matching providers', async () => {
|
||||
it('sends requests to matching providers', function (done) {
|
||||
const telemPromise = Promise.resolve([]);
|
||||
telemetryProvider.supportsRequest.and.returnValue(true);
|
||||
telemetryProvider.request.and.returnValue(telemPromise);
|
||||
telemetryAPI.addProvider(telemetryProvider);
|
||||
|
||||
await telemetryAPI.request(domainObject);
|
||||
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(
|
||||
domainObject,
|
||||
jasmine.any(Object)
|
||||
);
|
||||
expect(telemetryProvider.request).toHaveBeenCalledWith(
|
||||
domainObject,
|
||||
jasmine.any(Object)
|
||||
);
|
||||
telemetryAPI.request(domainObject).then(() => {
|
||||
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(
|
||||
domainObject,
|
||||
jasmine.any(Object)
|
||||
);
|
||||
expect(telemetryProvider.request).toHaveBeenCalledWith(
|
||||
domainObject,
|
||||
jasmine.any(Object)
|
||||
);
|
||||
}).finally(done);
|
||||
});
|
||||
|
||||
it('generates default request options', async () => {
|
||||
it('generates default request options', function (done) {
|
||||
telemetryProvider.supportsRequest.and.returnValue(true);
|
||||
telemetryProvider.request.and.returnValue(Promise.resolve([]));
|
||||
telemetryAPI.addProvider(telemetryProvider);
|
||||
|
||||
await telemetryAPI.request(domainObject);
|
||||
const { signal } = new AbortController();
|
||||
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(
|
||||
jasmine.any(Object),
|
||||
{
|
||||
signal,
|
||||
start: 0,
|
||||
end: 1,
|
||||
domain: 'system',
|
||||
timeContext: jasmine.any(Object)
|
||||
}
|
||||
);
|
||||
telemetryAPI.request(domainObject).then(() => {
|
||||
const { signal } = new AbortController();
|
||||
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(
|
||||
jasmine.any(Object),
|
||||
{
|
||||
signal,
|
||||
start: 0,
|
||||
end: 1,
|
||||
domain: 'system'
|
||||
}
|
||||
);
|
||||
|
||||
expect(telemetryProvider.request).toHaveBeenCalledWith(
|
||||
jasmine.any(Object),
|
||||
{
|
||||
signal,
|
||||
start: 0,
|
||||
end: 1,
|
||||
domain: 'system',
|
||||
timeContext: jasmine.any(Object)
|
||||
}
|
||||
);
|
||||
expect(telemetryProvider.request).toHaveBeenCalledWith(
|
||||
jasmine.any(Object),
|
||||
{
|
||||
signal,
|
||||
start: 0,
|
||||
end: 1,
|
||||
domain: 'system'
|
||||
}
|
||||
);
|
||||
|
||||
telemetryProvider.supportsRequest.calls.reset();
|
||||
telemetryProvider.request.calls.reset();
|
||||
telemetryProvider.supportsRequest.calls.reset();
|
||||
telemetryProvider.request.calls.reset();
|
||||
|
||||
await telemetryAPI.request(domainObject, {});
|
||||
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(
|
||||
jasmine.any(Object),
|
||||
{
|
||||
signal,
|
||||
start: 0,
|
||||
end: 1,
|
||||
domain: 'system',
|
||||
timeContext: jasmine.any(Object)
|
||||
}
|
||||
);
|
||||
telemetryAPI.request(domainObject, {}).then(() => {
|
||||
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(
|
||||
jasmine.any(Object),
|
||||
{
|
||||
signal,
|
||||
start: 0,
|
||||
end: 1,
|
||||
domain: 'system'
|
||||
}
|
||||
);
|
||||
|
||||
expect(telemetryProvider.request).toHaveBeenCalledWith(
|
||||
jasmine.any(Object),
|
||||
{
|
||||
signal,
|
||||
start: 0,
|
||||
end: 1,
|
||||
domain: 'system'
|
||||
}
|
||||
);
|
||||
});
|
||||
}).finally(done);
|
||||
|
||||
expect(telemetryProvider.request).toHaveBeenCalledWith(
|
||||
jasmine.any(Object),
|
||||
{
|
||||
signal,
|
||||
start: 0,
|
||||
end: 1,
|
||||
domain: 'system',
|
||||
timeContext: jasmine.any(Object)
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('do not overwrite existing request options', async () => {
|
||||
it('do not overwrite existing request options', function (done) {
|
||||
telemetryProvider.supportsRequest.and.returnValue(true);
|
||||
telemetryProvider.request.and.returnValue(Promise.resolve([]));
|
||||
telemetryAPI.addProvider(telemetryProvider);
|
||||
|
||||
await telemetryAPI.request(domainObject, {
|
||||
telemetryAPI.request(domainObject, {
|
||||
start: 20,
|
||||
end: 30,
|
||||
domain: 'someDomain'
|
||||
});
|
||||
const { signal } = new AbortController();
|
||||
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(
|
||||
jasmine.any(Object),
|
||||
{
|
||||
start: 20,
|
||||
end: 30,
|
||||
domain: 'someDomain',
|
||||
signal,
|
||||
timeContext: jasmine.any(Object)
|
||||
}
|
||||
);
|
||||
}).then(() => {
|
||||
const { signal } = new AbortController();
|
||||
expect(telemetryProvider.supportsRequest).toHaveBeenCalledWith(
|
||||
jasmine.any(Object),
|
||||
{
|
||||
start: 20,
|
||||
end: 30,
|
||||
domain: 'someDomain',
|
||||
signal
|
||||
}
|
||||
);
|
||||
|
||||
expect(telemetryProvider.request).toHaveBeenCalledWith(
|
||||
jasmine.any(Object),
|
||||
{
|
||||
start: 20,
|
||||
end: 30,
|
||||
domain: 'someDomain',
|
||||
signal,
|
||||
timeContext: jasmine.any(Object)
|
||||
}
|
||||
);
|
||||
expect(telemetryProvider.request).toHaveBeenCalledWith(
|
||||
jasmine.any(Object),
|
||||
{
|
||||
start: 20,
|
||||
end: 30,
|
||||
domain: 'someDomain',
|
||||
signal
|
||||
}
|
||||
);
|
||||
|
||||
}).finally(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('metadata', () => {
|
||||
describe('metadata', function () {
|
||||
let mockMetadata = {};
|
||||
let mockObjectType = {
|
||||
definition: {}
|
||||
};
|
||||
beforeEach(() => {
|
||||
beforeEach(function () {
|
||||
telemetryAPI.addProvider({
|
||||
key: 'mockMetadataProvider',
|
||||
supportsMetadata() {
|
||||
@@ -365,7 +369,7 @@ describe('Telemetry API', () => {
|
||||
openmct.types.get.and.returnValue(mockObjectType);
|
||||
});
|
||||
|
||||
it('respects explicit priority', () => {
|
||||
it('respects explicit priority', function () {
|
||||
mockMetadata.values = [
|
||||
{
|
||||
key: "name",
|
||||
@@ -404,7 +408,7 @@ describe('Telemetry API', () => {
|
||||
expect(value.hints.priority).toBe(index + 1);
|
||||
});
|
||||
});
|
||||
it('if no explicit priority, defaults to order defined', () => {
|
||||
it('if no explicit priority, defaults to order defined', function () {
|
||||
mockMetadata.values = [
|
||||
{
|
||||
key: "name",
|
||||
@@ -431,7 +435,7 @@ describe('Telemetry API', () => {
|
||||
expect(value.key).toBe(mockMetadata.values[index].key);
|
||||
});
|
||||
});
|
||||
it('respects domain priority', () => {
|
||||
it('respects domain priority', function () {
|
||||
mockMetadata.values = [
|
||||
{
|
||||
key: "name",
|
||||
@@ -473,7 +477,7 @@ describe('Telemetry API', () => {
|
||||
expect(values[0].key).toBe('timestamp-local');
|
||||
expect(values[1].key).toBe('timestamp-utc');
|
||||
});
|
||||
it('respects range priority', () => {
|
||||
it('respects range priority', function () {
|
||||
mockMetadata.values = [
|
||||
{
|
||||
key: "name",
|
||||
@@ -515,7 +519,7 @@ describe('Telemetry API', () => {
|
||||
expect(values[0].key).toBe('cos');
|
||||
expect(values[1].key).toBe('sin');
|
||||
});
|
||||
it('respects priority and domain ordering', () => {
|
||||
it('respects priority and domain ordering', function () {
|
||||
mockMetadata.values = [
|
||||
{
|
||||
key: "id",
|
||||
@@ -584,7 +588,7 @@ describe('Telemetry API', () => {
|
||||
definition: {}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(function () {
|
||||
openmct.telemetry = telemetryAPI;
|
||||
telemetryAPI.addProvider({
|
||||
key: 'mockMetadataProvider',
|
||||
@@ -640,14 +644,16 @@ describe('Telemetery', () => {
|
||||
return resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
it('should not abort request without navigation', async () => {
|
||||
it('should not abort request without navigation', function (done) {
|
||||
telemetryAPI.addProvider(telemetryProvider);
|
||||
|
||||
await telemetryAPI.request({});
|
||||
expect(watchedSignal.aborted).toBe(false);
|
||||
telemetryAPI.request({}).finally(() => {
|
||||
expect(watchedSignal.aborted).toBe(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should abort request on navigation', (done) => {
|
||||
it('should abort request on navigation', function (done) {
|
||||
telemetryAPI.addProvider(telemetryProvider);
|
||||
|
||||
telemetryAPI.request({}).finally(() => {
|
||||
|
||||
@@ -229,25 +229,6 @@ describe("The Time API", function () {
|
||||
expect(api.clock()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('Provides a default time context', () => {
|
||||
const timeContext = api.getContextForView([]);
|
||||
expect(timeContext).not.toBe(null);
|
||||
});
|
||||
|
||||
it("Without a clock, is in fixed time mode", () => {
|
||||
const timeContext = api.getContextForView([]);
|
||||
expect(timeContext.isRealTime()).toBe(false);
|
||||
});
|
||||
|
||||
it("Provided a clock, is in real-time mode", () => {
|
||||
const timeContext = api.getContextForView([]);
|
||||
timeContext.clock('mts', {
|
||||
start: 0,
|
||||
end: 1
|
||||
});
|
||||
expect(timeContext.isRealTime()).toBe(true);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it("on tick, observes offsets, and indicates tick in bounds callback", function () {
|
||||
|
||||
@@ -362,18 +362,6 @@ class TimeContext extends EventEmitter {
|
||||
this.boundsVal = newBounds;
|
||||
this.emit('bounds', this.boundsVal, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this time context is in real-time mode or not.
|
||||
* @returns {boolean} true if this context is in real-time mode, false if not
|
||||
*/
|
||||
isRealTime() {
|
||||
if (this.clock()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default TimeContext;
|
||||
|
||||
@@ -114,8 +114,6 @@ export default {
|
||||
this.formats = this.openmct.telemetry.getFormatMap(this.metadata);
|
||||
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
|
||||
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
|
||||
|
||||
this.limitEvaluator = this.openmct
|
||||
.telemetry
|
||||
.limitEvaluator(this.domainObject);
|
||||
@@ -136,8 +134,7 @@ export default {
|
||||
|
||||
this.telemetryCollection = this.openmct.telemetry.requestCollection(this.domainObject, {
|
||||
size: 1,
|
||||
strategy: 'latest',
|
||||
timeContext: this.timeContext
|
||||
strategy: 'latest'
|
||||
});
|
||||
this.telemetryCollection.on('add', this.setLatestValues);
|
||||
this.telemetryCollection.on('clear', this.resetValues);
|
||||
|
||||
@@ -30,12 +30,6 @@
|
||||
padding: $interiorMarginLg $interiorMarginLg * 2;
|
||||
}
|
||||
|
||||
.c-condition-widget__label {
|
||||
padding: $interiorMargin;
|
||||
text-align: center;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
a.c-condition-widget {
|
||||
// Widget is conditionally made into a <a> when URL property has been defined
|
||||
cursor: pointer !important;
|
||||
|
||||
@@ -282,15 +282,12 @@ export default {
|
||||
this.limitEvaluator = this.openmct.telemetry.limitEvaluator(this.domainObject);
|
||||
this.formats = this.openmct.telemetry.getFormatMap(this.metadata);
|
||||
|
||||
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
|
||||
|
||||
const valueMetadata = this.metadata ? this.metadata.value(this.item.value) : {};
|
||||
this.customStringformatter = this.openmct.telemetry.customStringFormatter(valueMetadata, this.item.format);
|
||||
|
||||
this.telemetryCollection = this.openmct.telemetry.requestCollection(this.domainObject, {
|
||||
size: 1,
|
||||
strategy: 'latest',
|
||||
timeContext: this.timeContext
|
||||
strategy: 'latest'
|
||||
});
|
||||
this.telemetryCollection.on('add', this.setLatestValues);
|
||||
this.telemetryCollection.on('clear', this.refreshData);
|
||||
|
||||
@@ -107,7 +107,7 @@ export default class CreateAction extends PropertiesAction {
|
||||
}
|
||||
|
||||
const url = '#/browse/' + objectPath
|
||||
.map(object => object && this.openmct.objects.makeKeyString(object.identifier))
|
||||
.map(object => object && this.openmct.objects.makeKeyString(object.identifier.key))
|
||||
.reverse()
|
||||
.join('/');
|
||||
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
const connections = [];
|
||||
let connected = false;
|
||||
let couchEventSource;
|
||||
let changesFeedUrl;
|
||||
const keepAliveTime = 20 * 1000;
|
||||
let keepAliveTimer;
|
||||
const controller = new AbortController();
|
||||
|
||||
self.onconnect = function (e) {
|
||||
@@ -38,8 +35,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
changesFeedUrl = event.data.url;
|
||||
self.listenForChanges();
|
||||
self.listenForChanges(event.data.url);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -67,28 +63,17 @@
|
||||
});
|
||||
};
|
||||
|
||||
self.listenForChanges = function () {
|
||||
if (keepAliveTimer) {
|
||||
clearTimeout(keepAliveTimer);
|
||||
}
|
||||
self.listenForChanges = function (url) {
|
||||
console.debug('⇿ Opening CouchDB change feed connection ⇿');
|
||||
|
||||
/**
|
||||
* Once the connection has been opened, poll every 20 seconds to see if the EventSource has closed unexpectedly.
|
||||
* If it has, attempt to reconnect.
|
||||
*/
|
||||
keepAliveTimer = setTimeout(self.listenForChanges, keepAliveTime);
|
||||
couchEventSource = new EventSource(url);
|
||||
couchEventSource.onerror = self.onerror;
|
||||
couchEventSource.onopen = self.onopen;
|
||||
|
||||
if (!couchEventSource || couchEventSource.readyState === EventSource.CLOSED) {
|
||||
console.debug('⇿ Opening CouchDB change feed connection ⇿');
|
||||
couchEventSource = new EventSource(changesFeedUrl);
|
||||
couchEventSource.onerror = self.onerror;
|
||||
couchEventSource.onopen = self.onopen;
|
||||
|
||||
// start listening for events
|
||||
couchEventSource.addEventListener('message', self.onCouchMessage);
|
||||
connected = true;
|
||||
console.debug('⇿ Opened connection ⇿');
|
||||
}
|
||||
// start listening for events
|
||||
couchEventSource.addEventListener('message', self.onCouchMessage);
|
||||
connected = true;
|
||||
console.debug('⇿ Opened connection ⇿');
|
||||
};
|
||||
|
||||
self.updateCouchStateIndicator = function () {
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
# Plan view and domain objects
|
||||
Plans provide a view for a list of activities grouped by categories.
|
||||
|
||||
## Plan category and activity JSON format
|
||||
The JSON format for a plan consists of categories/groups and a list of activities for each category.
|
||||
Activity properties include:
|
||||
* name: Name of the activity
|
||||
* start: Timestamps in milliseconds
|
||||
* end: Timestamps in milliseconds
|
||||
* type: Matches the name of the category it is in
|
||||
* color: Background color for the activity
|
||||
* textColor: Color of the name text for the activity
|
||||
* The format of the json file is as follows:
|
||||
|
||||
```json
|
||||
{
|
||||
"TEST_GROUP": [{
|
||||
"name": "Event 1 with a really long name",
|
||||
"start": 1665323197000,
|
||||
"end": 1665344921000,
|
||||
"type": "TEST_GROUP",
|
||||
"color": "orange",
|
||||
"textColor": "white"
|
||||
}],
|
||||
"GROUP_2": [{
|
||||
"name": "Event 2",
|
||||
"start": 1665409597000,
|
||||
"end": 1665456252000,
|
||||
"type": "GROUP_2",
|
||||
"color": "red",
|
||||
"textColor": "white"
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
## Plans using JSON file uploads
|
||||
Plan domain objects can be created by uploading a JSON file with the format above to render categories and activities.
|
||||
|
||||
## Using Domain Objects directly
|
||||
If uploading a JSON is not desired, it is possible to visualize domain objects of type 'plan'.
|
||||
The standard model is as follows:
|
||||
```javascript
|
||||
{
|
||||
identifier: {
|
||||
namespace: ""
|
||||
key: "test-plan"
|
||||
}
|
||||
name:"A plan object",
|
||||
type:"plan",
|
||||
location:"ROOT",
|
||||
selectFile: {
|
||||
body: {
|
||||
SOME_CATEGORY: [{
|
||||
name: "An activity",
|
||||
start: 1665323197000,
|
||||
end: 1665323197100,
|
||||
type: "SOME_CATEGORY"
|
||||
}
|
||||
],
|
||||
ANOTHER_CATEGORY: [{
|
||||
name: "An activity",
|
||||
start: 1665323197000,
|
||||
end: 1665323197100,
|
||||
type: "ANOTHER_CATEGORY"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If your data has non-standard keys for `start, end, type and activities` properties, use the `sourceMap` property mapping.
|
||||
```javascript
|
||||
{
|
||||
identifier: {
|
||||
namespace: ""
|
||||
key: "another-test-plan"
|
||||
}
|
||||
name:"Another plan object",
|
||||
type:"plan",
|
||||
location:"ROOT",
|
||||
sourceMap: {
|
||||
start: 'start_time',
|
||||
end: 'end_time',
|
||||
activities: 'items',
|
||||
groupId: 'category'
|
||||
},
|
||||
selectFile: {
|
||||
body: {
|
||||
items: [
|
||||
{
|
||||
name: "An activity",
|
||||
start_time: 1665323197000,
|
||||
end_time: 1665323197100,
|
||||
category: "SOME_CATEGORY"
|
||||
},
|
||||
{
|
||||
name: "Another activity",
|
||||
start_time: 1665323198000,
|
||||
end_time: 1665323198100,
|
||||
category: "ANOTHER_CATEGORY"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Rendering categories and activities:
|
||||
The algorithm to render categories and activities on a canvas is as follows:
|
||||
* Each category gets a swimlane.
|
||||
* Activities within a category are rendered within it's swimlane.
|
||||
* Render each activity on a given row if it's duration+label do not overlap (start/end times) with an existing activity on that row.
|
||||
* Move to the next available row within a swimlane if there is overlap
|
||||
* Labels for a given activity will be rendered within it's duration slot if it fits in that rectangular space.
|
||||
* Labels that do not fit within an activity's duration slot are rendered outside, to the right of the duration slot.
|
||||
|
||||
@@ -264,7 +264,7 @@ describe('the plugin', function () {
|
||||
it('provides an inspector view with the version information if available', () => {
|
||||
componentObject = component.$root.$children[0];
|
||||
const propertiesEls = componentObject.$el.querySelectorAll('.c-inspect-properties__row');
|
||||
expect(propertiesEls.length).toEqual(6);
|
||||
expect(propertiesEls.length).toEqual(4);
|
||||
const found = Array.from(propertiesEls).some((propertyEl) => {
|
||||
return (propertyEl.children[0].innerHTML.trim() === 'Version'
|
||||
&& propertyEl.children[1].innerHTML.trim() === 'v1');
|
||||
|
||||
@@ -382,7 +382,7 @@ export default {
|
||||
makeLimitLines(series) {
|
||||
this.clearLimitLines(series);
|
||||
|
||||
if (!series || !series.get('limitLines')) {
|
||||
if (!series.get('limitLines')) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -151,6 +151,16 @@ export default class PlotSeries extends Model {
|
||||
this.limitEvaluator = this.openmct.telemetry.limitEvaluator(options.domainObject);
|
||||
this.limitDefinition = this.openmct.telemetry.limitDefinition(options.domainObject);
|
||||
this.limits = [];
|
||||
this.limitDefinition.limits().then(response => {
|
||||
this.limits = [];
|
||||
|
||||
if (response) {
|
||||
this.limits = response;
|
||||
}
|
||||
|
||||
this.emit('limits', this);
|
||||
|
||||
});
|
||||
this.openmct.time.on('bounds', this.updateLimits);
|
||||
this.removeMutationListener = this.openmct.objects.observe(
|
||||
this.domainObject,
|
||||
@@ -166,6 +176,15 @@ export default class PlotSeries extends Model {
|
||||
this.emit('limitBounds', bounds);
|
||||
}
|
||||
|
||||
locateOldObject(oldStyleParent) {
|
||||
return oldStyleParent.useCapability('composition')
|
||||
.then(function (children) {
|
||||
this.oldObject = children
|
||||
.filter(function (child) {
|
||||
return child.getId() === this.keyString;
|
||||
}, this)[0];
|
||||
}.bind(this));
|
||||
}
|
||||
/**
|
||||
* Fetch historical data and establish a realtime subscription. Returns
|
||||
* a promise that is resolved when all connections have been successfully
|
||||
@@ -173,7 +192,7 @@ export default class PlotSeries extends Model {
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async fetch(options) {
|
||||
fetch(options) {
|
||||
let strategy;
|
||||
|
||||
if (this.model.interpolate !== 'none') {
|
||||
@@ -198,19 +217,23 @@ export default class PlotSeries extends Model {
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const points = await this.openmct.telemetry.request(this.domainObject, options);
|
||||
const data = this.getSeriesData();
|
||||
// eslint-disable-next-line you-dont-need-lodash-underscore/concat
|
||||
const newPoints = _(data)
|
||||
.concat(points)
|
||||
.sortBy(this.getXVal)
|
||||
.uniq(true, point => [this.getXVal(point), this.getYVal(point)].join())
|
||||
.value();
|
||||
this.reset(newPoints);
|
||||
} catch (error) {
|
||||
console.warn('Error fetching data', error);
|
||||
}
|
||||
/* eslint-disable you-dont-need-lodash-underscore/concat */
|
||||
return this.openmct
|
||||
.telemetry
|
||||
.request(this.domainObject, options)
|
||||
.then((points) => {
|
||||
const data = this.getSeriesData();
|
||||
const newPoints = _(data)
|
||||
.concat(points)
|
||||
.sortBy(this.getXVal)
|
||||
.uniq(true, point => [this.getXVal(point), this.getYVal(point)].join())
|
||||
.value();
|
||||
this.reset(newPoints);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn('Error fetching data', error);
|
||||
});
|
||||
/* eslint-enable you-dont-need-lodash-underscore/concat */
|
||||
}
|
||||
|
||||
updateName(name) {
|
||||
@@ -311,19 +334,16 @@ export default class PlotSeries extends Model {
|
||||
* Override this to implement plot series loading functionality. Must return
|
||||
* a promise that is resolved when loading is completed.
|
||||
*
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async load(options) {
|
||||
await this.fetch(options);
|
||||
this.emit('load');
|
||||
const limitsResponse = await this.limitDefinition.limits();
|
||||
this.limits = [];
|
||||
if (limitsResponse) {
|
||||
this.limits = limitsResponse;
|
||||
}
|
||||
load(options) {
|
||||
return this.fetch(options)
|
||||
.then(function (res) {
|
||||
this.emit('load');
|
||||
|
||||
this.emit('limits', this);
|
||||
this.emit('change:limitLines', this);
|
||||
return res;
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,12 +19,8 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
export default class RemoveAction {
|
||||
#transaction;
|
||||
|
||||
constructor(openmct) {
|
||||
|
||||
this.name = 'Remove';
|
||||
this.key = 'remove';
|
||||
this.description = 'Remove this object from its containing object.';
|
||||
@@ -33,25 +29,17 @@ export default class RemoveAction {
|
||||
this.priority = 1;
|
||||
|
||||
this.openmct = openmct;
|
||||
|
||||
this.removeFromComposition = this.removeFromComposition.bind(this); // for access to private transaction variable
|
||||
}
|
||||
|
||||
async invoke(objectPath) {
|
||||
invoke(objectPath) {
|
||||
let object = objectPath[0];
|
||||
let parent = objectPath[1];
|
||||
|
||||
try {
|
||||
await this.showConfirmDialog(object);
|
||||
} catch (error) {
|
||||
return; // form canceled, exit invoke
|
||||
}
|
||||
|
||||
await this.removeFromComposition(parent, object);
|
||||
|
||||
if (this.inNavigationPath(object)) {
|
||||
this.navigateTo(objectPath.slice(1));
|
||||
}
|
||||
this.showConfirmDialog(object).then(() => {
|
||||
this.removeFromComposition(parent, object);
|
||||
if (this.inNavigationPath(object)) {
|
||||
this.navigateTo(objectPath.slice(1));
|
||||
}
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
showConfirmDialog(object) {
|
||||
@@ -93,21 +81,20 @@ export default class RemoveAction {
|
||||
this.openmct.router.navigate('#/browse/' + urlPath);
|
||||
}
|
||||
|
||||
async removeFromComposition(parent, child) {
|
||||
this.startTransaction();
|
||||
removeFromComposition(parent, child) {
|
||||
let composition = parent.composition.filter(id =>
|
||||
!this.openmct.objects.areIdsEqual(id, child.identifier)
|
||||
);
|
||||
|
||||
const composition = this.openmct.composition.get(parent);
|
||||
composition.remove(child);
|
||||
|
||||
if (!this.isAlias(child, parent)) {
|
||||
this.openmct.objects.mutate(child, 'location', null);
|
||||
}
|
||||
this.openmct.objects.mutate(parent, 'composition', composition);
|
||||
|
||||
if (this.inNavigationPath(child) && this.openmct.editor.isEditing()) {
|
||||
this.openmct.editor.save();
|
||||
}
|
||||
|
||||
await this.saveTransaction();
|
||||
if (!this.isAlias(child, parent)) {
|
||||
this.openmct.objects.mutate(child, 'location', null);
|
||||
}
|
||||
}
|
||||
|
||||
isAlias(child, parent) {
|
||||
@@ -145,23 +132,4 @@ export default class RemoveAction {
|
||||
&& parentType.definition.creatable
|
||||
&& Array.isArray(parent.composition);
|
||||
}
|
||||
|
||||
startTransaction() {
|
||||
if (!this.openmct.objects.isTransactionActive()) {
|
||||
this.#transaction = this.openmct.objects.startTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
saveTransaction() {
|
||||
if (!this.#transaction) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.#transaction.commit()
|
||||
.catch(error => {
|
||||
throw error;
|
||||
}).finally(() => {
|
||||
this.openmct.objects.endTransaction();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,8 +38,6 @@ describe('the inspector', () => {
|
||||
folderItem = {
|
||||
name: 'folder',
|
||||
type: 'folder',
|
||||
createdBy: 'John Q',
|
||||
modifiedBy: 'Public',
|
||||
id: 'mock-folder-key',
|
||||
identifier: {
|
||||
namespace: '',
|
||||
@@ -76,8 +74,6 @@ describe('the inspector', () => {
|
||||
const [
|
||||
title,
|
||||
type,
|
||||
createdBy,
|
||||
modifiedBy,
|
||||
notes,
|
||||
timestamp
|
||||
] = details;
|
||||
@@ -91,14 +87,6 @@ describe('the inspector', () => {
|
||||
.toEqual('Type');
|
||||
expect(type.value.toLowerCase())
|
||||
.toEqual(folderItem.type);
|
||||
expect(createdBy.name)
|
||||
.toEqual('Created By');
|
||||
expect(createdBy.value)
|
||||
.toEqual(folderItem.createdBy);
|
||||
expect(modifiedBy.name)
|
||||
.toEqual('Modified By');
|
||||
expect(modifiedBy.value)
|
||||
.toEqual(folderItem.modifiedBy);
|
||||
expect(notes.value)
|
||||
.toEqual('This object should have some notes');
|
||||
|
||||
|
||||
@@ -90,13 +90,10 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
const UNKNOWN_USER = 'Unknown';
|
||||
const title = this.domainObject.name;
|
||||
const typeName = this.type ? this.type.definition.name : `Unknown: ${this.domainObject.type}`;
|
||||
const createdTimestamp = this.domainObject.created;
|
||||
const createdBy = this.domainObject.createdBy ? this.domainObject.createdBy : UNKNOWN_USER;
|
||||
const modifiedBy = this.domainObject.modifiedBy ? this.domainObject.modifiedBy : UNKNOWN_USER;
|
||||
const modifiedTimestamp = this.domainObject.modified ? this.domainObject.modified : this.domainObject.created;
|
||||
const timestampLabel = this.domainObject.modified ? 'Modified' : 'Created';
|
||||
const timestamp = this.domainObject.modified ? this.domainObject.modified : this.domainObject.created;
|
||||
const notes = this.domainObject.notes;
|
||||
const version = this.domainObject.version;
|
||||
|
||||
@@ -108,14 +105,6 @@ export default {
|
||||
{
|
||||
name: 'Type',
|
||||
value: typeName
|
||||
},
|
||||
{
|
||||
name: 'Created By',
|
||||
value: createdBy
|
||||
},
|
||||
{
|
||||
name: 'Modified By',
|
||||
value: modifiedBy
|
||||
}
|
||||
];
|
||||
|
||||
@@ -126,28 +115,15 @@ export default {
|
||||
});
|
||||
}
|
||||
|
||||
if (createdTimestamp !== undefined) {
|
||||
const formattedCreatedTimestamp = Moment.utc(createdTimestamp)
|
||||
if (timestamp !== undefined) {
|
||||
const formattedTimestamp = Moment.utc(timestamp)
|
||||
.format('YYYY-MM-DD[\n]HH:mm:ss')
|
||||
+ ' UTC';
|
||||
|
||||
details.push(
|
||||
{
|
||||
name: 'Created',
|
||||
value: formattedCreatedTimestamp
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (modifiedTimestamp !== undefined) {
|
||||
const formattedModifiedTimestamp = Moment.utc(modifiedTimestamp)
|
||||
.format('YYYY-MM-DD[\n]HH:mm:ss')
|
||||
+ ' UTC';
|
||||
|
||||
details.push(
|
||||
{
|
||||
name: 'Modified',
|
||||
value: formattedModifiedTimestamp
|
||||
name: timestampLabel,
|
||||
value: formattedTimestamp
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,19 +36,9 @@ module.exports = merge(common, {
|
||||
],
|
||||
devtool: 'eval-source-map',
|
||||
devServer: {
|
||||
devMiddleware: {
|
||||
writeToDisk: (filePathString) => {
|
||||
const filePath = path.parse(filePathString);
|
||||
const shouldWrite = !(filePath.base.includes('hot-update'));
|
||||
|
||||
return shouldWrite;
|
||||
}
|
||||
},
|
||||
watchFiles: ['**/*.css'],
|
||||
static: {
|
||||
directory: path.join(__dirname, '/dist'),
|
||||
publicPath: '/dist',
|
||||
watch: false
|
||||
directory: path.join(__dirname, '/dist/'),
|
||||
publicPath: '/dist'
|
||||
},
|
||||
client: {
|
||||
progress: true,
|
||||
|
||||
Reference in New Issue
Block a user