Compare commits

..

9 Commits

Author SHA1 Message Date
Joshi
200e8eecce Addresses review comments 2021-05-28 11:38:33 -07:00
Joshi
7a1e7edb79 Merge branch 'master' of https://github.com/nasa/openmct into fix-plots-view-large-request 2021-05-28 11:37:26 -07:00
Nikhil
e1e0eeac56 upgrade to webpack5 (#3871)
Upgrade to webpack 5
Changes dependencies to work with webpack 5 as well.
2021-05-27 15:16:03 -07:00
Nikhil
c90dfb2a1f Fix the browser back button in Open MCT (#3526)
Fixes Open MCT back button.

Co-authored-by: Joshi <simplyrender@gmail.com>
2021-05-26 17:00:36 -07:00
Joshi
53232a1c70 Fix failing test 2021-05-26 10:39:04 -07:00
Joshi
35b6952cc1 Use resize obeserver to detect a change in the parent container's size for plots and re-request telemetry 2021-05-25 14:47:42 -07:00
Shefali Joshi
1dfa5e5b8c Prepare snapshot for sprint 1.7.3 (#3877)
Co-authored-by: John Hill <jchill2.spam@gmail.com>
2021-05-24 10:54:51 -07:00
Artur Carvalho
99896b72ea Small typo (#3838)
Looks like there is a small typo: `this this object`.

Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
2021-05-21 15:57:53 -07:00
Charles Hacskaylo
979ba77c8e Normalize "OK" to uppercase in all dialogs; (#3850)
Co-authored-by: Jamie V <jamie.j.vigliotta@nasa.gov>
2021-05-21 15:38:29 -07:00
55 changed files with 9450 additions and 820 deletions

2
.gitignore vendored
View File

@@ -39,5 +39,3 @@ npm-debug.log
# karma reports
report.*.json
package-lock.json

2
API.md
View File

@@ -423,7 +423,7 @@ attribute | type | flags | notes
###### Value Hints
Each telemetry value description has an object defining hints. Keys in this this object represent the hint itself, and the value represents the weight of that hint. A lower weight means the hint has a higher priority. For example, multiple values could be hinted for use as the y-axis of a plot (raw, engineering), but the highest priority would be the default choice. Likewise, a table will use hints to determine the default order of columns.
Each telemetry value description has an object defining hints. Keys in this object represent the hint itself, and the value represents the weight of that hint. A lower weight means the hint has a higher priority. For example, multiple values could be hinted for use as the y-axis of a plot (raw, engineering), but the highest priority would be the default choice. Likewise, a table will use hints to determine the default order of columns.
Known hints:

10
app.js
View File

@@ -7,6 +7,14 @@
* node app.js [options]
*/
class WatchRunPlugin {
apply(compiler) {
compiler.hooks.emit.tapAsync('WatchRunPlugin', (compilation, callback) => {
console.log('Begin compile at ' + new Date());
callback();
});
}
}
const options = require('minimist')(process.argv.slice(2));
const express = require('express');
@@ -43,7 +51,7 @@ app.use('/proxyUrl', function proxyRequest(req, res, next) {
const webpack = require('webpack');
const webpackConfig = require('./webpack.config.js');
webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin());
webpackConfig.plugins.push(function() { this.plugin('watch-run', function(watching, callback) { console.log('Begin compile at ' + new Date()); callback(); }) });
webpackConfig.plugins.push(new WatchRunPlugin());
webpackConfig.entry.openmct = [
'webpack-hot-middleware/client?reload=true',

View File

@@ -78,6 +78,7 @@ module.exports = (config) => {
preserveDescribeNesting: true,
foldAll: false
},
browserConsoleLogOptions: { level: "error", format: "%b %T: %m", terminal: true },
coverageIstanbulReporter: {
fixWebpackSourcePaths: true,
dir: process.env.CIRCLE_ARTIFACTS ?

View File

@@ -21,11 +21,6 @@
*****************************************************************************/
/*global module*/
const LogRocket = require('logrocket');
// Instrumented
LogRocket.init('omjbg1/openmct-demo');
const matcher = /\/openmct.js$/;
if (document.currentScript) {
let src = document.currentScript.src;

8613
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,17 +1,15 @@
{
"name": "openmct",
"version": "1.7.1-SNAPSHOT",
"version": "1.7.3-SNAPSHOT",
"description": "The Open MCT core platform",
"dependencies": {
"logrocket": "^1.2.0"
},
"dependencies": {},
"devDependencies": {
"angular": ">=1.8.0",
"angular-route": "1.4.14",
"babel-eslint": "10.0.3",
"comma-separated-values": "^3.6.4",
"concurrently": "^3.6.1",
"copy-webpack-plugin": "^4.5.2",
"copy-webpack-plugin": "^9.0.0",
"cross-env": "^6.0.3",
"css-loader": "^1.0.0",
"d3-array": "1.2.x",
@@ -43,19 +41,19 @@
"jsdoc": "^3.3.2",
"karma": "5.1.1",
"karma-chrome-launcher": "3.1.0",
"karma-firefox-launcher": "1.3.0",
"karma-cli": "2.0.0",
"karma-coverage": "2.0.3",
"karma-coverage-istanbul-reporter": "3.0.3",
"karma-firefox-launcher": "1.3.0",
"karma-html-reporter": "0.2.7",
"karma-jasmine": "3.3.1",
"karma-sourcemap-loader": "0.3.7",
"karma-webpack": "4.0.2",
"karma-webpack": "^5.0.0",
"location-bar": "^3.0.1",
"lodash": "^4.17.12",
"markdown-toc": "^0.11.7",
"marked": "^0.3.5",
"mini-css-extract-plugin": "^0.4.1",
"mini-css-extract-plugin": "^1.6.0",
"minimist": "^1.2.5",
"moment": "2.25.3",
"moment-duration-format": "^2.2.2",
@@ -71,16 +69,17 @@
"uuid": "^3.3.3",
"v8-compile-cache": "^1.1.0",
"vue": "2.5.6",
"vue-loader": "^15.2.6",
"vue-loader": "^15.9.7",
"vue-template-compiler": "2.5.6",
"webpack": "^4.16.2",
"webpack-cli": "^3.1.0",
"webpack": "^5.37.0",
"webpack-cli": "^3.3.12",
"webpack-dev-middleware": "^3.1.3",
"webpack-hot-middleware": "^2.22.3",
"zepto": "^1.2.0"
},
"scripts": {
"clean": "rm -rf ./dist",
"clean": "rm -rf ./dist /node_modules; rm package-lock.json",
"clean-test-lint": "npm run clean; npm install ; npm run test; npm run lint",
"start": "node app.js",
"lint": "eslint platform example src --ext .js,.vue openmct.js",
"lint:fix": "eslint platform example src --ext .js,.vue openmct.js --fix",

View File

@@ -86,7 +86,7 @@ define(
})
.join('/');
window.location.href = url;
openmct.router.navigate(url);
if (isFirstViewEditable(object.useCapability('adapter'), objectPath)) {
openmct.editor.edit();

View File

@@ -252,7 +252,7 @@ define([
this.status = new api.StatusAPI(this);
this.router = new ApplicationRouter();
this.router = new ApplicationRouter(this);
this.branding = BrandingAPI.default;

View File

@@ -119,7 +119,8 @@ describe('The ActionCollection', () => {
afterEach(() => {
actionCollection.destroy();
resetApplicationState(openmct);
return resetApplicationState(openmct);
});
describe("disable method invoked with action keys", () => {

View File

@@ -99,7 +99,7 @@ describe('The Actions API', () => {
});
afterEach(() => {
resetApplicationState(openmct);
return resetApplicationState(openmct);
});
describe("register method", () => {

View File

@@ -76,7 +76,7 @@ describe ('The Menu API', () => {
});
afterEach(() => {
resetApplicationState(openmct);
return resetApplicationState(openmct);
});
describe("showMenu method", () => {

View File

@@ -22,7 +22,7 @@ describe("The Status API", () => {
});
afterEach(() => {
resetApplicationState(openmct);
return resetApplicationState(openmct);
});
describe("set function", () => {

View File

@@ -292,6 +292,11 @@ describe("The LAD Table Set", () => {
});
afterEach(() => {
openmct.time.timeSystem('utc', {
start: 0,
end: 1
});
return resetApplicationState(openmct);
});

View File

@@ -19,10 +19,6 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import {
getAllSearchParams,
setAllSearchParams
} from 'utils/openmctLocation';
const TIME_EVENTS = ['timeSystem', 'clock', 'clockOffsets'];
const SEARCH_MODE = 'tc.mode';
@@ -49,9 +45,8 @@ export default class URLTimeSettingsSynchronizer {
}
initialize() {
this.updateTimeSettings();
this.openmct.router.on('change:params', this.updateTimeSettings);
window.addEventListener('hashchange', this.updateTimeSettings);
TIME_EVENTS.forEach(event => {
this.openmct.time.on(event, this.setUrlFromTimeApi);
});
@@ -59,7 +54,8 @@ export default class URLTimeSettingsSynchronizer {
}
destroy() {
window.removeEventListener('hashchange', this.updateTimeSettings);
this.openmct.router.off('change:params', this.updateTimeSettings);
this.openmct.off('start', this.initialize);
this.openmct.off('destroy', this.destroy);
@@ -70,22 +66,18 @@ export default class URLTimeSettingsSynchronizer {
}
updateTimeSettings() {
// Prevent from triggering self
if (!this.isUrlUpdateInProgress) {
let timeParameters = this.parseParametersFromUrl();
let timeParameters = this.parseParametersFromUrl();
if (this.areTimeParametersValid(timeParameters)) {
this.setTimeApiFromUrl(timeParameters);
} else {
this.setUrlFromTimeApi();
}
if (this.areTimeParametersValid(timeParameters)) {
this.setTimeApiFromUrl(timeParameters);
this.openmct.router.setLocationFromUrl();
} else {
this.isUrlUpdateInProgress = false;
this.setUrlFromTimeApi();
}
}
parseParametersFromUrl() {
let searchParams = getAllSearchParams();
let searchParams = this.openmct.router.getAllSearchParams();
let mode = searchParams.get(SEARCH_MODE);
let timeSystem = searchParams.get(SEARCH_TIME_SYSTEM);
@@ -148,7 +140,7 @@ export default class URLTimeSettingsSynchronizer {
}
setUrlFromTimeApi() {
let searchParams = getAllSearchParams();
let searchParams = this.openmct.router.getAllSearchParams();
let clock = this.openmct.time.clock();
let bounds = this.openmct.time.bounds();
let clockOffsets = this.openmct.time.clockOffsets();
@@ -176,8 +168,7 @@ export default class URLTimeSettingsSynchronizer {
}
searchParams.set(SEARCH_TIME_SYSTEM, this.openmct.time.timeSystem().key);
this.isUrlUpdateInProgress = true;
setAllSearchParams(searchParams);
this.openmct.router.setAllSearchParams(searchParams);
}
areTimeParametersValid(timeParameters) {

View File

@@ -25,306 +25,118 @@ import {
} from 'utils/testing';
describe("The URLTimeSettingsSynchronizer", () => {
let appHolder;
let openmct;
let testClock;
let resolveFunction;
let oldHash;
beforeEach((done) => {
openmct = createOpenMct();
openmct.install(openmct.plugins.MyItems());
openmct.install(openmct.plugins.LocalTimeSystem());
testClock = jasmine.createSpyObj("testClock", ["start", "stop", "tick", "currentValue", "on", "off"]);
testClock.key = "test-clock";
testClock.currentValue.and.returnValue(0);
openmct.time.addClock(testClock);
openmct.install(openmct.plugins.UTCTimeSystem());
openmct.on('start', done);
openmct.startHeadless();
appHolder = document.createElement("div");
openmct.start(appHolder);
});
afterEach(() => resetApplicationState(openmct));
afterEach(() => {
openmct.time.stopClock();
openmct.router.removeListener('change:hash', resolveFunction);
describe("realtime mode", () => {
it("when the clock is set via the time API, it is immediately reflected in the URL", () => {
//Test expected initial conditions
appHolder = undefined;
openmct = undefined;
resolveFunction = undefined;
return resetApplicationState(openmct);
});
it("initial clock is set to fixed is reflected in URL", (done) => {
resolveFunction = () => {
oldHash = window.location.hash;
expect(window.location.hash.includes('tc.mode=fixed')).toBe(true);
openmct.router.removeListener('change:hash', resolveFunction);
done();
};
openmct.router.on('change:hash', resolveFunction);
});
it("when the clock is set via the time API, it is reflected in the URL", (done) => {
let success;
resolveFunction = () => {
openmct.time.clock('local', {
start: -1000,
end: 100
});
expect(window.location.hash.includes('tc.mode=local')).toBe(true);
//Test that expected initial conditions are no longer true
expect(window.location.hash.includes('tc.mode=fixed')).toBe(false);
});
it("when offsets are set via the time API, they are immediately reflected in the URL", () => {
//Test expected initial conditions
expect(window.location.hash.includes('tc.startDelta')).toBe(false);
expect(window.location.hash.includes('tc.endDelta')).toBe(false);
openmct.time.clock('local', {
start: -1000,
end: 100
});
expect(window.location.hash.includes('tc.startDelta=1000')).toBe(true);
expect(window.location.hash.includes('tc.endDelta=100')).toBe(true);
openmct.time.clockOffsets({
start: -2000,
end: 200
});
expect(window.location.hash.includes('tc.startDelta=2000')).toBe(true);
expect(window.location.hash.includes('tc.endDelta=200')).toBe(true);
//Test that expected initial conditions are no longer true
expect(window.location.hash.includes('tc.mode=fixed')).toBe(false);
});
describe("when set in the url", () => {
it("will change from fixed to realtime mode when the mode changes", () => {
expectLocationToBeInFixedMode();
const hasStartDelta = window.location.hash.includes('tc.startDelta=2000');
const hasEndDelta = window.location.hash.includes('tc.endDelta=200');
const hasLocalClock = window.location.hash.includes('tc.mode=local');
success = hasStartDelta && hasEndDelta && hasLocalClock;
if (success) {
expect(success).toBe(true);
return switchToRealtimeMode().then(() => {
let clock = openmct.time.clock();
openmct.router.removeListener('change:hash', resolveFunction);
done();
}
};
expect(clock).toBeDefined();
expect(clock.key).toBe('local');
});
});
it("the clock is correctly set in the API from the URL parameters", () => {
return switchToRealtimeMode().then(() => {
let resolveFunction;
return new Promise((resolve) => {
resolveFunction = resolve;
//The 'hashchange' event appears to be asynchronous, so we need to wait until a clock change has been
//detected in the API.
openmct.time.on('clock', resolveFunction);
let hash = window.location.hash;
hash = hash.replace('tc.mode=local', 'tc.mode=test-clock');
window.location.hash = hash;
}).then(() => {
let clock = openmct.time.clock();
expect(clock).toBeDefined();
expect(clock.key).toBe('test-clock');
openmct.time.off('clock', resolveFunction);
});
});
});
it("the clock offsets are correctly set in the API from the URL parameters", () => {
return switchToRealtimeMode().then(() => {
let resolveFunction;
return new Promise((resolve) => {
resolveFunction = resolve;
//The 'hashchange' event appears to be asynchronous, so we need to wait until a clock change has been
//detected in the API.
openmct.time.on('clockOffsets', resolveFunction);
let hash = window.location.hash;
hash = hash.replace('tc.startDelta=1000', 'tc.startDelta=2000');
hash = hash.replace('tc.endDelta=100', 'tc.endDelta=200');
window.location.hash = hash;
}).then(() => {
let clockOffsets = openmct.time.clockOffsets();
expect(clockOffsets).toBeDefined();
expect(clockOffsets.start).toBe(-2000);
expect(clockOffsets.end).toBe(200);
openmct.time.off('clockOffsets', resolveFunction);
});
});
});
it("the time system is correctly set in the API from the URL parameters", () => {
return switchToRealtimeMode().then(() => {
let resolveFunction;
return new Promise((resolve) => {
resolveFunction = resolve;
//The 'hashchange' event appears to be asynchronous, so we need to wait until a clock change has been
//detected in the API.
openmct.time.on('timeSystem', resolveFunction);
let hash = window.location.hash;
hash = hash.replace('tc.timeSystem=utc', 'tc.timeSystem=local');
window.location.hash = hash;
}).then(() => {
let timeSystem = openmct.time.timeSystem();
expect(timeSystem).toBeDefined();
expect(timeSystem.key).toBe('local');
openmct.time.off('timeSystem', resolveFunction);
});
});
});
});
});
describe("fixed timespan mode", () => {
beforeEach(() => {
openmct.time.stopClock();
openmct.time.timeSystem('utc', {
start: 0,
end: 1
});
});
it("when bounds are set via the time API, they are immediately reflected in the URL", () => {
//Test expected initial conditions
expect(window.location.hash.includes('tc.startBound=0')).toBe(true);
expect(window.location.hash.includes('tc.endBound=1')).toBe(true);
openmct.time.bounds({
start: 10,
end: 20
});
expect(window.location.hash.includes('tc.startBound=10')).toBe(true);
expect(window.location.hash.includes('tc.endBound=20')).toBe(true);
//Test that expected initial conditions are no longer true
expect(window.location.hash.includes('tc.startBound=0')).toBe(false);
expect(window.location.hash.includes('tc.endBound=1')).toBe(false);
});
it("when time system is set via the time API, it is immediately reflected in the URL", () => {
//Test expected initial conditions
expect(window.location.hash.includes('tc.timeSystem=utc')).toBe(true);
openmct.time.timeSystem('local', {
start: 20,
end: 30
});
expect(window.location.hash.includes('tc.timeSystem=local')).toBe(true);
//Test that expected initial conditions are no longer true
expect(window.location.hash.includes('tc.timeSystem=utc')).toBe(false);
});
describe("when set in the url", () => {
it("time system changes are reflected in the API", () => {
let resolveFunction;
return new Promise((resolve) => {
let timeSystem = openmct.time.timeSystem();
resolveFunction = resolve;
expect(timeSystem.key).toBe('utc');
window.location.hash = window.location.hash.replace('tc.timeSystem=utc', 'tc.timeSystem=local');
openmct.time.on('timeSystem', resolveFunction);
}).then(() => {
let timeSystem = openmct.time.timeSystem();
expect(timeSystem.key).toBe('local');
openmct.time.off('timeSystem', resolveFunction);
});
});
it("mode can be changed from realtime to fixed", () => {
return switchToRealtimeMode().then(() => {
expectLocationToBeInRealtimeMode();
expect(openmct.time.clock()).toBeDefined();
}).then(switchToFixedMode).then(() => {
let clock = openmct.time.clock();
expect(clock).not.toBeDefined();
});
});
it("bounds are correctly set in the API from the URL parameters", () => {
let resolveFunction;
expectLocationToBeInFixedMode();
return new Promise((resolve) => {
resolveFunction = resolve;
openmct.time.on('bounds', resolveFunction);
let hash = window.location.hash;
hash = hash.replace('tc.startBound=0', 'tc.startBound=222')
.replace('tc.endBound=1', 'tc.endBound=333');
window.location.hash = hash;
}).then(() => {
let bounds = openmct.time.bounds();
expect(bounds).toBeDefined();
expect(bounds.start).toBe(222);
expect(bounds.end).toBe(333);
});
});
it("bounds are correctly set in the API from the URL parameters where only the end bound changes", () => {
let resolveFunction;
expectLocationToBeInFixedMode();
return new Promise((resolve) => {
resolveFunction = resolve;
openmct.time.on('bounds', resolveFunction);
let hash = window.location.hash;
hash = hash.replace('tc.endBound=1', 'tc.endBound=333');
window.location.hash = hash;
}).then(() => {
let bounds = openmct.time.bounds();
expect(bounds).toBeDefined();
expect(bounds.start).toBe(0);
expect(bounds.end).toBe(333);
});
});
});
openmct.router.on('change:hash', resolveFunction);
});
function setRealtimeLocationParameters() {
let hash = window.location.hash.toString()
.replace('tc.mode=fixed', 'tc.mode=local')
.replace('tc.startBound=0', 'tc.startDelta=1000')
.replace('tc.endBound=1', 'tc.endDelta=100');
it("when the clock mode is set to local, it is reflected in the URL", (done) => {
let success;
window.location.hash = hash;
}
resolveFunction = () => {
let hash = window.location.hash;
hash = hash.replace('tc.mode=fixed', 'tc.mode=local');
window.location.hash = hash;
function setFixedLocationParameters() {
let hash = window.location.hash.toString()
.replace('tc.mode=local', 'tc.mode=fixed')
.replace('tc.timeSystem=utc', 'tc.timeSystem=local')
.replace('tc.startDelta=1000', 'tc.startBound=50')
.replace('tc.endDelta=100', 'tc.endBound=60');
success = window.location.hash.includes('tc.mode=local');
if (success) {
expect(success).toBe(true);
done();
}
};
window.location.hash = hash;
}
openmct.router.on('change:hash', resolveFunction);
});
function switchToRealtimeMode() {
let resolveFunction;
it("when the clock mode is set to local, it is reflected in the URL", (done) => {
let success;
return new Promise((resolve) => {
resolveFunction = resolve;
openmct.time.on('clock', resolveFunction);
setRealtimeLocationParameters();
}).then(() => {
openmct.time.off('clock', resolveFunction);
});
}
resolveFunction = () => {
let hash = window.location.hash;
function switchToFixedMode() {
let resolveFunction;
hash = hash.replace('tc.mode=fixed', 'tc.mode=local');
window.location.hash = hash;
success = window.location.hash.includes('tc.mode=local');
if (success) {
expect(success).toBe(true);
done();
}
};
return new Promise((resolve) => {
resolveFunction = resolve;
//The 'hashchange' event appears to be asynchronous, so we need to wait until a clock change has been
//detected in the API.
openmct.time.on('clock', resolveFunction);
setFixedLocationParameters();
}).then(() => {
openmct.time.off('clock', resolveFunction);
});
}
openmct.router.on('change:hash', resolveFunction);
});
function expectLocationToBeInRealtimeMode() {
expect(window.location.hash.includes('tc.mode=local')).toBe(true);
expect(window.location.hash.includes('tc.startDelta=1000')).toBe(true);
expect(window.location.hash.includes('tc.endDelta=100')).toBe(true);
expect(window.location.hash.includes('tc.mode=fixed')).toBe(false);
}
it("reset hash", (done) => {
let success;
function expectLocationToBeInFixedMode() {
expect(window.location.hash.includes('tc.mode=fixed')).toBe(true);
expect(window.location.hash.includes('tc.startBound=0')).toBe(true);
expect(window.location.hash.includes('tc.endBound=1')).toBe(true);
expect(window.location.hash.includes('tc.mode=local')).toBe(false);
}
window.location.hash = oldHash;
resolveFunction = () => {
success = window.location.hash === oldHash;
if (success) {
expect(success).toBe(true);
done();
}
};
openmct.router.on('change:hash', resolveFunction);
});
});

View File

@@ -43,7 +43,6 @@ import {TRIGGER_CONJUNCTION, TRIGGER_LABEL} from "./utils/constants";
* }
*/
export default class Condition extends EventEmitter {
/**
* Manages criteria and emits the result of - true or false - based on criteria evaluated.
* @constructor

View File

@@ -52,7 +52,6 @@
<div class="c-inspect-styles__content c-inspect-styles__condition-set">
<a v-if="conditionSetDomainObject"
class="c-object-label icon-conditional"
:href="navigateToPath"
@click="navigateOrPreview"
>
<span class="c-object-label__name">{{ conditionSetDomainObject.name }}</span>
@@ -286,6 +285,8 @@ export default {
if (this.openmct.editor.isEditing()) {
event.preventDefault();
this.previewAction.invoke(this.objectPath);
} else {
this.openmct.router.navigate(this.navigateToPath);
}
},
removeConditionSet() {

View File

@@ -66,7 +66,6 @@
<div class="c-inspect-styles__content c-inspect-styles__condition-set">
<a v-if="conditionSetDomainObject"
class="c-object-label"
:href="navigateToPath"
@click="navigateOrPreview"
>
<span class="c-object-label__type-icon icon-conditional"></span>
@@ -309,6 +308,8 @@ export default {
if (this.openmct.editor.isEditing()) {
event.preventDefault();
this.previewAction.invoke(this.objectPath);
} else {
this.openmct.router.navigate(this.navigateToPath);
}
},
isItemType(type, item) {

View File

@@ -46,7 +46,7 @@ xdescribe("the plugin", () => {
});
afterEach(() => {
resetApplicationState(openmct);
return resetApplicationState(openmct);
});
it('installs the new folder action', () => {

View File

@@ -235,7 +235,7 @@ define(['lodash'], function (_) {
message: `Warning! This action will remove this item from the Display Layout. Do you want to continue?`,
buttons: [
{
label: 'Ok',
label: 'OK',
emphasis: 'true',
callback: function () {
removeItem(getAllTypes(selection));

View File

@@ -112,7 +112,7 @@ describe("The Duplicate Action plugin", () => {
});
afterEach(() => {
resetApplicationState(openmct);
return resetApplicationState(openmct);
});
it("should be defined", () => {

View File

@@ -97,7 +97,7 @@ function ToolbarProvider(openmct) {
message: `This action will remove this frame from this Flexible Layout. Do you want to continue?`,
buttons: [
{
label: 'Ok',
label: 'OK',
emphasis: 'true',
callback: function () {
deleteFrameAction(primary.context.frameId);
@@ -162,7 +162,7 @@ function ToolbarProvider(openmct) {
message: 'This action will permanently delete this container from this Flexible Layout',
buttons: [
{
label: 'Ok',
label: 'OK',
emphasis: 'true',
callback: function () {
removeContainer(containerId);

View File

@@ -5,7 +5,7 @@
'is-alias': item.isAlias === true,
'c-grid-item--unknown': item.type.cssClass === undefined || item.type.cssClass.indexOf('unknown') !== -1
}, statusClass]"
:href="objectLink"
@click="navigate"
>
<div
class="c-grid-item__type-icon"
@@ -49,11 +49,17 @@ import statusListener from './status-listener';
export default {
mixins: [contextMenuGesture, objectLink, statusListener],
inject: ['openmct'],
props: {
item: {
type: Object,
required: true
}
},
methods: {
navigate() {
this.openmct.router.navigate(this.objectLink);
}
}
};
</script>

View File

@@ -11,7 +11,7 @@
ref="objectLink"
class="c-object-label"
:class="[statusClass]"
:href="objectLink"
@click="navigate"
>
<div
class="c-object-label__type-icon c-list-item__name__type-icon"
@@ -45,6 +45,7 @@ import statusListener from './status-listener';
export default {
mixins: [contextMenuGesture, objectLink, statusListener],
inject: ['openmct'],
props: {
item: {
type: Object,
@@ -56,7 +57,7 @@ export default {
return moment(timestamp).format(format);
},
navigate() {
this.$refs.objectLink.click();
this.openmct.router.navigate(this.objectLink);
}
}
};

View File

@@ -41,7 +41,7 @@ export default class GoToOriginalAction {
.slice(1)
.join('/');
window.location.href = url;
this._openmct.router.navigate(url);
});
}
appliesTo(objectPath) {

View File

@@ -47,7 +47,6 @@ describe("the plugin", () => {
});
describe('when invoked', () => {
beforeEach(() => {
mockObjectPath = [{
name: 'mock folder',
@@ -63,11 +62,15 @@ describe("the plugin", () => {
key: 'test'
}
}));
goToFolderAction.invoke(mockObjectPath);
});
it('goes to the original location', () => {
expect(window.location.href).toContain('context.html#/browse/?tc.mode=fixed&tc.startBound=0&tc.endBound=1&tc.timeSystem=utc');
it('goes to the original location', (done) => {
setTimeout(() => {
expect(window.location.href).toContain('context.html#/browse/?tc.mode=fixed&tc.startBound=0&tc.endBound=1&tc.timeSystem=utc');
done();
}, 2500);
});
});
});

View File

@@ -390,7 +390,9 @@ export default {
delete this.unsubscribe;
}
this.imageContainerResizeObserver.disconnect();
if (this.imageContainerResizeObserver) {
this.imageContainerResizeObserver.disconnect();
}
if (this.relatedTelemetry.hasRelatedTelemetry) {
this.relatedTelemetry.destroy();
@@ -702,7 +704,7 @@ export default {
window.clearInterval(this.durationTracker);
},
updateDuration() {
let currentTime = this.openmct.time.clock().currentValue();
let currentTime = this.openmct.time.clock() && this.openmct.time.clock().currentValue();
this.numericDuration = currentTime - this.parsedSelectedTime;
},
resetAgeCSS() {

View File

@@ -19,7 +19,7 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import ImageryPlugin from './plugin.js';
import Vue from 'vue';
import {
createOpenMct,
@@ -89,15 +89,11 @@ describe("The Imagery View Layout", () => {
const START = Date.now();
const COUNT = 10;
let resolveFunction;
let openmct;
let imageryPlugin;
let parent;
let child;
let timeFormat = 'utc';
let bounds = {
start: START - TEN_MINUTES,
end: START
};
let imageTelemetry = generateTelemetry(START - TEN_MINUTES, COUNT);
let imageryObject = {
identifier: {
@@ -205,6 +201,10 @@ describe("The Imagery View Layout", () => {
openmct = createOpenMct();
openmct.install(openmct.plugins.MyItems());
openmct.install(openmct.plugins.LocalTimeSystem());
openmct.install(openmct.plugins.UTCTimeSystem());
parent = document.createElement('div');
child = document.createElement('div');
parent.appendChild(child);
@@ -215,22 +215,18 @@ describe("The Imagery View Layout", () => {
});
spyOn(openmct.telemetry, 'request').and.returnValue(Promise.resolve([]));
imageryPlugin = new ImageryPlugin();
openmct.install(imageryPlugin);
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({}));
openmct.time.timeSystem(timeFormat, {
start: 0,
end: 4
});
openmct.on('start', done);
openmct.startHeadless(appHolder);
openmct.start(appHolder);
});
afterEach(() => {
openmct.time.timeSystem('utc', {
start: 0,
end: 1
});
return resetApplicationState(openmct);
});
@@ -248,7 +244,7 @@ describe("The Imagery View Layout", () => {
let imageryViewProvider;
let imageryView;
beforeEach(async (done) => {
beforeEach(async () => {
let telemetryRequestResolve;
let telemetryRequestPromise = new Promise((resolve) => {
telemetryRequestResolve = resolve;
@@ -260,23 +256,18 @@ describe("The Imagery View Layout", () => {
return telemetryRequestPromise;
});
openmct.time.clock('local', {
start: bounds.start,
end: bounds.end + 100
});
applicableViews = openmct.objectViews.get(imageryObject, []);
imageryViewProvider = applicableViews.find(viewProvider => viewProvider.key === imageryKey);
imageryView = imageryViewProvider.view(imageryObject);
imageryView.show(child);
await telemetryRequestPromise;
await Vue.nextTick();
return done();
});
afterEach(() => {
openmct.time.stopClock();
openmct.router.removeListener('change:hash', resolveFunction);
imageryView.destroy();
});
@@ -286,43 +277,44 @@ describe("The Imagery View Layout", () => {
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 1].timeId)).not.toEqual(-1);
});
it("should show the clicked thumbnail as the main image", async () => {
it("should show the clicked thumbnail as the main image", (done) => {
const target = imageTelemetry[5].url;
parent.querySelectorAll(`img[src='${target}']`)[0].click();
await Vue.nextTick();
const imageInfo = getImageInfo(parent);
Vue.nextTick(() => {
const imageInfo = getImageInfo(parent);
expect(imageInfo.url.indexOf(imageTelemetry[5].timeId)).not.toEqual(-1);
});
it("should show that an image is new", async (done) => {
await Vue.nextTick();
// used in code, need to wait to the 500ms here too
setTimeout(() => {
const imageIsNew = isNew(parent);
expect(imageIsNew).toBeTrue();
expect(imageInfo.url.indexOf(imageTelemetry[5].timeId)).not.toEqual(-1);
done();
}, REFRESH_CSS_MS);
});
});
it("should show that an image is not new", async (done) => {
xit("should show that an image is new", (done) => {
Vue.nextTick(() => {
// used in code, need to wait to the 500ms here too
setTimeout(() => {
const imageIsNew = isNew(parent);
expect(imageIsNew).toBeTrue();
done();
}, REFRESH_CSS_MS);
});
});
xit("should show that an image is not new", (done) => {
const target = imageTelemetry[2].url;
parent.querySelectorAll(`img[src='${target}']`)[0].click();
await Vue.nextTick();
Vue.nextTick(() => {
// used in code, need to wait to the 500ms here too
setTimeout(() => {
const imageIsNew = isNew(parent);
// used in code, need to wait to the 500ms here too
setTimeout(() => {
const imageIsNew = isNew(parent);
expect(imageIsNew).toBeFalse();
done();
}, REFRESH_CSS_MS);
expect(imageIsNew).toBeFalse();
done();
}, REFRESH_CSS_MS);
});
});
it("should navigate via arrow keys", async () => {
it("should navigate via arrow keys", (done) => {
let keyOpts = {
element: parent.querySelector('.c-imagery'),
key: 'ArrowLeft',
@@ -332,14 +324,15 @@ describe("The Imagery View Layout", () => {
simulateKeyEvent(keyOpts);
await Vue.nextTick();
Vue.nextTick(() => {
const imageInfo = getImageInfo(parent);
const imageInfo = getImageInfo(parent);
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 2].timeId)).not.toEqual(-1);
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 2].timeId)).not.toEqual(-1);
done();
});
});
it("should navigate via numerous arrow keys", async () => {
it("should navigate via numerous arrow keys", (done) => {
let element = parent.querySelector('.c-imagery');
let type = 'keyup';
let leftKeyOpts = {
@@ -362,12 +355,12 @@ describe("The Imagery View Layout", () => {
// right once
simulateKeyEvent(rightKeyOpts);
await Vue.nextTick();
Vue.nextTick(() => {
const imageInfo = getImageInfo(parent);
const imageInfo = getImageInfo(parent);
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 3].timeId)).not.toEqual(-1);
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 3].timeId)).not.toEqual(-1);
done();
});
});
});
});

View File

@@ -55,7 +55,7 @@ describe("The local time", () => {
beforeEach(() => {
localTimeSystem = openmct.time.timeSystem(LOCAL_SYSTEM_KEY, {
start: 0,
end: 4
end: 1
});
});

View File

@@ -81,7 +81,7 @@ describe("The Move Action plugin", () => {
});
afterEach(() => {
resetApplicationState(openmct);
return resetApplicationState(openmct);
});
it("should be defined", () => {

View File

@@ -135,6 +135,7 @@ import SearchResults from './SearchResults.vue';
import Sidebar from './Sidebar.vue';
import { clearDefaultNotebook, getDefaultNotebook, setDefaultNotebook, setDefaultNotebookSection, setDefaultNotebookPage } from '../utils/notebook-storage';
import { addNotebookEntry, createNewEmbed, getEntryPosById, getNotebookEntries, mutateObject } from '../utils/notebook-entries';
import { NOTEBOOK_VIEW_TYPE } from '../notebook-constants';
import objectUtils from 'objectUtils';
import { debounce } from 'lodash';
@@ -189,14 +190,14 @@ export default {
selectedPage() {
const pages = this.getPages();
if (!pages) {
return null;
return {};
}
return pages.find(page => page.isSelected);
},
selectedSection() {
if (!this.sections.length) {
return null;
return {};
}
return this.sections.find(section => section.isSelected);
@@ -216,6 +217,7 @@ export default {
window.addEventListener('orientationchange', this.formatSidebar);
window.addEventListener("hashchange", this.navigateToSectionPage, false);
this.openmct.router.on('change:params', this.changeSectionPage);
this.navigateToSectionPage();
},
@@ -226,6 +228,7 @@ export default {
window.removeEventListener('orientationchange', this.formatSidebar);
window.removeEventListener("hashchange", this.navigateToSectionPage);
this.openmct.router.off('change:params', this.changeSectionPage);
},
updated: function () {
this.$nextTick(() => {
@@ -233,6 +236,28 @@ export default {
});
},
methods: {
changeSectionPage(newParams, oldParams, changedParams) {
if (newParams.view !== NOTEBOOK_VIEW_TYPE) {
return;
}
let pageId = newParams.pageId;
let sectionId = newParams.sectionId;
if (!pageId && !sectionId) {
return;
}
this.sections.forEach(section => {
section.isSelected = Boolean(section.id === sectionId);
if (section.isSelected) {
section.pages.forEach(page => {
page.isSelected = Boolean(page.id === pageId);
});
}
});
},
changeSelectedSection({ sectionId, pageId }) {
const sections = this.sections.map(s => {
s.isSelected = false;
@@ -518,9 +543,11 @@ export default {
return this.sections.find(section => section.isSelected);
},
navigateToSectionPage() {
const { pageId, sectionId } = this.openmct.router.getParams();
let { pageId, sectionId } = this.openmct.router.getParams();
if (!pageId || !sectionId) {
return;
sectionId = this.selectedSection.id;
pageId = this.selectedPage.id;
}
const sections = this.sections.map(s => {

View File

@@ -145,7 +145,7 @@ export default {
const relativeHash = hash.slice(hash.indexOf('#'));
const url = new URL(relativeHash, `${location.protocol}//${location.host}${location.pathname}`);
window.location.hash = url.hash;
this.openmct.router.navigate(url.hash);
},
formatTime(unixTime, timeFormat) {
return Moment.utc(unixTime).format(timeFormat);

View File

@@ -111,10 +111,6 @@ export default {
}
}
},
data() {
return {
};
},
computed: {
pages() {
const selectedSection = this.sections.find(section => section.isSelected);

View File

@@ -1,3 +1,4 @@
export const EVENT_SNAPSHOTS_UPDATED = 'SNAPSHOTS_UPDATED';
export const NOTEBOOK_DEFAULT = 'DEFAULT';
export const NOTEBOOK_SNAPSHOT = 'SNAPSHOT';
export const NOTEBOOK_VIEW_TYPE = 'notebook-vue';

View File

@@ -65,7 +65,8 @@ describe("Notebook plugin:", () => {
afterAll(() => {
appHolder.remove();
resetApplicationState(openmct);
return resetApplicationState(openmct);
});
it("has type as Notebook", () => {

View File

@@ -140,7 +140,8 @@ describe('Notebook Entries:', () => {
afterEach(() => {
notebookDomainObject.configuration.entries[selectedSection.id][selectedPage.id] = [];
resetApplicationState(openmct);
return resetApplicationState(openmct);
});
it('getNotebookEntries has no entries', () => {

View File

@@ -83,7 +83,7 @@ describe('Notebook Storage:', () => {
});
afterEach(() => {
resetApplicationState(openmct);
return resetApplicationState(openmct);
});
it('has empty local Storage', () => {

View File

@@ -117,7 +117,7 @@ describe('the plugin', () => {
});
});
it('updates an object', () => {
it('updates an object', (done) => {
return openmct.objects.save(mockDomainObject).then((result) => {
expect(result).toBeTrue();
expect(provider.create).toHaveBeenCalled();
@@ -128,6 +128,7 @@ describe('the plugin', () => {
return openmct.objects.save(mockDomainObject).then((updatedResult) => {
expect(updatedResult).toBeTrue();
expect(provider.update).toHaveBeenCalled();
done();
});
});
});

View File

@@ -159,6 +159,7 @@ import MctTicks from "./MctTicks.vue";
import MctChart from "./chart/MctChart.vue";
import XAxis from "./axis/XAxis.vue";
import YAxis from "./axis/YAxis.vue";
import _ from "lodash";
export default {
components: {
@@ -496,6 +497,10 @@ export default {
},
initialize() {
_.debounce(this.handleWindowResize, 400);
this.plotContainerResizeObserver = new ResizeObserver(this.handleWindowResize);
this.plotContainerResizeObserver.observe(this.$parent.$refs.plotWrapper);
// Setup canvas etc.
this.xScale = new LinearScale(this.config.xAxis.get('displayRange'));
this.yScale = new LinearScale(this.config.yAxis.get('displayRange'));
@@ -999,12 +1004,20 @@ export default {
this.removeStatusListener();
}
this.plotContainerResizeObserver.disconnect();
this.openmct.time.off('clock', this.updateRealTime);
this.openmct.time.off('bounds', this.updateDisplayBounds);
this.openmct.objectViews.off('clearData', this.clearData);
},
updateStatus(status) {
this.$emit('statusUpdated', status);
},
handleWindowResize() {
if (this.offsetWidth !== this.$parent.$refs.plotWrapper.offsetWidth) {
this.offsetWidth = this.$parent.$refs.plotWrapper.offsetWidth;
this.config.series.models.forEach(this.loadSeriesData, this);
}
}
}
};

View File

@@ -228,15 +228,16 @@ export default {
doTickUpdate() {
if (this.shouldCheckWidth) {
const tickElements = this.$refs.tickContainer.querySelectorAll('.gl-plot-tick > span');
const tickElements = this.$refs.tickContainer && this.$refs.tickContainer.querySelectorAll('.gl-plot-tick > span');
if (tickElements) {
const tickWidth = Number([].reduce.call(tickElements, function (memo, first) {
return Math.max(memo, first.offsetWidth);
}, 0));
const tickWidth = Number([].reduce.call(tickElements, function (memo, first) {
return Math.max(memo, first.offsetWidth);
}, 0));
this.tickWidth = tickWidth;
this.$emit('plotTickWidth', tickWidth);
this.shouldCheckWidth = false;
this.tickWidth = tickWidth;
this.$emit('plotTickWidth', tickWidth);
this.shouldCheckWidth = false;
}
}
this.tickUpdate = false;

View File

@@ -99,6 +99,11 @@ describe("the plugin", function () {
element.appendChild(child);
document.body.appendChild(element);
spyOn(window, 'ResizeObserver').and.returnValue({
observe() {},
disconnect() {}
});
openmct.time.timeSystem("utc", {
start: 0,
end: 4
@@ -118,6 +123,11 @@ describe("the plugin", function () {
});
afterEach((done) => {
openmct.time.timeSystem('utc', {
start: 0,
end: 1
});
// Needs to be in a timeout because plots use a bunch of setTimeouts, some of which can resolve during or after
// teardown, which causes problems
// This is hacky, we should find a better approach here.
@@ -129,7 +139,7 @@ describe("the plugin", function () {
configStore.deleteAll();
resetApplicationState(openmct).then(done);
resetApplicationState(openmct).then(done).catch(done);
});
});

View File

@@ -78,7 +78,7 @@ export default class RemoveAction {
.map(object => this.openmct.objects.makeKeyString(object.identifier))
.join("/");
window.location.href = '#/browse/' + urlPath;
this.openmct.router.navigate('#/browse/' + urlPath);
}
removeFromComposition(parent, child) {

View File

@@ -72,7 +72,7 @@ describe("The Remove Action plugin", () => {
});
afterEach(() => {
resetApplicationState(openmct);
return resetApplicationState(openmct);
});
it("should be defined", () => {

View File

@@ -65,11 +65,6 @@
<script>
import ObjectView from '../../../ui/components/ObjectView.vue';
import RemoveAction from '../../remove/RemoveAction.js';
import {
getSearchParam,
setSearchParam,
deleteSearchParam
} from 'utils/openmctLocation';
const unknownObjectType = {
definition: {
@@ -115,7 +110,7 @@ export default {
this.composition.on('remove', this.removeItem);
this.composition.on('reorder', this.onReorder);
this.composition.load().then(() => {
let currentTabIndexFromURL = getSearchParam(this.searchTabKey);
let currentTabIndexFromURL = this.openmct.router.getSearchParam(this.searchTabKey);
let currentTabIndexFromDomainObject = this.internalDomainObject.currentTabIndex;
if (currentTabIndexFromURL !== null) {
@@ -129,6 +124,8 @@ export default {
this.unsubscribe = this.openmct.objects.observe(this.internalDomainObject, '*', this.updateInternalDomainObject);
this.openmct.router.on('change:params', this.updateCurrentTab.bind(this));
this.RemoveAction = new RemoveAction(this.openmct);
document.addEventListener('dragstart', this.dragstart);
document.addEventListener('dragend', this.dragend);
@@ -148,6 +145,8 @@ export default {
this.unsubscribe();
this.clearCurrentTabIndexFromURL();
this.openmct.router.off('change:params', this.updateCurrentTab.bind(this));
document.removeEventListener('dragstart', this.dragstart);
document.removeEventListener('dragend', this.dragend);
},
@@ -181,7 +180,7 @@ export default {
message: `This action will remove this tab from the Tabs Layout. Do you want to continue?`,
buttons: [
{
label: 'Ok',
label: 'OK',
emphasis: 'true',
callback: () => {
this.composition.remove(childDomainObject);
@@ -285,15 +284,15 @@ export default {
this.openmct.objects.mutate(this.internalDomainObject, 'currentTabIndex', index);
},
storeCurrentTabIndexInURL(index) {
let currentTabIndexInURL = getSearchParam(this.searchTabKey);
let currentTabIndexInURL = this.openmct.router.getSearchParam(this.searchTabKey);
if (index !== currentTabIndexInURL) {
setSearchParam(this.searchTabKey, index);
this.openmct.router.setSearchParam(this.searchTabKey, index);
this.currentTabIndex = index;
}
},
clearCurrentTabIndexFromURL() {
deleteSearchParam(this.searchTabKey);
this.openmct.router.deleteSearchParam(this.searchTabKey);
},
updateStatus(keyString, status) {
let tabPos = this.tabsList.findIndex((tab) => {
@@ -309,6 +308,19 @@ export default {
} else {
return this.loadedTabs[tab.keyString];
}
},
updateCurrentTab(newParams, oldParams, changedParams) {
const tabIndex = changedParams[this.searchTabKey];
if (!tabIndex) {
return;
}
if (this.currentTabIndex === parseInt(tabIndex, 10)) {
return;
}
this.currentTabIndex = tabIndex;
this.currentTab = this.tabsList[tabIndex];
}
}
};

View File

@@ -82,6 +82,11 @@ describe("the plugin", () => {
});
afterEach(() => {
openmct.time.timeSystem('utc', {
start: 0,
end: 1
});
return resetApplicationState(openmct);
});

View File

@@ -59,7 +59,7 @@ describe("The UTC Time System", () => {
it("can be set to be the main time system", () => {
openmct.time.timeSystem(UTC_SYSTEM_AND_FORMAT_KEY, {
start: 0,
end: 4
end: 1
});
expect(openmct.time.timeSystem().key).toBe(UTC_SYSTEM_AND_FORMAT_KEY);

View File

@@ -75,7 +75,7 @@ export default {
event.preventDefault();
this.preview();
} else {
window.location.assign(this.objectLink);
this.openmct.router.navigate(this.objectLink);
}
},
preview() {

View File

@@ -180,16 +180,16 @@ export default {
},
hasParent() {
return this.domainObject !== PLACEHOLDER_OBJECT
&& this.parentUrl !== '#/browse';
&& this.parentUrl !== '/browse';
},
parentUrl() {
let objectKeyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
let hash = window.location.hash;
const objectKeyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
const hash = this.openmct.router.getCurrentLocation().path;
return hash.slice(0, hash.lastIndexOf('/' + objectKeyString));
},
type() {
let objectType = this.openmct.types.get(this.domainObject.type);
const objectType = this.openmct.types.get(this.domainObject.type);
if (!objectType) {
return {};
}
@@ -288,7 +288,7 @@ export default {
message: 'Any unsaved changes will be lost. Are you sure you want to continue?',
buttons: [
{
label: 'Ok',
label: 'OK',
emphasis: true,
callback: () => {
this.openmct.editor.cancel().then(() => {
@@ -336,7 +336,7 @@ export default {
});
},
goToParent() {
window.location.hash = this.parentUrl;
this.openmct.router.navigate(this.parentUrl);
},
updateActionItems(actionItems) {
this.statusBarItems = this.actionCollection.getStatusBarActions();

View File

@@ -23,23 +23,7 @@
const LocationBar = require('location-bar');
const EventEmitter = require('EventEmitter');
function paramsToObject(searchParams) {
let params = {};
for (let [key, value] of searchParams.entries()) {
if (params[key]) {
if (!Array.isArray(params[key])) {
params[key] = [params[key]];
}
params[key].push(value);
} else {
params[key] = value;
}
}
return params;
}
const _ = require('lodash');
class ApplicationRouter extends EventEmitter {
/**
@@ -57,11 +41,158 @@ class ApplicationRouter extends EventEmitter {
* route(path, handler);
* start(); Start routing.
*/
constructor() {
constructor(openmct) {
super();
this.locationBar = new LocationBar();
this.openmct = openmct;
this.routes = [];
this.started = false;
this.locationBar = new LocationBar();
this.setHash = _.debounce(this.setHash.bind(this), 300);
}
// Public Methods
destroy() {
this.locationBar.stop();
}
/**
* Delete a given query parameter from current url
*
* @param {string} paramName name of searchParam to delete from current url searchParams
*/
deleteSearchParam(paramName) {
let url = this.getHashRelativeURL();
url.searchParams.delete(paramName);
this.setLocationFromUrl();
}
/**
* object for accessing all current search parameters
*
* @returns {URLSearchParams} A {@link https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/entries|URLSearchParams}
*/
getAllSearchParams() {
return this.getHashRelativeURL().searchParams;
}
/**
* Uniquely identifies a domain object.
*
* @typedef CurrentLocation
* @property {URL} url current url location
* @property {string} path current url location pathname
* @property {string} getQueryString a function which returns url search query
* @property {object} params object representing url searchParams
*/
/**
* object for accessing current url location and search params
*
* @returns {CurrentLocation} A {@link CurrentLocation}
*/
getCurrentLocation() {
return this.currentLocation;
}
/**
* Get current location URL Object
*
* @returns {URL} current url location
*/
getHashRelativeURL() {
return this.getCurrentLocation().url;
}
/**
* Get current location URL Object searchParams
*
* @returns {object} object representing current url searchParams
*/
getParams() {
return this.currentLocation.params;
}
/**
* Get a value of given param from current url searchParams
*
* @returns {string} value of paramName from current url searchParams
*/
getSearchParam(paramName) {
return this.getAllSearchParams().get(paramName);
}
/**
* Navgate to given hash and update current location object and notify listeners about location change
*
* @param {string} paramName name of searchParam to get from current url searchParams
*
* @returns {string} value of paramName from current url searchParams
*/
navigate(hash) {
this.handleLocationChange(hash.substring(1));
}
/**
* Add routes listeners
*
* @param {string} matcher Regex to match value in url
* @param {@function} callback function called when found match in url
*/
route(matcher, callback) {
this.routes.push({
matcher,
callback
});
}
/**
* Set url hash using path and queryString
*
* @param {string} path path for url
* @param {string} queryString queryString for url
*/
set(path, queryString) {
this.setHash(`${path}?${queryString}`);
}
/**
* Will replace all current search parameters with the ones defined in urlSearchParams
*/
setAllSearchParams() {
this.setLocationFromUrl();
}
/**
* To force update url based on value in currentLocation object
*/
setLocationFromUrl() {
this.updateTimeSettings();
}
/**
* Set url hash using path
*
* @param {string} path path for url
*/
setPath(path) {
this.handleLocationChange(path.substring(1));
}
/**
* Update param value from current url searchParams
*
* @param {string} paramName param name from current url searchParams
* @param {string} paramValue param value from current url searchParams
*/
setSearchParam(paramName, paramValue) {
let url = this.getHashRelativeURL();
url.searchParams.set(paramName, paramValue);
this.setLocationFromUrl();
}
/**
@@ -74,105 +205,18 @@ class ApplicationRouter extends EventEmitter {
this.started = true;
this.locationBar.onChange(p => this.handleLocationChange(p));
this.locationBar.onChange(p => this.hashChaged(p));
this.locationBar.start({
root: location.pathname
});
}
destroy() {
this.locationBar.stop();
this.removeAllListeners();
}
handleLocationChange(pathString) {
if (pathString[0] !== '/') {
pathString = '/' + pathString;
}
let url = new URL(
pathString,
`${location.protocol}//${location.host}${location.pathname}`
);
let oldLocation = this.currentLocation;
let newLocation = {
url: url,
path: url.pathname,
queryString: url.search.replace(/^\?/, ''),
params: paramsToObject(url.searchParams)
};
this.currentLocation = newLocation;
if (!oldLocation) {
this.doPathChange(newLocation.path, null, newLocation);
this.doParamsChange(newLocation.params, {}, newLocation);
return;
}
if (oldLocation.path !== newLocation.path) {
this.doPathChange(
newLocation.path,
oldLocation.path,
this
);
}
if (!_.isEqual(oldLocation.params, newLocation.params)) {
this.doParamsChange(
newLocation.params,
oldLocation.params,
newLocation
);
}
}
doPathChange(newPath, oldPath, newLocation) {
let route = this.routes.filter(r => r.matcher.test(newPath))[0];
if (route) {
route.callback(newPath, route.matcher.exec(newPath), this.currentLocation.params);
}
this.emit('change:path', newPath, oldPath);
}
doParamsChange(newParams, oldParams, newLocation) {
let changedParams = {};
Object.entries(newParams).forEach(([key, value]) => {
if (value !== oldParams[key]) {
changedParams[key] = value;
}
});
Object.keys(oldParams).forEach(key => {
if (!Object.prototype.hasOwnProperty.call(newParams, key)) {
changedParams[key] = undefined;
}
});
this.emit('change:params', newParams, oldParams, changedParams);
}
/**
* Update route params. Takes an object of updates. New parameters
* Set url hash using path and searchParams object
*
* @param {string} path path for url
* @param {string} params oject representing searchParams key/value
*/
updateParams(updateParams) {
let searchParams = this.currentLocation.url.searchParams;
Object.entries(updateParams).forEach(([key, value]) => {
if (typeof value === 'undefined') {
searchParams.delete(key);
} else {
searchParams.set(key, value);
}
});
this.setQueryString(searchParams.toString());
}
getParams() {
return this.currentLocation.params;
}
update(path, params) {
let searchParams = this.currentLocation.url.searchParams;
for (let [key, value] of Object.entries(params)) {
@@ -186,24 +230,190 @@ class ApplicationRouter extends EventEmitter {
this.set(path, searchParams.toString());
}
set(path, queryString) {
location.hash = `${path}?${queryString}`;
}
setQueryString(queryString) {
this.set(this.currentLocation.path, queryString);
}
setPath(path) {
this.set(path, this.currentLocation.queryString);
}
route(matcher, callback) {
this.routes.push({
matcher,
callback
/**
* Update route params. Takes an object of updates. New parameters
*/
updateParams(updateParams) {
let searchParams = this.currentLocation.url.searchParams;
Object.entries(updateParams).forEach(([key, value]) => {
if (typeof value === 'undefined') {
searchParams.delete(key);
} else {
searchParams.set(key, value);
}
});
this.setQueryString(searchParams.toString());
}
/**
* To force update url based on value in currentLocation object
*/
updateTimeSettings() {
const hash = `${this.currentLocation.path}?${this.currentLocation.getQueryString()}`;
this.setHash(hash);
}
// Private Methods
/**
* @private
* Create currentLocation object
*
* @param {string} pathString USVString representing relative URL.
*
* @returns {CurrentLocation} A {@link CurrentLocation}
*/
createLocation(pathString) {
if (pathString[0] !== '/') {
pathString = '/' + pathString;
}
let url = new URL(
pathString,
`${location.protocol}//${location.host}${location.pathname}`
);
return {
url: url,
path: url.pathname,
getQueryString: () => url.search.replace(/^\?/, ''),
params: paramsToObject(url.searchParams)
};
}
/**
* @private
* Compare new and old path and on change emit event 'change:path'
*
* @param {string} newPath new path of url
* @param {string} oldPath old path of url
*/
doPathChange(newPath, oldPath) {
if (newPath === oldPath) {
return;
}
let route = this.routes.filter(r => r.matcher.test(newPath))[0];
if (route) {
route.callback(newPath, route.matcher.exec(newPath), this.currentLocation.params);
}
this.emit('change:path', newPath, oldPath);
}
/**
* @private
* Compare new and old params and on change emit event 'change:params'
*
* @param {object} newParams new params of url
* @param {object} oldParams old params of url
*/
doParamsChange(newParams, oldParams) {
if (_.isEqual(newParams, oldParams)) {
return;
}
let changedParams = {};
Object.entries(newParams).forEach(([key, value]) => {
if (value !== oldParams[key]) {
changedParams[key] = value;
}
});
Object.keys(oldParams).forEach(key => {
if (!Object.prototype.hasOwnProperty.call(newParams, key)) {
changedParams[key] = undefined;
}
});
this.emit('change:params', newParams, oldParams, changedParams);
}
/**
* @private
* On location change, update currentLocation object and emit appropriate events
*
* @param {string} pathString USVString representing relative URL.
*/
handleLocationChange(pathString) {
let oldLocation = this.currentLocation;
let newLocation = this.createLocation(pathString);
this.currentLocation = newLocation;
if (!oldLocation) {
this.doPathChange(newLocation.path, null);
this.doParamsChange(newLocation.params, {});
return;
}
this.doPathChange(
newLocation.path,
oldLocation.path
);
this.doParamsChange(
newLocation.params,
oldLocation.params
);
}
/**
* @private
* On hash changed, update currentLocation object and emit appropriate events
*
* @param {string} hash new hash for url
*/
hashChaged(hash) {
this.emit('change:hash', hash);
this.handleLocationChange(hash);
}
/**
* @private
* Set new hash for url
*
* @param {string} hash new hash for url
*/
setHash(hash) {
location.hash = '#' + hash.replace(/#/g, '');
}
/**
* @private
* Set queryString part of current url
*
* @param {string} queryString queryString part of url
*/
setQueryString(queryString) {
this.handleLocationChange(`${this.currentLocation.path}?${queryString}`);
}
}
/**
* Convert searchParams into Object
*
* @param {URLSearchParams} searchParams queryString part of url
*
* @returns {Object}
*/
function paramsToObject(searchParams) {
let params = {};
for (let [key, value] of searchParams.entries()) {
if (params[key]) {
if (!Array.isArray(params[key])) {
params[key] = [params[key]];
}
params[key].push(value);
} else {
params[key] = value;
}
}
return params;
}
module.exports = ApplicationRouter;

View File

@@ -0,0 +1,139 @@
import { createOpenMct, resetApplicationState } from 'utils/testing';
let openmct;
let element;
let child;
let appHolder;
let resolveFunction;
let initialHash = '';
describe('Application router utility functions', () => {
beforeAll(done => {
appHolder = document.createElement('div');
appHolder.style.width = '640px';
appHolder.style.height = '480px';
openmct = createOpenMct();
openmct.install(openmct.plugins.MyItems());
openmct.install(openmct.plugins.LocalTimeSystem());
openmct.install(openmct.plugins.UTCTimeSystem());
element = document.createElement('div');
child = document.createElement('div');
element.appendChild(child);
openmct.on('start', done);
openmct.start(appHolder);
document.body.append(appHolder);
});
afterAll(() => {
openmct.router.setHash(initialHash);
appHolder.remove();
return resetApplicationState(openmct);
});
it('has initial hash when loaded', (done) => {
let success;
resolveFunction = () => {
openmct.router.setLocationFromUrl();
success = window.location.hash !== null;
if (success) {
initialHash = window.location.hash;
expect(success).toBe(true);
openmct.router.removeListener('change:hash', resolveFunction);
done();
}
};
openmct.router.on('change:hash', resolveFunction);
});
it('The setSearchParam function sets an individual search parameter in the window location hash', (done) => {
let success;
openmct.router.setSearchParam('testParam', 'testValue');
resolveFunction = () => {
success = window.location.hash.includes('testParam=testValue');
if (success) {
expect(success).toBe(true);
openmct.router.removeListener('change:hash', resolveFunction);
done();
}
};
openmct.router.on('change:hash', resolveFunction);
});
it('The getSearchParam function returns the value of an individual search paramater in the window location hash', () => {
expect(openmct.router.getSearchParam('testParam')).toBe('testValue');
});
it('The deleteSearchParam function deletes an individual search paramater in the window location hash', (done) => {
let success;
openmct.router.deleteSearchParam('testParam');
resolveFunction = () => {
success = window.location.hash.includes('testParam=testValue') === false;
if (success) {
expect(success).toBe(true);
openmct.router.removeListener('change:hash', resolveFunction);
done();
}
};
openmct.router.on('change:hash', resolveFunction);
});
it('The setSearchParam function sets an individual search parameters in the window location hash', (done) => {
let success;
openmct.router.setSearchParam('testParam1', 'testValue1');
openmct.router.setSearchParam('testParam2', 'testValue2');
resolveFunction = () => {
const hasTestParam1 = window.location.hash.includes('testParam1=testValue1');
const hasTestParam2 = window.location.hash.includes('testParam2=testValue2');
success = hasTestParam1 && hasTestParam2;
if (success) {
expect(success).toBe(true);
openmct.router.removeListener('change:hash', resolveFunction);
done();
}
};
openmct.router.on('change:hash', resolveFunction);
});
it('The setAllSearchParams function replaces all search paramaters in the window location hash', (done) => {
let success;
openmct.router.setSearchParam('testParam2', 'updatedtestValue2');
openmct.router.setSearchParam('newTestParam3', 'newTestValue3');
resolveFunction = () => {
const hasupdatedValueForTestParam2 = window.location.hash.includes('testParam2=updatedtestValue2');
const hasNewTestParam3 = window.location.hash.includes('newTestParam3=newTestValue3');
success = hasupdatedValueForTestParam2 && hasNewTestParam3;
if (success) {
expect(success).toBe(true);
openmct.router.removeListener('change:hash', resolveFunction);
done();
}
};
openmct.router.on('change:hash', resolveFunction);
});
it('The getAllSearchParams function returns the values of all search paramaters in the window location hash', () => {
let searchParams = openmct.router.getAllSearchParams();
expect(searchParams.get('testParam1')).toBe('testValue1');
expect(searchParams.get('testParam2')).toBe('updatedtestValue2');
expect(searchParams.get('newTestParam3')).toBe('newTestValue3');
});
});

View File

@@ -13,13 +13,12 @@ define([
let mutable;
openmct.router.route(/^\/browse\/?$/, navigateToFirstChildOfRoot);
openmct.router.route(/^\/browse\/(.*)$/, (path, results, params) => {
isRoutingInProgress = true;
let navigatePath = results[1];
clearMutationListeners();
navigateToPath(navigatePath, params.view);
onParamsChanged(null, null, params);
});
openmct.router.on('change:params', onParamsChanged);
@@ -133,18 +132,21 @@ define([
}
function navigateToFirstChildOfRoot() {
openmct.objects.get('ROOT').then(rootObject => {
openmct.composition.get(rootObject).load()
.then(children => {
let lastChild = children[children.length - 1];
if (!lastChild) {
console.error('Unable to navigate to anything. No root objects found.');
} else {
let lastChildId = openmct.objects.makeKeyString(lastChild.identifier);
openmct.router.setPath(`#/browse/${lastChildId}`);
}
});
});
openmct.objects.get('ROOT')
.then(rootObject => {
openmct.composition.get(rootObject).load()
.then(children => {
let lastChild = children[children.length - 1];
if (!lastChild) {
console.error('Unable to navigate to anything. No root objects found.');
} else {
let lastChildId = openmct.objects.makeKeyString(lastChild.identifier);
openmct.router.setPath(`#/browse/${lastChildId}`);
}
})
.catch(e => console.error(e));
})
.catch(e => console.error(e));
}
function clearMutationListeners() {

View File

@@ -1,108 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import objectUtils from '../api/objects/object-utils.js';
/**
* Utility functions for getting and setting Open MCT search parameters and navigated object path.
* Open MCT encodes application state into the "hash" of the url, making it awkward to use standard browser API such
* as URL for modifying state in the URL. This wraps native API with some utility functions that operate only on the
* hash section of the URL.
*/
export function setSearchParam(paramName, paramValue) {
let url = getHashRelativeURL();
url.searchParams.set(paramName, paramValue);
setLocationFromUrl(url);
}
export function deleteSearchParam(paramName) {
let url = getHashRelativeURL();
url.searchParams.delete(paramName);
setLocationFromUrl(url);
}
/**
* Will replace all current search parameters with the ones defined in urlSearchParams
* @param {URLSearchParams} paramMap
*/
export function setAllSearchParams(newSearchParams) {
let url = getHashRelativeURL();
Array.from(url.searchParams.keys()).forEach((key) => url.searchParams.delete(key));
Array.from(newSearchParams.keys()).forEach(key => {
url.searchParams.set(key, newSearchParams.get(key));
});
setLocationFromUrl(url);
}
export function getSearchParam(paramName) {
return getAllSearchParams().get(paramName);
}
/**
* @returns {URLSearchParams} A {@link https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/entries|URLSearchParams}
* object for accessing all current search parameters
*/
export function getAllSearchParams() {
return getHashRelativeURL().searchParams;
}
export function getObjectPath() {
return getHashRelativeURL().pathname;
}
export function setObjectPath(objectPath) {
let objectPathString;
let url = getHashRelativeURL();
if (objectPath instanceof Array) {
if (objectPath.length > 0 && isDomainObject(objectPath[0])) {
throw 'setObjectPath must be called with either a string, or an array of Domain Objects';
}
objectPathString = objectPath.reduce((pathString, object) => {
return `${pathString}/${objectUtils.makeKeyString(object.identifier)}`;
}, '');
} else {
objectPathString = objectPath;
}
url.pathname = objectPathString;
setLocationFromUrl(url);
}
function isDomainObject(potentialObject) {
return potentialObject.identifier === undefined;
}
function setLocationFromUrl(url) {
window.location.hash = `${url.pathname}${url.search}`;
}
function getHashRelativeURL() {
return new URL(window.location.hash.substring(1), window.location.origin);
}

View File

@@ -1,113 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import {
setSearchParam,
deleteSearchParam,
getAllSearchParams,
getSearchParam,
setAllSearchParams,
getObjectPath,
setObjectPath
} from './openmctLocation';
import {resetApplicationState} from 'utils/testing';
describe('the openmct location utility functions', () => {
afterEach(() => resetApplicationState());
it('The setSearchParam function sets an individual search parameters in the window location hash', () => {
setSearchParam('testParam', 'testValue');
expect(window.location.hash.includes('testParam=testValue')).toBe(true);
});
it('The deleteSearchParam function deletes an individual search paramater in the window location hash', () => {
window.location.hash = '#/?testParam=testValue';
deleteSearchParam('testParam');
expect(window.location.hash.includes('testParam=testValue')).toBe(false);
});
it('The getSearchParam function returns the value of an individual search paramater in the window location hash', () => {
window.location.hash = '#/?testParam=testValue';
expect(getSearchParam('testParam')).toBe('testValue');
});
it('The getAllSearchParams function returns the values of all search paramaters in the window location hash', () => {
window.location.hash = '#/?testParam1=testValue1&testParam2=testValue2&testParam3=testValue3';
let searchParams = getAllSearchParams();
expect(searchParams.get('testParam1')).toBe('testValue1');
expect(searchParams.get('testParam2')).toBe('testValue2');
expect(searchParams.get('testParam3')).toBe('testValue3');
});
it('The setAllSearchParams function replaces all search paramaters in the window location hash', () => {
window.location.hash = '#/?testParam1=testValue1&testParam2=testValue2&testParam3=testValue3';
let searchParams = getAllSearchParams();
searchParams.delete('testParam3');
searchParams.set('testParam1', 'updatedTestValue1');
searchParams.set('newTestParam4', 'newTestValue4');
setAllSearchParams(searchParams);
expect(window.location.hash).toBe('#/?testParam1=updatedTestValue1&testParam2=testValue2&newTestParam4=newTestValue4');
});
it('The getObjectPath function returns the current object path', () => {
window.location.hash = '#/some/object/path?someParameter=someValue';
expect(getObjectPath()).toBe('/some/object/path');
});
it('The setObjectPath function allows the object path to be set to a given string', () => {
window.location.hash = '#/some/object/path?someParameter=someValue';
setObjectPath('/some/other/object/path');
expect(window.location.hash).toBe('#/some/other/object/path?someParameter=someValue');
});
it('The setObjectPath function allows the object path to be set from an array of domain objects', () => {
const OBJECT_PATH = [
{
identifier: {
namespace: 'namespace',
key: 'objectKey1'
}
},
{
identifier: {
namespace: 'namespace',
key: 'objectKey2'
}
},
{
identifier: {
namespace: 'namespace',
key: 'objectKey3'
}
}
];
window.location.hash = '#/some/object/path?someParameter=someValue';
setObjectPath(OBJECT_PATH);
expect(window.location.hash).toBe('#/namespace:objectKey1/namespace:objectKey2/namespace:objectKey3?someParameter=someValue');
});
it('The setObjectPath function throws an error if called with anything other than a string or an array of domain objects', () => {
expect(() => setObjectPath(["array", "of", "strings"])).toThrow();
expect(() => setObjectPath([{}, {someKey: 'someValue'}])).toThrow();
});
});

View File

@@ -64,21 +64,27 @@ const webpackConfig = {
filename: '[name].css',
chunkFilename: '[name].css'
}),
new CopyWebpackPlugin([
{
from: 'src/images/favicons',
to: 'favicons'
},
{
from: './index.html',
transform: function (content) {
return content.toString().replace(/dist\//g, '');
new CopyWebpackPlugin({
patterns: [
{
from: 'src/images/favicons',
to: 'favicons'
},
{
from: './index.html',
transform: function (content) {
return content.toString().replace(/dist\//g, '');
}
}
}
])
]
})
],
module: {
rules: [
{
test: /\.vue$/,
use: 'vue-loader'
},
{
test: /\.(sc|sa|c)ss$/,
use: [
@@ -118,17 +124,14 @@ const webpackConfig = {
}
}
},
{
test: /\.vue$/,
use: 'vue-loader'
}
]
},
ignoreWarnings: [/asset size limit/g],
stats: {
modules: false,
timings: true,
colors: true,
warningsFilter: /asset size limit/g
colors: true
}
};