Compare commits

..

5 Commits

Author SHA1 Message Date
Scott Bell
db495ea12e If no matching tags, do not attempt tag search (#5839)
* do not attempt search if no matching tags

* fix timing on test

* commit again in hopes that github will run checks

* add back null tag check

* add some better documentation to tests

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2022-10-06 15:33:51 -07:00
David Tsay
9022d5d859 Keep transaction open on failed editor save (#5840)
* do not end a transaction on a failed editor save
* add unit tests for successful editor save and unsuccessful editor save
2022-10-06 12:21:44 -07:00
Jamie V
8fed722f72 [Overlay Plot] Inspector series and legend sync fix (#5835)
* fixed overlay plots to react to series removals correctly, added alias visual to elements pool aliased items
2022-10-04 10:23:31 -07:00
Jesse Mazzella
9a64220db4 Add aria-label for time conductor history button (#5830) 2022-10-03 16:49:00 +00:00
Scott Bell
eeba4703dd Don't delete annotations if there aren't any (#5829)
* don't delete annotations if there aren't any

* add test and align playwright-test

* align core with test

* added annotation describing test
2022-10-03 09:42:43 -07:00
28 changed files with 421 additions and 2798 deletions

View File

@@ -1 +0,0 @@
name: 'Custom CodeQL config'

View File

@@ -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:

View File

@@ -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
View 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

View File

@@ -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

View File

@@ -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

View File

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

View File

@@ -94,6 +94,7 @@ describe("The Annotation API", () => {
openmct.startHeadless();
});
afterEach(async () => {
openmct.objects.providers = {};
await resetApplicationState(openmct);
});
it("is defined", () => {

View File

@@ -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) {

View File

@@ -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 =

View File

@@ -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 () {

View File

@@ -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(() => {

View File

@@ -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 () {

View File

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

View File

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

View File

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

View File

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

View File

@@ -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('/');

View File

@@ -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 () {

View File

@@ -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.

View File

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

View File

@@ -382,7 +382,7 @@ export default {
makeLimitLines(series) {
this.clearLimitLines(series);
if (!series || !series.get('limitLines')) {
if (!series.get('limitLines')) {
return;
}

View File

@@ -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));
}
/**

View File

@@ -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();
});
}
}

View File

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

View File

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

View File

@@ -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,