Compare commits

..

11 Commits

Author SHA1 Message Date
62ff404bb3 Testing with non-reactive API 2021-09-27 16:45:54 -07:00
8243cf5d7b Fix missing object handling in several vues (#4259)
* If there is a pending create request for an id, queue a duplicate request.
* Fix downsteam errors when objects are missing
* Changed error logging from console.log to console.warn
2021-09-27 14:25:33 -07:00
c4c1fea17f snapshot clicked while in edit mode should open in preview mode #4115 (#4257) 2021-09-27 10:14:03 -07:00
5e920e90ce Fix bargraph color selection (#4253)
* Fix typo for attribute key
* Adds section heading for Bar Graph Series
2021-09-24 10:07:53 -07:00
886db23eb6 Hide independent time conductor mode if only 1 mode option is available. (#4250)
* If there is a pending create request for an id, queue a duplicate request.
* Hide independent time conductor mode if there is only 1 mode available
2021-09-23 10:47:35 -07:00
0ccb546a2e starting loading as false, since that makes sense (#4247) 2021-09-23 09:42:39 -07:00
271f8ed38f Fix file selection on pressing enter key (#4246)
* Invoke angular digest cycle after file input selection returns
2021-09-22 15:14:30 -07:00
650f84e95c [Telemetry Tables] Handling Request Loading (#4245)
* added two new events for telemetry collections to denote historical requests starting and ending (can be used for loading indicators)

* updating refresh data to use correct outstanding requests variable, binding request count update methods

* removing loading spinner (replaced with progress bar)

* if making a request, cancel any existing ones

* reverting edge case code updates
2021-09-22 15:01:28 -07:00
b70af5a1bb If there is a pending create request for an id, queue a duplicate request. (#4243) 2021-09-22 09:44:22 -07:00
0af21632db Use timeFormatter.parse to get the timestamp of imagery since the source could be something other than key (#4238) 2021-09-21 11:10:18 -07:00
e2f1ff5442 Notebook conflict auto retry 1.7.7 (#4230) 2021-09-20 14:33:38 -07:00
68 changed files with 613 additions and 529 deletions

View File

@ -2,7 +2,6 @@
* [ ] Have you followed the guidelines in our [Contributing document](https://github.com/nasa/openmct/blob/master/CONTRIBUTING.md)? * [ ] Have you followed the guidelines in our [Contributing document](https://github.com/nasa/openmct/blob/master/CONTRIBUTING.md)?
* [ ] Have you checked to ensure there aren't other open [Pull Requests](https://github.com/nasa/openmct/pulls) for the same update/change? * [ ] Have you checked to ensure there aren't other open [Pull Requests](https://github.com/nasa/openmct/pulls) for the same update/change?
* [ ] Is this change backwards compatible? Will users need to change how they are calling the API, or how they've extended core plugins such as Tables or Plots?
### Author Checklist ### Author Checklist

View File

@ -317,7 +317,6 @@ checklist).
### Reviewer Checklist ### Reviewer Checklist
* [ ] Changes appear to address issue? * [ ] Changes appear to address issue?
* [ ] Changes appear not to be breaking changes?
* [ ] Appropriate unit tests included? * [ ] Appropriate unit tests included?
* [ ] Code style and in-line documentation are appropriate? * [ ] Code style and in-line documentation are appropriate?
* [ ] Commit messages meet standards? * [ ] Commit messages meet standards?

View File

@ -41,6 +41,11 @@ define([
"$scope" "$scope"
] ]
} }
],
"routes": [
{
"templateUrl": "templates/exampleForm.html"
}
] ]
} }
} }

View File

@ -96,9 +96,5 @@ define([
return this.workerInterface.subscribe(workerRequest, callback); return this.workerInterface.subscribe(workerRequest, callback);
}; };
GeneratorProvider.prototype.destroy = function () {
this.workerInterface.destroy();
};
return GeneratorProvider; return GeneratorProvider;
}); });

View File

@ -40,11 +40,6 @@ define([
this.callbacks = {}; this.callbacks = {};
} }
WorkerInterface.prototype.destroy = function () {
delete this.worker.onmessage;
this.worker.terminate();
};
WorkerInterface.prototype.onMessage = function (message) { WorkerInterface.prototype.onMessage = function (message) {
message = message.data; message = message.data;
var callback = this.callbacks[message.id]; var callback = this.callbacks[message.id];

View File

@ -181,11 +181,7 @@ define([
} }
}); });
const generatorProvider = new GeneratorProvider(); openmct.telemetry.addProvider(new GeneratorProvider());
openmct.once('destroy', () => {
generatorProvider.destroy();
});
openmct.telemetry.addProvider(generatorProvider);
openmct.telemetry.addProvider(new GeneratorMetadataProvider()); openmct.telemetry.addProvider(new GeneratorMetadataProvider());
openmct.telemetry.addProvider(new SinewaveLimitProvider()); openmct.telemetry.addProvider(new SinewaveLimitProvider());
}; };

View File

@ -164,6 +164,16 @@ define([
"license": "license-apache", "license": "license-apache",
"link": "http://logging.apache.org/log4net/license.html" "link": "http://logging.apache.org/log4net/license.html"
} }
],
"routes": [
{
"when": "/licenses",
"template": licensesTemplate
},
{
"when": "/licenses-md",
"template": licensesExportMdTemplate
}
] ]
} }
} }

View File

@ -50,6 +50,8 @@ define([
name: "platform/commonUI/browse", name: "platform/commonUI/browse",
definition: { definition: {
"extensions": { "extensions": {
"routes": [
],
"constants": [ "constants": [
{ {
"key": "DEFAULT_PATH", "key": "DEFAULT_PATH",

View File

@ -39,6 +39,9 @@ define(
this.callbacks = []; this.callbacks = [];
this.checks = []; this.checks = [];
this.$window = $window; this.$window = $window;
this.oldUnload = $window.onbeforeunload;
$window.onbeforeunload = this.onBeforeUnload.bind(this);
} }
/** /**

View File

@ -71,8 +71,7 @@ define([
"implementation": TickerService, "implementation": TickerService,
"depends": [ "depends": [
"$timeout", "$timeout",
"now", "now"
"$rootScope"
] ]
}, },
{ {

View File

@ -32,13 +32,8 @@ define(
* @param $timeout Angular's $timeout * @param $timeout Angular's $timeout
* @param {Function} now function to provide the current time in ms * @param {Function} now function to provide the current time in ms
*/ */
function TickerService($timeout, now, $rootScope) { function TickerService($timeout, now) {
var self = this; var self = this;
var timeoutId;
$rootScope.$on('$destroy', function () {
$timeout.cancel(timeoutId);
});
function tick() { function tick() {
var timestamp = now(), var timestamp = now(),
@ -53,7 +48,7 @@ define(
} }
// Try to update at exactly the next second // Try to update at exactly the next second
timeoutId = $timeout(tick, 1000 - millis, true); $timeout(tick, 1000 - millis, true);
} }
tick(); tick();

View File

@ -30,18 +30,16 @@ define(
var mockTimeout, var mockTimeout,
mockNow, mockNow,
mockCallback, mockCallback,
tickerService, tickerService;
mockRootScope;
beforeEach(function () { beforeEach(function () {
mockTimeout = jasmine.createSpy('$timeout'); mockTimeout = jasmine.createSpy('$timeout');
mockNow = jasmine.createSpy('now'); mockNow = jasmine.createSpy('now');
mockCallback = jasmine.createSpy('callback'); mockCallback = jasmine.createSpy('callback');
mockRootScope = jasmine.createSpyObj('rootScope', ['$on']);
mockNow.and.returnValue(TEST_TIMESTAMP); mockNow.and.returnValue(TEST_TIMESTAMP);
tickerService = new TickerService(mockTimeout, mockNow, mockRootScope); tickerService = new TickerService(mockTimeout, mockNow);
}); });
it("notifies listeners of clock ticks", function () { it("notifies listeners of clock ticks", function () {

View File

@ -44,9 +44,11 @@ define(
setText(result.name); setText(result.name);
scope.ngModel[scope.field] = result; scope.ngModel[scope.field] = result;
control.$setValidity("file-input", true); control.$setValidity("file-input", true);
scope.$digest();
}, function () { }, function () {
setText('Select File'); setText('Select File');
control.$setValidity("file-input", false); control.$setValidity("file-input", false);
scope.$digest();
}); });
} }

View File

@ -58,7 +58,7 @@ define([
) { ) {
var $http = this.$http, var $http = this.$http,
$log = this.$log, $log = this.$log,
app = angular.module(Constants.MODULE_NAME, []), app = angular.module(Constants.MODULE_NAME, ["ngRoute"]),
loader = new BundleLoader($http, $log, openmct.legacyRegistry), loader = new BundleLoader($http, $log, openmct.legacyRegistry),
resolver = new BundleResolver( resolver = new BundleResolver(
new ExtensionResolver( new ExtensionResolver(

View File

@ -28,7 +28,8 @@
define( define(
[ [
'./FrameworkLayer', './FrameworkLayer',
'angular' 'angular',
'angular-route'
], ],
function ( function (
FrameworkLayer, FrameworkLayer,

View File

@ -138,6 +138,34 @@ define(
} }
} }
// Custom registration function for extensions of category "route"
function registerRoute(extension) {
var app = this.app,
$log = this.$log,
route = Object.create(extension);
// Adjust path for bundle
if (route.templateUrl) {
route.templateUrl = [
route.bundle.path,
route.bundle.resources,
route.templateUrl
].join(Constants.SEPARATOR);
}
// Log the registration
$log.info("Registering route: " + (route.key || route.when));
// Register the route with Angular
app.config(['$routeProvider', function ($routeProvider) {
if (route.when) {
$routeProvider.when(route.when, route);
} else {
$routeProvider.otherwise(route);
}
}]);
}
// Handle service compositing // Handle service compositing
function registerComponents(components) { function registerComponents(components) {
var app = this.app, var app = this.app,
@ -166,6 +194,13 @@ define(
CustomRegistrars.prototype.constants = CustomRegistrars.prototype.constants =
mapUpon(registerConstant); mapUpon(registerConstant);
/**
* Register Angular routes.
* @param {Array} extensions the resolved extensions
*/
CustomRegistrars.prototype.routes =
mapUpon(registerRoute);
/** /**
* Register Angular directives. * Register Angular directives.
* @param {Array} extensions the resolved extensions * @param {Array} extensions the resolved extensions

View File

@ -57,6 +57,7 @@ define(
expect(customRegistrars.directives).toBeTruthy(); expect(customRegistrars.directives).toBeTruthy();
expect(customRegistrars.controllers).toBeTruthy(); expect(customRegistrars.controllers).toBeTruthy();
expect(customRegistrars.services).toBeTruthy(); expect(customRegistrars.services).toBeTruthy();
expect(customRegistrars.routes).toBeTruthy();
expect(customRegistrars.constants).toBeTruthy(); expect(customRegistrars.constants).toBeTruthy();
expect(customRegistrars.runs).toBeTruthy(); expect(customRegistrars.runs).toBeTruthy();
}); });
@ -138,6 +139,47 @@ define(
expect(mockLog.warn.calls.count()).toEqual(0); expect(mockLog.warn.calls.count()).toEqual(0);
}); });
it("allows routes to be registered", function () {
var mockRouteProvider = jasmine.createSpyObj(
"$routeProvider",
["when", "otherwise"]
),
bundle = {
path: "test/bundle",
resources: "res"
},
routes = [
{
when: "foo",
templateUrl: "templates/test.html",
bundle: bundle
},
{
templateUrl: "templates/default.html",
bundle: bundle
}
];
customRegistrars.routes(routes);
// Give it the route provider based on its config call
mockApp.config.calls.all().forEach(function (call) {
// Invoke the provided callback
call.args[0][1](mockRouteProvider);
});
// The "when" clause should have been mapped to the when method...
expect(mockRouteProvider.when).toHaveBeenCalled();
expect(mockRouteProvider.when.calls.mostRecent().args[0]).toEqual("foo");
expect(mockRouteProvider.when.calls.mostRecent().args[1].templateUrl)
.toEqual("test/bundle/res/templates/test.html");
// ...while the other should have been treated as a default route
expect(mockRouteProvider.otherwise).toHaveBeenCalled();
expect(mockRouteProvider.otherwise.calls.mostRecent().args[0].templateUrl)
.toEqual("test/bundle/res/templates/default.html");
});
it("accepts components for service compositing", function () { it("accepts components for service compositing", function () {
// Most relevant code will be exercised in service compositor spec // Most relevant code will be exercised in service compositor spec
expect(customRegistrars.components).toBeTruthy(); expect(customRegistrars.components).toBeTruthy();

View File

@ -110,15 +110,8 @@ define([
worker = workerService.run('bareBonesSearchWorker'); worker = workerService.run('bareBonesSearchWorker');
} }
function handleWorkerMessage(messageEvent) { worker.addEventListener('message', function (messageEvent) {
provider.onWorkerMessage(messageEvent); provider.onWorkerMessage(messageEvent);
}
worker.addEventListener('message', handleWorkerMessage);
this.openmct.once('destroy', () => {
worker.removeEventListener('message', handleWorkerMessage);
worker.terminate();
}); });
return worker; return worker;

View File

@ -31,6 +31,7 @@ define([
'objectUtils', 'objectUtils',
'./plugins/plugins', './plugins/plugins',
'./adapter/indicators/legacy-indicators-plugin', './adapter/indicators/legacy-indicators-plugin',
'./plugins/buildInfo/plugin',
'./ui/registries/ViewRegistry', './ui/registries/ViewRegistry',
'./plugins/imagery/plugin', './plugins/imagery/plugin',
'./ui/registries/InspectorViewRegistry', './ui/registries/InspectorViewRegistry',
@ -39,7 +40,6 @@ define([
'./ui/router/Browse', './ui/router/Browse',
'../platform/framework/src/Main', '../platform/framework/src/Main',
'./ui/layout/Layout.vue', './ui/layout/Layout.vue',
'./ui/inspector/styles/StylesManager',
'../platform/core/src/objects/DomainObjectImpl', '../platform/core/src/objects/DomainObjectImpl',
'../platform/core/src/capabilities/ContextualDomainObject', '../platform/core/src/capabilities/ContextualDomainObject',
'./ui/preview/plugin', './ui/preview/plugin',
@ -60,6 +60,7 @@ define([
objectUtils, objectUtils,
plugins, plugins,
LegacyIndicatorsPlugin, LegacyIndicatorsPlugin,
buildInfoPlugin,
ViewRegistry, ViewRegistry,
ImageryPlugin, ImageryPlugin,
InspectorViewRegistry, InspectorViewRegistry,
@ -68,7 +69,6 @@ define([
Browse, Browse,
Main, Main,
Layout, Layout,
stylesManager,
DomainObjectImpl, DomainObjectImpl,
ContextualDomainObject, ContextualDomainObject,
PreviewPlugin, PreviewPlugin,
@ -123,7 +123,6 @@ define([
}; };
this.destroy = this.destroy.bind(this); this.destroy = this.destroy.bind(this);
/** /**
* Tracks current selection state of the application. * Tracks current selection state of the application.
* @private * @private
@ -289,6 +288,8 @@ define([
this.install(this.plugins.ObjectInterceptors()); this.install(this.plugins.ObjectInterceptors());
this.install(this.plugins.NonEditableFolder()); this.install(this.plugins.NonEditableFolder());
this.install(this.plugins.DeviceClassifier()); this.install(this.plugins.DeviceClassifier());
this._isVue = true;
} }
MCT.prototype = Object.create(EventEmitter.prototype); MCT.prototype = Object.create(EventEmitter.prototype);
@ -377,7 +378,6 @@ define([
* MCT; if undefined, MCT will be run in the body of the document * MCT; if undefined, MCT will be run in the body of the document
*/ */
MCT.prototype.start = function (domElement = document.body, isHeadlessMode = false) { MCT.prototype.start = function (domElement = document.body, isHeadlessMode = false) {
if (this.types.get('layout') === undefined) { if (this.types.get('layout') === undefined) {
this.install(this.plugins.DisplayLayout({ this.install(this.plugins.DisplayLayout({
showAsView: ['summary-widget'] showAsView: ['summary-widget']
@ -436,9 +436,6 @@ define([
domElement.appendChild(appLayout.$mount().$el); domElement.appendChild(appLayout.$mount().$el);
this.layout = appLayout.$refs.layout; this.layout = appLayout.$refs.layout;
this.once('destroy', () => {
appLayout.$destroy();
});
Browse(this); Browse(this);
} }
@ -467,40 +464,9 @@ define([
}; };
MCT.prototype.destroy = function () { MCT.prototype.destroy = function () {
if (this._destroyed === true) {
return;
}
window.removeEventListener('beforeunload', this.destroy); window.removeEventListener('beforeunload', this.destroy);
this.emit('destroy'); this.emit('destroy');
this.removeAllListeners(); this.router.destroy();
if (this.$injector) {
this.$injector.get('$rootScope').$destroy();
this.$injector = null;
}
if (this.$angular) {
this.$angular.element(this.element).off().removeData();
this.$angular.element(this.element).empty();
this.$angular = null;
}
this.overlays.destroy();
if (this.element) {
this.element.remove();
}
stylesManager.default.removeAllListeners();
window.angular = null;
window.openmct = null;
Object.keys(require.cache).forEach(key => delete require.cache[key]);
this._destroyed = true;
}; };
MCT.prototype.plugins = plugins; MCT.prototype.plugins = plugins;

View File

@ -32,10 +32,6 @@ define([
// cannot be injected. // cannot be injected.
function AlternateCompositionInitializer(openmct) { function AlternateCompositionInitializer(openmct) {
AlternateCompositionCapability.appliesTo = function (model, id) { AlternateCompositionCapability.appliesTo = function (model, id) {
openmct.once('destroy', () => {
delete AlternateCompositionCapability.appliesTo;
});
model = objectUtils.toNewFormat(model, id || ''); model = objectUtils.toNewFormat(model, id || '');
return Boolean(openmct.composition.get(model)); return Boolean(openmct.composition.get(model));

View File

@ -81,14 +81,8 @@ define([
return models; return models;
} }
return this.apiFetch(missingIds) //Temporary fix for missing models - don't retry using this.apiFetch
.then(function (apiResults) {
Object.keys(apiResults).forEach(function (k) {
models[k] = apiResults[k];
});
return models; return models;
});
}.bind(this)); }.bind(this));
}; };

View File

@ -28,10 +28,6 @@ export default class Editor extends EventEmitter {
super(); super();
this.editing = false; this.editing = false;
this.openmct = openmct; this.openmct = openmct;
openmct.once('destroy', () => {
this.removeAllListeners();
});
} }
/** /**

View File

@ -44,15 +44,7 @@ describe('The ActionCollection', () => {
} }
}); });
openmct.$injector.get.and.callFake((key) => { openmct.$injector.get.and.returnValue(mockIdentifierService);
return {
'identifierService': mockIdentifierService,
'$rootScope': {
'$destroy': () => {}
}
}[key];
});
mockObjectPath = [ mockObjectPath = [
{ {
name: 'mock folder', name: 'mock folder',

View File

@ -110,7 +110,7 @@ class ActionsAPI extends EventEmitter {
return actionsObject; return actionsObject;
} }
_groupAndSortActions(actionsArray) { _groupAndSortActions(actionsArray = []) {
if (!Array.isArray(actionsArray) && typeof actionsArray === 'object') { if (!Array.isArray(actionsArray) && typeof actionsArray === 'object') {
actionsArray = Object.keys(actionsArray).map(key => actionsArray[key]); actionsArray = Object.keys(actionsArray).map(key => actionsArray[key]);
} }

View File

@ -0,0 +1,2 @@
export default class ConflictError extends Error {
}

View File

@ -26,6 +26,7 @@ import RootRegistry from './RootRegistry';
import RootObjectProvider from './RootObjectProvider'; import RootObjectProvider from './RootObjectProvider';
import EventEmitter from 'EventEmitter'; import EventEmitter from 'EventEmitter';
import InterceptorRegistry from './InterceptorRegistry'; import InterceptorRegistry from './InterceptorRegistry';
import ConflictError from './ConflictError';
/** /**
* Utilities for loading, saving, and manipulating domain objects. * Utilities for loading, saving, and manipulating domain objects.
@ -34,6 +35,7 @@ import InterceptorRegistry from './InterceptorRegistry';
*/ */
function ObjectAPI(typeRegistry, openmct) { function ObjectAPI(typeRegistry, openmct) {
this.openmct = openmct;
this.typeRegistry = typeRegistry; this.typeRegistry = typeRegistry;
this.eventEmitter = new EventEmitter(); this.eventEmitter = new EventEmitter();
this.providers = {}; this.providers = {};
@ -47,6 +49,10 @@ function ObjectAPI(typeRegistry, openmct) {
this.interceptorRegistry = new InterceptorRegistry(); this.interceptorRegistry = new InterceptorRegistry();
this.SYNCHRONIZED_OBJECT_TYPES = ['notebook', 'plan']; this.SYNCHRONIZED_OBJECT_TYPES = ['notebook', 'plan'];
this.errors = {
Conflict: ConflictError
};
} }
/** /**
@ -181,8 +187,17 @@ ObjectAPI.prototype.get = function (identifier, abortSignal) {
let objectPromise = provider.get(identifier, abortSignal).then(result => { let objectPromise = provider.get(identifier, abortSignal).then(result => {
delete this.cache[keystring]; delete this.cache[keystring];
result = this.applyGetInterceptors(identifier, result); result = this.applyGetInterceptors(identifier, result);
return result;
}).catch((result) => {
console.warn(`Failed to retrieve ${keystring}:`, result);
delete this.cache[keystring];
result = this.applyGetInterceptors(identifier);
return result; return result;
}); });
@ -285,6 +300,7 @@ ObjectAPI.prototype.isPersistable = function (idOrKeyString) {
ObjectAPI.prototype.save = function (domainObject) { ObjectAPI.prototype.save = function (domainObject) {
let provider = this.getProvider(domainObject.identifier); let provider = this.getProvider(domainObject.identifier);
let savedResolve; let savedResolve;
let savedReject;
let result; let result;
if (!this.isPersistable(domainObject.identifier)) { if (!this.isPersistable(domainObject.identifier)) {
@ -294,13 +310,17 @@ ObjectAPI.prototype.save = function (domainObject) {
} else { } else {
const persistedTime = Date.now(); const persistedTime = Date.now();
if (domainObject.persisted === undefined) { if (domainObject.persisted === undefined) {
result = new Promise((resolve) => { result = new Promise((resolve, reject) => {
savedResolve = resolve; savedResolve = resolve;
savedReject = reject;
}); });
domainObject.persisted = persistedTime; domainObject.persisted = persistedTime;
provider.create(domainObject).then((response) => { provider.create(domainObject)
.then((response) => {
this.mutate(domainObject, 'persisted', persistedTime); this.mutate(domainObject, 'persisted', persistedTime);
savedResolve(response); savedResolve(response);
}).catch((error) => {
savedReject(error);
}); });
} else { } else {
domainObject.persisted = persistedTime; domainObject.persisted = persistedTime;

View File

@ -17,7 +17,11 @@ class OverlayAPI {
this.dismissLastOverlay = this.dismissLastOverlay.bind(this); this.dismissLastOverlay = this.dismissLastOverlay.bind(this);
document.addEventListener('keyup', this.dismissLastOverlay); document.addEventListener('keyup', (event) => {
if (event.key === 'Escape') {
this.dismissLastOverlay();
}
});
} }
@ -48,20 +52,18 @@ class OverlayAPI {
/** /**
* private * private
*/ */
dismissLastOverlay(event) { dismissLastOverlay() {
if (event.key === 'Escape') {
let lastOverlay = this.activeOverlays[this.activeOverlays.length - 1]; let lastOverlay = this.activeOverlays[this.activeOverlays.length - 1];
if (lastOverlay && lastOverlay.dismissable) { if (lastOverlay && lastOverlay.dismissable) {
lastOverlay.dismiss(); lastOverlay.dismiss();
} }
} }
}
/** /**
* A description of option properties that can be passed into the overlay * A description of option properties that can be passed into the overlay
* @typedef options * @typedef options
* @property {object} element DOMElement that is to be inserted/shown on the overlay * @property {object} element DOMElement that is to be inserted/shown on the overlay
* @property {string} size prefered size of the overlay (large, small, fit) * @property {string} size preferred size of the overlay (large, small, fit)
* @property {array} buttons optional button objects with label and callback properties * @property {array} buttons optional button objects with label and callback properties
* @property {function} onDestroy callback to be called when overlay is destroyed * @property {function} onDestroy callback to be called when overlay is destroyed
* @property {boolean} dismissable allow user to dismiss overlay by using esc, and clicking away * @property {boolean} dismissable allow user to dismiss overlay by using esc, and clicking away
@ -130,10 +132,6 @@ class OverlayAPI {
return progressDialog; return progressDialog;
} }
destroy() {
document.removeEventListener('keyup', this.dismissLastOverlay);
}
} }
export default OverlayAPI; export default OverlayAPI;

View File

@ -32,10 +32,6 @@ export default class StatusAPI extends EventEmitter {
this.get = this.get.bind(this); this.get = this.get.bind(this);
this.set = this.set.bind(this); this.set = this.set.bind(this);
this.observe = this.observe.bind(this); this.observe = this.observe.bind(this);
openmct.once('destroy', () => {
this.removeAllListeners();
});
} }
get(identifier) { get(identifier) {

View File

@ -483,6 +483,10 @@ define([
* @returns {Object<String, {TelemetryValueFormatter}>} * @returns {Object<String, {TelemetryValueFormatter}>}
*/ */
TelemetryAPI.prototype.getFormatMap = function (metadata) { TelemetryAPI.prototype.getFormatMap = function (metadata) {
if (!metadata) {
return {};
}
if (!this.formatMapCache.has(metadata)) { if (!this.formatMapCache.has(metadata)) {
const formatMap = metadata.values().reduce(function (map, valueMetadata) { const formatMap = metadata.values().reduce(function (map, valueMetadata) {
map[valueMetadata.key] = this.getValueFormatter(valueMetadata); map[valueMetadata.key] = this.getValueFormatter(valueMetadata);

View File

@ -130,8 +130,13 @@ export class TelemetryCollection extends EventEmitter {
this.options.onPartialResponse = this._processNewTelemetry.bind(this); this.options.onPartialResponse = this._processNewTelemetry.bind(this);
try { try {
if (this.requestAbort) {
this.requestAbort.abort();
}
this.requestAbort = new AbortController(); this.requestAbort = new AbortController();
this.options.signal = this.requestAbort.signal; this.options.signal = this.requestAbort.signal;
this.emit('requestStarted');
historicalData = await this.historicalProvider.request(this.domainObject, this.options); historicalData = await this.historicalProvider.request(this.domainObject, this.options);
} catch (error) { } catch (error) {
if (error.name !== 'AbortError') { if (error.name !== 'AbortError') {
@ -140,6 +145,7 @@ export class TelemetryCollection extends EventEmitter {
} }
} }
this.emit('requestEnded');
this.requestAbort = undefined; this.requestAbort = undefined;
this._processNewTelemetry(historicalData); this._processNewTelemetry(historicalData);

View File

@ -41,7 +41,6 @@ const DEFAULTS = [
'platform/forms', 'platform/forms',
'platform/identity', 'platform/identity',
'platform/persistence/aggregator', 'platform/persistence/aggregator',
'platform/persistence/queue',
'platform/policy', 'platform/policy',
'platform/entanglement', 'platform/entanglement',
'platform/search', 'platform/search',

View File

@ -32,7 +32,7 @@ describe('the plugin', function () {
let openmct; let openmct;
let composition; let composition;
beforeEach((done) => { beforeEach(() => {
openmct = createOpenMct(); openmct = createOpenMct();
@ -47,11 +47,6 @@ describe('the plugin', function () {
} }
})); }));
openmct.on('start', done);
openmct.startHeadless();
composition = openmct.composition.get({identifier});
spyOn(couchPlugin.couchProvider, 'getObjectsByFilter').and.returnValue(Promise.resolve([ spyOn(couchPlugin.couchProvider, 'getObjectsByFilter').and.returnValue(Promise.resolve([
{ {
identifier: { identifier: {
@ -66,6 +61,19 @@ describe('the plugin', function () {
} }
} }
])); ]));
spyOn(couchPlugin.couchProvider, "get").and.callFake((id) => {
return Promise.resolve({
identifier: id
});
});
return new Promise((resolve) => {
openmct.once('start', resolve);
openmct.startHeadless();
}).then(() => {
composition = openmct.composition.get({identifier});
});
}); });
afterEach(() => { afterEach(() => {

View File

@ -96,11 +96,11 @@ export default {
this.timestampKey = this.openmct.time.timeSystem().key; this.timestampKey = this.openmct.time.timeSystem().key;
this.valueMetadata = this this.valueMetadata = this.metadata ? this
.metadata .metadata
.valuesForHints(['range'])[0]; .valuesForHints(['range'])[0] : undefined;
this.valueKey = this.valueMetadata.key; this.valueKey = this.valueMetadata ? this.valueMetadata.key : undefined;
this.unsubscribe = this.openmct this.unsubscribe = this.openmct
.telemetry .telemetry
@ -151,7 +151,10 @@ export default {
size: 1, size: 1,
strategy: 'latest' strategy: 'latest'
}) })
.then((array) => this.updateValues(array[array.length - 1])); .then((array) => this.updateValues(array[array.length - 1]))
.catch((error) => {
console.warn('Error fetching data', error);
});
}, },
updateBounds(bounds, isTick) { updateBounds(bounds, isTick) {
this.bounds = bounds; this.bounds = bounds;

View File

@ -73,8 +73,9 @@ export default {
hasUnits() { hasUnits() {
let itemsWithUnits = this.items.filter((item) => { let itemsWithUnits = this.items.filter((item) => {
let metadata = this.openmct.telemetry.getMetadata(item.domainObject); let metadata = this.openmct.telemetry.getMetadata(item.domainObject);
const valueMetadatas = metadata ? metadata.valueMetadatas : [];
return this.metadataHasUnits(metadata.valueMetadatas); return this.metadataHasUnits(valueMetadatas);
}); });

View File

@ -98,6 +98,8 @@ describe('the plugin', function () {
conditionSetDefinition.initialize(mockConditionSetDomainObject); conditionSetDefinition.initialize(mockConditionSetDomainObject);
spyOn(openmct.objects, "save").and.returnValue(Promise.resolve(true));
openmct.on('start', done); openmct.on('start', done);
openmct.startHeadless(); openmct.startHeadless();
}); });

View File

@ -101,7 +101,7 @@ export default {
addChildren(domainObject) { addChildren(domainObject) {
let keyString = this.openmct.objects.makeKeyString(domainObject.identifier); let keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
let metadata = this.openmct.telemetry.getMetadata(domainObject); let metadata = this.openmct.telemetry.getMetadata(domainObject);
let metadataWithFilters = metadata.valueMetadatas.filter(value => value.filters); let metadataWithFilters = metadata ? metadata.valueMetadatas.filter(value => value.filters) : [];
let hasFiltersWithKeyString = this.persistedFilters[keyString] !== undefined; let hasFiltersWithKeyString = this.persistedFilters[keyString] !== undefined;
let mutateFilters = false; let mutateFilters = false;
let childObject = { let childObject = {

View File

@ -159,7 +159,7 @@ export default {
let image = { ...datum }; let image = { ...datum };
image.formattedTime = this.formatTime(datum); image.formattedTime = this.formatTime(datum);
image.url = this.formatImageUrl(datum); image.url = this.formatImageUrl(datum);
image.time = datum[this.timeKey]; image.time = this.parseTime(image.formattedTime);
image.imageDownloadName = this.getImageDownloadName(datum); image.imageDownloadName = this.getImageDownloadName(datum);
return image; return image;

View File

@ -75,16 +75,7 @@ describe("the plugin", () => {
mockDialogService.getUserInput.and.returnValue(mockPromise); mockDialogService.getUserInput.and.returnValue(mockPromise);
spyOn(openmct.$injector, 'get'); spyOn(openmct.$injector, 'get').and.returnValue(mockDialogService);
openmct.$injector.get.and.callFake((key) => {
return {
'dialogService': mockDialogService,
'$rootScope': {
'$destroy': () => {}
}
}[key];
});
spyOn(compositionAPI, 'get').and.returnValue(mockComposition); spyOn(compositionAPI, 'get').and.returnValue(mockComposition);
spyOn(openmct.objects, 'save').and.returnValue(Promise.resolve(true)); spyOn(openmct.objects, 'save').and.returnValue(Promise.resolve(true));

View File

@ -180,9 +180,13 @@ export default {
this.openmct.notifications.alert(message); this.openmct.notifications.alert(message);
} }
if (this.openmct.editor.isEditing()) {
this.previewEmbed();
} else {
const relativeHash = hash.slice(hash.indexOf('#')); const relativeHash = hash.slice(hash.indexOf('#'));
const url = new URL(relativeHash, `${location.protocol}//${location.host}${location.pathname}`); const url = new URL(relativeHash, `${location.protocol}//${location.host}${location.pathname}`);
this.openmct.router.navigate(url.hash); this.openmct.router.navigate(url.hash);
}
}, },
formatTime(unixTime, timeFormat) { formatTime(unixTime, timeFormat) {
return Moment.utc(unixTime).format(timeFormat); return Moment.utc(unixTime).format(timeFormat);

View File

@ -0,0 +1,72 @@
import {NOTEBOOK_TYPE} from './notebook-constants';
export default function (openmct) {
const apiSave = openmct.objects.save.bind(openmct.objects);
openmct.objects.save = async (domainObject) => {
if (domainObject.type !== NOTEBOOK_TYPE) {
return apiSave(domainObject);
}
const localMutable = openmct.objects._toMutable(domainObject);
let result;
try {
result = await apiSave(localMutable);
} catch (error) {
if (error instanceof openmct.objects.errors.Conflict) {
result = resolveConflicts(localMutable, openmct);
} else {
result = Promise.reject(error);
}
} finally {
openmct.objects.destroyMutable(localMutable);
}
return result;
};
}
function resolveConflicts(localMutable, openmct) {
return openmct.objects.getMutable(localMutable.identifier).then((remoteMutable) => {
const localEntries = localMutable.configuration.entries;
remoteMutable.$refresh(remoteMutable);
applyLocalEntries(remoteMutable, localEntries);
openmct.objects.destroyMutable(remoteMutable);
return true;
});
}
function applyLocalEntries(mutable, entries) {
Object.entries(entries).forEach(([sectionKey, pagesInSection]) => {
Object.entries(pagesInSection).forEach(([pageKey, localEntries]) => {
const remoteEntries = mutable.configuration.entries[sectionKey][pageKey];
const mergedEntries = [].concat(remoteEntries);
let shouldMutate = false;
const locallyAddedEntries = _.differenceBy(localEntries, remoteEntries, 'id');
const locallyModifiedEntries = _.differenceWith(localEntries, remoteEntries, (localEntry, remoteEntry) => {
return localEntry.id === remoteEntry.id && localEntry.text === remoteEntry.text;
});
locallyAddedEntries.forEach((localEntry) => {
mergedEntries.push(localEntry);
shouldMutate = true;
});
locallyModifiedEntries.forEach((locallyModifiedEntry) => {
let mergedEntry = mergedEntries.find(entry => entry.id === locallyModifiedEntry.id);
if (mergedEntry !== undefined) {
mergedEntry.text = locallyModifiedEntry.text;
shouldMutate = true;
}
});
if (shouldMutate) {
mutable.$set(`configuration.entries.${sectionKey}.${pageKey}`, mergedEntries);
}
});
});
}

View File

@ -2,6 +2,7 @@ import CopyToNotebookAction from './actions/CopyToNotebookAction';
import Notebook from './components/Notebook.vue'; import Notebook from './components/Notebook.vue';
import NotebookSnapshotIndicator from './components/NotebookSnapshotIndicator.vue'; import NotebookSnapshotIndicator from './components/NotebookSnapshotIndicator.vue';
import SnapshotContainer from './snapshot-container'; import SnapshotContainer from './snapshot-container';
import monkeyPatchObjectAPIForNotebooks from './monkeyPatchObjectAPIForNotebooks.js';
import { notebookImageMigration } from '../notebook/utils/notebook-migration'; import { notebookImageMigration } from '../notebook/utils/notebook-migration';
import { NOTEBOOK_TYPE } from './notebook-constants'; import { NOTEBOOK_TYPE } from './notebook-constants';
@ -117,10 +118,6 @@ export default function NotebookPlugin() {
key: 'notebook-snapshot-indicator' key: 'notebook-snapshot-indicator'
}; };
openmct.once('destroy', () => {
snapshotContainer.destroy();
});
openmct.indicators.add(indicator); openmct.indicators.add(indicator);
openmct.objectViews.addProvider({ openmct.objectViews.addProvider({
@ -169,5 +166,7 @@ export default function NotebookPlugin() {
return domainObject; return domainObject;
} }
}); });
monkeyPatchObjectAPIForNotebooks(openmct);
}; };
} }

View File

@ -154,6 +154,8 @@ describe("Notebook plugin:", () => {
testObjectProvider.get.and.returnValue(Promise.resolve(notebookViewObject)); testObjectProvider.get.and.returnValue(Promise.resolve(notebookViewObject));
openmct.objects.addProvider('test-namespace', testObjectProvider); openmct.objects.addProvider('test-namespace', testObjectProvider);
testObjectProvider.observe.and.returnValue(() => {}); testObjectProvider.observe.and.returnValue(() => {});
testObjectProvider.create.and.returnValue(Promise.resolve(true));
testObjectProvider.update.and.returnValue(Promise.resolve(true));
return openmct.objects.getMutable(notebookViewObject.identifier).then((mutableObject) => { return openmct.objects.getMutable(notebookViewObject.identifier).then((mutableObject) => {
mutableNotebookObject = mutableObject; mutableNotebookObject = mutableObject;

View File

@ -85,8 +85,4 @@ export default class SnapshotContainer extends EventEmitter {
return this.saveSnapshots(updatedSnapshots); return this.saveSnapshots(updatedSnapshots);
} }
destroy() {
delete SnapshotContainer.instance;
}
} }

View File

@ -125,7 +125,7 @@ export function addNotebookEntry(openmct, domainObject, notebookStorage, embed =
const newEntries = addEntryIntoPage(notebookStorage, entries, entry); const newEntries = addEntryIntoPage(notebookStorage, entries, entry);
addDefaultClass(domainObject, openmct); addDefaultClass(domainObject, openmct);
openmct.objects.mutate(domainObject, 'configuration.entries', newEntries); domainObject.configuration.entries = newEntries;
return id; return id;
} }

View File

@ -22,18 +22,7 @@
import * as NotebookEntries from './notebook-entries'; import * as NotebookEntries from './notebook-entries';
import { createOpenMct, resetApplicationState } from 'utils/testing'; import { createOpenMct, resetApplicationState } from 'utils/testing';
let notebookStorage; const notebookStorage = {
let notebookEntries;
let notebookDomainObject;
let selectedSection;
let selectedPage;
let openmct;
let mockIdentifierService;
describe('Notebook Entries:', () => {
beforeEach(() => {
notebookStorage = {
name: 'notebook', name: 'notebook',
identifier: { identifier: {
namespace: '', namespace: '',
@ -43,13 +32,13 @@ describe('Notebook Entries:', () => {
defaultPageId: '8b548fd9-2b8a-4b02-93a9-4138e22eba00' defaultPageId: '8b548fd9-2b8a-4b02-93a9-4138e22eba00'
}; };
notebookEntries = { const notebookEntries = {
'03a79b6a-971c-4e56-9892-ec536332c3f0': { '03a79b6a-971c-4e56-9892-ec536332c3f0': {
'8b548fd9-2b8a-4b02-93a9-4138e22eba00': [] '8b548fd9-2b8a-4b02-93a9-4138e22eba00': []
} }
}; };
notebookDomainObject = { const notebookDomainObject = {
identifier: { identifier: {
key: 'notebook', key: 'notebook',
namespace: '' namespace: ''
@ -65,7 +54,7 @@ describe('Notebook Entries:', () => {
} }
}; };
selectedSection = { const selectedSection = {
id: '03a79b6a-971c-4e56-9892-ec536332c3f0', id: '03a79b6a-971c-4e56-9892-ec536332c3f0',
isDefault: false, isDefault: false,
isSelected: true, isSelected: true,
@ -96,7 +85,7 @@ describe('Notebook Entries:', () => {
sectionTitle: 'Section' sectionTitle: 'Section'
}; };
selectedPage = { const selectedPage = {
id: '8b548fd9-2b8a-4b02-93a9-4138e22eba00', id: '8b548fd9-2b8a-4b02-93a9-4138e22eba00',
isDefault: false, isDefault: false,
isSelected: true, isSelected: true,
@ -104,27 +93,24 @@ describe('Notebook Entries:', () => {
pageTitle: 'Page' pageTitle: 'Page'
}; };
let openmct;
let mockIdentifierService;
describe('Notebook Entries:', () => {
beforeEach(() => {
openmct = createOpenMct(); openmct = createOpenMct();
openmct.$injector = jasmine.createSpyObj('$injector', ['get']); openmct.$injector = jasmine.createSpyObj('$injector', ['get']);
mockIdentifierService = jasmine.createSpyObj( mockIdentifierService = jasmine.createSpyObj(
'identifierService', 'identifierService',
['parse'] ['parse']
); );
openmct.$injector.get.and.callFake((key) => {
return {
'identifierService': mockIdentifierService,
'$rootScope': {
'$destroy': () => {}
}
}[key];
});
mockIdentifierService.parse.and.returnValue({ mockIdentifierService.parse.and.returnValue({
getSpace: () => { getSpace: () => {
return ''; return '';
} }
}); });
openmct.$injector.get.and.returnValue(mockIdentifierService);
openmct.types.addType('notebook', { openmct.types.addType('notebook', {
creatable: true creatable: true
}); });

View File

@ -23,16 +23,7 @@
import * as NotebookStorage from './notebook-storage'; import * as NotebookStorage from './notebook-storage';
import { createOpenMct, resetApplicationState } from 'utils/testing'; import { createOpenMct, resetApplicationState } from 'utils/testing';
let notebookSection; const notebookSection = {
let domainObject;
let notebookStorage;
let openmct;
let mockIdentifierService;
describe('Notebook Storage:', () => {
beforeEach(() => {
notebookSection = {
id: 'temp-section', id: 'temp-section',
isDefault: false, isDefault: false,
isSelected: true, isSelected: true,
@ -49,7 +40,7 @@ describe('Notebook Storage:', () => {
sectionTitle: 'Section' sectionTitle: 'Section'
}; };
domainObject = { const domainObject = {
name: 'notebook', name: 'notebook',
identifier: { identifier: {
namespace: '', namespace: '',
@ -62,7 +53,7 @@ describe('Notebook Storage:', () => {
} }
}; };
notebookStorage = { const notebookStorage = {
name: 'notebook', name: 'notebook',
identifier: { identifier: {
namespace: '', namespace: '',
@ -72,6 +63,11 @@ describe('Notebook Storage:', () => {
defaultPageId: 'temp-page' defaultPageId: 'temp-page'
}; };
let openmct;
let mockIdentifierService;
describe('Notebook Storage:', () => {
beforeEach(() => {
openmct = createOpenMct(); openmct = createOpenMct();
openmct.$injector = jasmine.createSpyObj('$injector', ['get']); openmct.$injector = jasmine.createSpyObj('$injector', ['get']);
mockIdentifierService = jasmine.createSpyObj( mockIdentifierService = jasmine.createSpyObj(
@ -84,15 +80,7 @@ describe('Notebook Storage:', () => {
} }
}); });
openmct.$injector.get.and.callFake((key) => { openmct.$injector.get.and.returnValue(mockIdentifierService);
return {
'identifierService': mockIdentifierService,
'$rootScope': {
'$destroy': () => {}
}
}[key];
});
window.localStorage.setItem('notebook-storage', null); window.localStorage.setItem('notebook-storage', null);
openmct.objects.addProvider('', jasmine.createSpyObj('mockNotebookProvider', [ openmct.objects.addProvider('', jasmine.createSpyObj('mockNotebookProvider', [
'create', 'create',

View File

@ -15,12 +15,16 @@
port.onmessage = async function (event) { port.onmessage = async function (event) {
if (event.data.request === 'close') { if (event.data.request === 'close') {
console.log('Closing connection');
connections.splice(event.data.connectionId - 1, 1); connections.splice(event.data.connectionId - 1, 1);
if (connections.length <= 0) { if (connections.length <= 0) {
// abort any outstanding requests if there's nobody listening to it. // abort any outstanding requests if there's nobody listening to it.
controller.abort(); controller.abort();
} }
console.log('Closed.');
connected = false;
return; return;
} }
@ -29,14 +33,28 @@
return; return;
} }
connected = true; do {
await self.listenForChanges(event.data.url, event.data.body, port);
} while (connected);
}
};
let url = event.data.url; port.start();
let body = event.data.body;
};
self.onerror = function () {
//do nothing
console.log('Error on feed');
};
self.listenForChanges = async function (url, body, port) {
connected = true;
let error = false; let error = false;
// feed=continuous maintains an indefinitely open connection with a keep-alive of HEARTBEAT milliseconds until this client closes the connection // feed=continuous maintains an indefinitely open connection with a keep-alive of HEARTBEAT milliseconds until this client closes the connection
// style=main_only returns only the current winning revision of the document // style=main_only returns only the current winning revision of the document
console.log('Opening changes feed connection.');
const response = await fetch(url, { const response = await fetch(url, {
method: 'POST', method: 'POST',
headers: { headers: {
@ -65,6 +83,7 @@
let chunk = new Uint8Array(value.length); let chunk = new Uint8Array(value.length);
chunk.set(value, 0); chunk.set(value, 0);
const decodedChunk = new TextDecoder("utf-8").decode(chunk).split('\n'); const decodedChunk = new TextDecoder("utf-8").decode(chunk).split('\n');
console.log('Received chunk');
if (decodedChunk.length && decodedChunk[decodedChunk.length - 1] === '') { if (decodedChunk.length && decodedChunk[decodedChunk.length - 1] === '') {
decodedChunk.forEach((doc, index) => { decodedChunk.forEach((doc, index) => {
try { try {
@ -86,21 +105,7 @@
} }
if (error) { console.log('Done reading changes feed');
port.postMessage({
error
});
}
}
};
port.start();
};
self.onerror = function () {
//do nothing
console.log('Error on feed');
}; };
}()); }());

View File

@ -29,7 +29,7 @@ const ID = "_id";
const HEARTBEAT = 50000; const HEARTBEAT = 50000;
const ALL_DOCS = "_all_docs?include_docs=true"; const ALL_DOCS = "_all_docs?include_docs=true";
export default class CouchObjectProvider { class CouchObjectProvider {
constructor(openmct, options, namespace) { constructor(openmct, options, namespace) {
options = this._normalize(options); options = this._normalize(options);
this.openmct = openmct; this.openmct = openmct;
@ -74,13 +74,6 @@ export default class CouchObjectProvider {
if (event.data.type === 'connection') { if (event.data.type === 'connection') {
this.changesFeedSharedWorkerConnectionId = event.data.connectionId; this.changesFeedSharedWorkerConnectionId = event.data.connectionId;
} else { } else {
const error = event.data.error;
if (error && Object.keys(this.observers).length > 0) {
this.observeObjectChanges();
return;
}
let objectChanges = event.data.objectChanges; let objectChanges = event.data.objectChanges;
objectChanges.identifier = { objectChanges.identifier = {
namespace: this.namespace, namespace: this.namespace,
@ -126,11 +119,12 @@ export default class CouchObjectProvider {
} }
return fetch(this.url + '/' + subPath, fetchOptions) return fetch(this.url + '/' + subPath, fetchOptions)
.then(response => response.json()) .then((response) => {
.then(function (response) { if (response.status === CouchObjectProvider.HTTP_CONFLICT) {
return response; throw new this.openmct.objects.errors.Conflict(`Conflict persisting ${fetchOptions.body.name}`);
}, function () { }
return undefined;
return response.json();
}); });
} }
@ -561,12 +555,18 @@ export default class CouchObjectProvider {
let intermediateResponse = this.getIntermediateResponse(); let intermediateResponse = this.getIntermediateResponse();
const key = model.identifier.key; const key = model.identifier.key;
this.enqueueObject(key, model, intermediateResponse); this.enqueueObject(key, model, intermediateResponse);
if (!this.objectQueue[key].pending) {
this.objectQueue[key].pending = true; this.objectQueue[key].pending = true;
const queued = this.objectQueue[key].dequeue(); const queued = this.objectQueue[key].dequeue();
let document = new CouchDocument(key, queued.model); let document = new CouchDocument(key, queued.model);
this.request(key, "PUT", document).then((response) => { this.request(key, "PUT", document).then((response) => {
console.log('create check response', key);
this.checkResponse(response, queued.intermediateResponse, key); this.checkResponse(response, queued.intermediateResponse, key);
}).catch(error => {
queued.intermediateResponse.reject(error);
this.objectQueue[key].pending = false;
}); });
}
return intermediateResponse.promise; return intermediateResponse.promise;
} }
@ -581,6 +581,9 @@ export default class CouchObjectProvider {
let document = new CouchDocument(key, queued.model, this.objectQueue[key].rev); let document = new CouchDocument(key, queued.model, this.objectQueue[key].rev);
this.request(key, "PUT", document).then((response) => { this.request(key, "PUT", document).then((response) => {
this.checkResponse(response, queued.intermediateResponse, key); this.checkResponse(response, queued.intermediateResponse, key);
}).catch((error) => {
queued.intermediateResponse.reject(error);
this.objectQueue[key].pending = false;
}); });
} }
} }
@ -594,3 +597,7 @@ export default class CouchObjectProvider {
return intermediateResponse.promise; return intermediateResponse.promise;
} }
} }
CouchObjectProvider.HTTP_CONFLICT = 409;
export default CouchObjectProvider;

View File

@ -29,8 +29,13 @@ describe('the plugin', function () {
let element; let element;
let child; let child;
let openmct; let openmct;
let appHolder;
beforeEach((done) => { beforeEach((done) => {
appHolder = document.createElement('div');
appHolder.style.width = '640px';
appHolder.style.height = '480px';
openmct = createOpenMct(); openmct = createOpenMct();
openmct.install(new PlanPlugin()); openmct.install(new PlanPlugin());
@ -45,7 +50,7 @@ describe('the plugin', function () {
element.appendChild(child); element.appendChild(child);
openmct.on('start', done); openmct.on('start', done);
openmct.start(element); openmct.start(appHolder);
}); });
afterEach(() => { afterEach(() => {
@ -94,7 +99,6 @@ describe('the plugin', function () {
} }
]; ];
let planView; let planView;
let view;
beforeEach(() => { beforeEach(() => {
openmct.time.timeSystem('utc', { openmct.time.timeSystem('utc', {
@ -135,16 +139,12 @@ describe('the plugin', function () {
const applicableViews = openmct.objectViews.get(planDomainObject, []); const applicableViews = openmct.objectViews.get(planDomainObject, []);
planView = applicableViews.find((viewProvider) => viewProvider.key === 'plan.view'); planView = applicableViews.find((viewProvider) => viewProvider.key === 'plan.view');
view = planView.view(planDomainObject, mockObjectPath); let view = planView.view(planDomainObject, mockObjectPath);
view.show(child, true); view.show(child, true);
return Vue.nextTick(); return Vue.nextTick();
}); });
afterEach(() => {
view.destroy();
});
it('loads activities into the view', () => { it('loads activities into the view', () => {
const svgEls = element.querySelectorAll('.c-plan__contents svg'); const svgEls = element.querySelectorAll('.c-plan__contents svg');
expect(svgEls.length).toEqual(1); expect(svgEls.length).toEqual(1);

View File

@ -21,13 +21,9 @@
--> -->
<template> <template>
<div class="u-contents"> <div class="u-contents">
<ul v-if="canEdit" <div v-if="canEdit"
class="l-inspector-part" class="grid-row"
> >
<h2 v-if="heading"
:title="heading"
>{{ heading }}</h2>
<li class="grid-row">
<div class="grid-cell label" <div class="grid-cell label"
:title="editTitle" :title="editTitle"
>{{ shortLabel }}</div> >{{ shortLabel }}</div>
@ -60,15 +56,10 @@
</div> </div>
</div> </div>
</div> </div>
</li> </div>
</ul> <div v-else
<ul v-else class="grid-row"
class="l-inspector-part"
> >
<h2 v-if="heading"
:title="heading"
>{{ heading }}</h2>
<li class="grid-row">
<div class="grid-cell label" <div class="grid-cell label"
:title="viewTitle" :title="viewTitle"
>{{ shortLabel }}</div> >{{ shortLabel }}</div>
@ -80,8 +71,7 @@
> >
</span> </span>
</div> </div>
</li> </div>
</ul>
</div> </div>
</template> </template>
@ -114,12 +104,6 @@ export default {
default() { default() {
return 'Color'; return 'Color';
} }
},
heading: {
type: String,
default() {
return '';
}
} }
}, },
data() { data() {

View File

@ -107,7 +107,7 @@ export default {
}; };
this.openmct.objects.mutate( this.openmct.objects.mutate(
this.domainObject, this.domainObject,
`configuration.barStyles[${this.key}]`, `configuration.barStyles[${key}]`,
this.domainObject.configuration.barStyles[key] this.domainObject.configuration.barStyles[key]
); );
} else { } else {
@ -150,6 +150,10 @@ export default {
}, },
getAxisMetadata(telemetryObject) { getAxisMetadata(telemetryObject) {
const metadata = this.openmct.telemetry.getMetadata(telemetryObject); const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
if (!metadata) {
return {};
}
const yAxisMetadata = metadata.valuesForHints(['range'])[0]; const yAxisMetadata = metadata.valuesForHints(['range'])[0];
//Exclude 'name' and 'time' based metadata specifically, from the x-Axis values by using range hints only //Exclude 'name' and 'time' based metadata specifically, from the x-Axis values by using range hints only
const xAxisMetadata = metadata.valuesForHints(['range']); const xAxisMetadata = metadata.valuesForHints(['range']);
@ -255,6 +259,9 @@ export default {
data.forEach((datum) => { data.forEach((datum) => {
this.processData(telemetryObject, datum, axisMetadata); this.processData(telemetryObject, datum, axisMetadata);
}); });
})
.catch((error) => {
console.warn(`Error fetching data`, error);
}); });
}, },
subscribeToObject(telemetryObject) { subscribeToObject(telemetryObject) {

View File

@ -33,9 +33,9 @@
</li> </li>
<ColorSwatch v-if="expanded" <ColorSwatch v-if="expanded"
:current-color="currentColor" :current-color="currentColor"
title="Manually set the color for this bar graph." title="Manually set the color for this bar graph series."
edit-title="Manually set the color for this bar graph" edit-title="Manually set the color for this bar graph series"
view-title="The color for this bar graph." view-title="The color for this bar graph series."
short-label="Color" short-label="Color"
class="grid-properties" class="grid-properties"
@colorSet="setColor" @colorSet="setColor"

View File

@ -20,15 +20,13 @@
at runtime from the About dialog for additional information. at runtime from the About dialog for additional information.
--> -->
<template> <template>
<div>
<ul class="c-tree"> <ul class="c-tree">
<li v-for="series in domainObject.composition" <h2 title="Display properties for this object">Bar Graph Series</h2>
<bar-graph-options v-for="series in domainObject.composition"
:key="series.key" :key="series.key"
> :item="series"
<bar-graph-options :item="series" /> />
</li>
</ul> </ul>
</div>
</template> </template>
<script> <script>

View File

@ -82,12 +82,17 @@ export default class PlotSeries extends Model {
.openmct .openmct
.telemetry .telemetry
.getMetadata(options.domainObject); .getMetadata(options.domainObject);
this.formats = options this.formats = options
.openmct .openmct
.telemetry .telemetry
.getFormatMap(this.metadata); .getFormatMap(this.metadata);
const range = this.metadata.valuesForHints(['range'])[0]; //if the object is missing or doesn't have metadata for some reason
let range = {};
if (this.metadata) {
range = this.metadata.valuesForHints(['range'])[0];
}
return { return {
name: options.domainObject.name, name: options.domainObject.name,
@ -191,7 +196,10 @@ export default class PlotSeries extends Model {
.uniq(true, point => [this.getXVal(point), this.getYVal(point)].join()) .uniq(true, point => [this.getXVal(point), this.getYVal(point)].join())
.value(); .value();
this.reset(newPoints); this.reset(newPoints);
}.bind(this)); }.bind(this))
.catch((error) => {
console.warn('Error fetching data', error);
});
/* eslint-enable you-dont-need-lodash-underscore/concat */ /* eslint-enable you-dont-need-lodash-underscore/concat */
} }
/** /**
@ -199,8 +207,10 @@ export default class PlotSeries extends Model {
*/ */
onXKeyChange(xKey) { onXKeyChange(xKey) {
const format = this.formats[xKey]; const format = this.formats[xKey];
if (format) {
this.getXVal = format.parse.bind(format); this.getXVal = format.parse.bind(format);
} }
}
/** /**
* Update y formatter on change, default to stepAfter interpolation if * Update y formatter on change, default to stepAfter interpolation if
* y range is an enumeration. * y range is an enumeration.

View File

@ -184,7 +184,7 @@ export default class YAxisModel extends Model {
this.set('values', yMetadata.values); this.set('values', yMetadata.values);
if (!label) { if (!label) {
const labelName = series.map(function (s) { const labelName = series.map(function (s) {
return s.metadata.value(s.get('yKey')).name; return s.metadata ? s.metadata.value(s.get('yKey')).name : '';
}).reduce(function (a, b) { }).reduce(function (a, b) {
if (a === undefined) { if (a === undefined) {
return b; return b;
@ -204,7 +204,7 @@ export default class YAxisModel extends Model {
} }
const labelUnits = series.map(function (s) { const labelUnits = series.map(function (s) {
return s.metadata.value(s.get('yKey')).units; return s.metadata ? s.metadata.value(s.get('yKey')).units : '';
}).reduce(function (a, b) { }).reduce(function (a, b) {
if (a === undefined) { if (a === undefined) {
return b; return b;

View File

@ -60,18 +60,17 @@ define([
this.addTelemetryObject = this.addTelemetryObject.bind(this); this.addTelemetryObject = this.addTelemetryObject.bind(this);
this.removeTelemetryObject = this.removeTelemetryObject.bind(this); this.removeTelemetryObject = this.removeTelemetryObject.bind(this);
this.removeTelemetryCollection = this.removeTelemetryCollection.bind(this); this.removeTelemetryCollection = this.removeTelemetryCollection.bind(this);
this.incrementOutstandingRequests = this.incrementOutstandingRequests.bind(this);
this.decrementOutstandingRequests = this.decrementOutstandingRequests.bind(this);
this.resetRowsFromAllData = this.resetRowsFromAllData.bind(this); this.resetRowsFromAllData = this.resetRowsFromAllData.bind(this);
this.isTelemetryObject = this.isTelemetryObject.bind(this); this.isTelemetryObject = this.isTelemetryObject.bind(this);
this.refreshData = this.refreshData.bind(this);
this.updateFilters = this.updateFilters.bind(this); this.updateFilters = this.updateFilters.bind(this);
this.clearData = this.clearData.bind(this);
this.buildOptionsFromConfiguration = this.buildOptionsFromConfiguration.bind(this); this.buildOptionsFromConfiguration = this.buildOptionsFromConfiguration.bind(this);
this.filterObserver = undefined; this.filterObserver = undefined;
this.createTableRowCollections(); this.createTableRowCollections();
openmct.time.on('bounds', this.refreshData);
openmct.time.on('timeSystem', this.refreshData);
} }
/** /**
@ -141,8 +140,6 @@ define([
let columnMap = this.getColumnMapForObject(keyString); let columnMap = this.getColumnMapForObject(keyString);
let limitEvaluator = this.openmct.telemetry.limitEvaluator(telemetryObject); let limitEvaluator = this.openmct.telemetry.limitEvaluator(telemetryObject);
this.incrementOutstandingRequests();
const telemetryProcessor = this.getTelemetryProcessor(keyString, columnMap, limitEvaluator); const telemetryProcessor = this.getTelemetryProcessor(keyString, columnMap, limitEvaluator);
const telemetryRemover = this.getTelemetryRemover(); const telemetryRemover = this.getTelemetryRemover();
@ -151,13 +148,13 @@ define([
this.telemetryCollections[keyString] = this.openmct.telemetry this.telemetryCollections[keyString] = this.openmct.telemetry
.requestCollection(telemetryObject, requestOptions); .requestCollection(telemetryObject, requestOptions);
this.telemetryCollections[keyString].on('requestStarted', this.incrementOutstandingRequests);
this.telemetryCollections[keyString].on('requestEnded', this.decrementOutstandingRequests);
this.telemetryCollections[keyString].on('remove', telemetryRemover); this.telemetryCollections[keyString].on('remove', telemetryRemover);
this.telemetryCollections[keyString].on('add', telemetryProcessor); this.telemetryCollections[keyString].on('add', telemetryProcessor);
this.telemetryCollections[keyString].on('clear', this.tableRows.clear); this.telemetryCollections[keyString].on('clear', this.clearData);
this.telemetryCollections[keyString].load(); this.telemetryCollections[keyString].load();
this.decrementOutstandingRequests();
this.telemetryObjects[keyString] = { this.telemetryObjects[keyString] = {
telemetryObject, telemetryObject,
keyString, keyString,
@ -268,17 +265,6 @@ define([
this.emit('object-removed', objectIdentifier); this.emit('object-removed', objectIdentifier);
} }
refreshData(bounds, isTick) {
if (!isTick && this.tableRows.outstandingRequests === 0) {
this.tableRows.clear();
this.tableRows.sortBy({
key: this.openmct.time.timeSystem().key,
direction: 'asc'
});
this.tableRows.resubscribe();
}
}
clearData() { clearData() {
this.tableRows.clear(); this.tableRows.clear();
this.emit('refresh'); this.emit('refresh');
@ -378,9 +364,6 @@ define([
let keystrings = Object.keys(this.telemetryCollections); let keystrings = Object.keys(this.telemetryCollections);
keystrings.forEach(this.removeTelemetryCollection); keystrings.forEach(this.removeTelemetryCollection);
this.openmct.time.off('bounds', this.refreshData);
this.openmct.time.off('timeSystem', this.refreshData);
if (this.filterObserver) { if (this.filterObserver) {
this.filterObserver(); this.filterObserver();
} }

View File

@ -131,7 +131,8 @@ export default {
objects.forEach(object => this.addColumnsForObject(object, false)); objects.forEach(object => this.addColumnsForObject(object, false));
}, },
addColumnsForObject(telemetryObject) { addColumnsForObject(telemetryObject) {
let metadataValues = this.openmct.telemetry.getMetadata(telemetryObject).values(); const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
let metadataValues = metadata ? metadata.values() : [];
metadataValues.forEach(metadatum => { metadataValues.forEach(metadatum => {
let column = new TelemetryTableColumn(this.openmct, metadatum); let column = new TelemetryTableColumn(this.openmct, metadatum);
this.tableConfiguration.addSingleColumnForObject(telemetryObject, column); this.tableConfiguration.addSingleColumnForObject(telemetryObject, column);

View File

@ -105,7 +105,8 @@ export default {
composition.load().then((domainObjects) => { composition.load().then((domainObjects) => {
domainObjects.forEach(telemetryObject => { domainObjects.forEach(telemetryObject => {
let keyString = this.openmct.objects.makeKeyString(telemetryObject.identifier); let keyString = this.openmct.objects.makeKeyString(telemetryObject.identifier);
let metadataValues = this.openmct.telemetry.getMetadata(telemetryObject).values(); const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
let metadataValues = metadata ? metadata.values() : [];
let filters = this.filteredTelemetry[keyString]; let filters = this.filteredTelemetry[keyString];
if (filters !== undefined) { if (filters !== undefined) {

View File

@ -125,7 +125,6 @@
<div <div
class="c-table c-telemetry-table c-table--filterable c-table--sortable has-control-bar u-style-receiver js-style-receiver" class="c-table c-telemetry-table c-table--filterable c-table--sortable has-control-bar u-style-receiver js-style-receiver"
:class="{ :class="{
'loading': loading,
'is-paused' : paused 'is-paused' : paused
}" }"
> >
@ -362,7 +361,7 @@ export default {
autoScroll: true, autoScroll: true,
sortOptions: {}, sortOptions: {},
filters: {}, filters: {},
loading: true, loading: false,
scrollable: undefined, scrollable: undefined,
tableEl: undefined, tableEl: undefined,
headersHolderEl: undefined, headersHolderEl: undefined,
@ -422,6 +421,14 @@ export default {
} }
}, },
watch: { watch: {
loading: {
handler(isLoading) {
if (this.viewActionsCollection) {
let action = isLoading ? 'disable' : 'enable';
this.viewActionsCollection[action](['export-csv-all']);
}
}
},
markedRows: { markedRows: {
handler(newVal, oldVal) { handler(newVal, oldVal) {
this.$emit('marked-rows-updated', newVal, oldVal); this.$emit('marked-rows-updated', newVal, oldVal);
@ -1020,6 +1027,12 @@ export default {
this.viewActionsCollection.disable(['export-csv-marked', 'unmark-all-rows']); this.viewActionsCollection.disable(['export-csv-marked', 'unmark-all-rows']);
} }
if (this.loading) {
this.viewActionsCollection.disable(['export-csv-all']);
} else {
this.viewActionsCollection.enable(['export-csv-all']);
}
if (this.paused) { if (this.paused) {
this.viewActionsCollection.hide(['pause-data']); this.viewActionsCollection.hide(['pause-data']);
this.viewActionsCollection.show(['play-data']); this.viewActionsCollection.show(['play-data']);

View File

@ -98,10 +98,7 @@ export default {
//Respond to changes in conductor //Respond to changes in conductor
this.openmct.time.on("timeSystem", this.setViewFromTimeSystem); this.openmct.time.on("timeSystem", this.setViewFromTimeSystem);
this.resizeTimer = setInterval(this.resize, RESIZE_POLL_INTERVAL); setInterval(this.resize, RESIZE_POLL_INTERVAL);
},
destroyed() {
clearInterval(this.resizeTimer);
}, },
methods: { methods: {
setAxisDimensions() { setAxisDimensions() {

View File

@ -151,29 +151,22 @@ export default {
this.stopFollowingTimeContext(); this.stopFollowingTimeContext();
this.timeContext = this.openmct.time.getContextForView([this.domainObject]); this.timeContext = this.openmct.time.getContextForView([this.domainObject]);
this.timeContext.on('timeContext', this.setTimeContext); this.timeContext.on('timeContext', this.setTimeContext);
this.timeContext.on('clock', this.setViewFromClock); this.timeContext.on('clock', this.setTimeOptions);
}, },
stopFollowingTimeContext() { stopFollowingTimeContext() {
if (this.timeContext) { if (this.timeContext) {
this.timeContext.off('timeContext', this.setTimeContext); this.timeContext.off('timeContext', this.setTimeContext);
this.timeContext.off('clock', this.setViewFromClock); this.timeContext.off('clock', this.setTimeOptions);
} }
}, },
setViewFromClock(clock) { setTimeOptions(clock) {
if (!this.timeOptions.mode) { this.timeOptions.clockOffsets = this.timeOptions.clockOffsets || this.timeContext.clockOffsets();
this.setTimeOptions(clock); this.timeOptions.fixedOffsets = this.timeOptions.fixedOffsets || this.timeContext.bounds();
}
},
setTimeOptions() {
if (!this.timeOptions || !this.timeOptions.mode) {
this.mode = this.timeContext.clock() === undefined ? { key: 'fixed' } : { key: Object.create(this.timeContext.clock()).key};
this.timeOptions = {
clockOffsets: this.timeContext.clockOffsets(),
fixedOffsets: this.timeContext.bounds()
};
}
if (!this.timeOptions.mode) {
this.mode = this.timeContext.clock() === undefined ? {key: 'fixed'} : {key: Object.create(this.timeContext.clock()).key};
this.registerIndependentTimeOffsets(); this.registerIndependentTimeOffsets();
}
}, },
saveFixedOffsets(offsets) { saveFixedOffsets(offsets) {
const newOptions = Object.assign({}, this.timeOptions, { const newOptions = Object.assign({}, this.timeOptions, {

View File

@ -20,7 +20,8 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
<template> <template>
<div ref="modeMenuButton" <div v-if="modes.length > 1"
ref="modeMenuButton"
class="c-ctrl-wrapper c-ctrl-wrapper--menus-up" class="c-ctrl-wrapper c-ctrl-wrapper--menus-up"
> >
<div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left"> <div class="c-menu-button c-ctrl-wrapper c-ctrl-wrapper--menus-left">

View File

@ -88,7 +88,7 @@ export default {
this.mutablePromise.then(() => { this.mutablePromise.then(() => {
this.openmct.objects.destroyMutable(this.domainObject); this.openmct.objects.destroyMutable(this.domainObject);
}); });
} else { } else if (this.domainObject.isMutable) {
this.openmct.objects.destroyMutable(this.domainObject); this.openmct.objects.destroyMutable(this.domainObject);
} }
}, },

View File

@ -38,10 +38,6 @@ define(
this.openmct = openmct; this.openmct = openmct;
this.selected = []; this.selected = [];
this.openmct.once('destroy', () => {
this.removeAllListeners();
});
} }
Selection.prototype = Object.create(EventEmitter.prototype); Selection.prototype = Object.create(EventEmitter.prototype);

View File

@ -160,7 +160,9 @@ export default {
this.status = this.openmct.status.get(this.domainObject.identifier); this.status = this.openmct.status.get(this.domainObject.identifier);
this.removeStatusListener = this.openmct.status.observe(this.domainObject.identifier, this.setStatus); this.removeStatusListener = this.openmct.status.observe(this.domainObject.identifier, this.setStatus);
const provider = this.openmct.objectViews.get(this.domainObject, this.objectPath)[0]; const provider = this.openmct.objectViews.get(this.domainObject, this.objectPath)[0];
if (provider) {
this.$refs.objectView.show(this.domainObject, provider.key, false, this.objectPath); this.$refs.objectView.show(this.domainObject, provider.key, false, this.objectPath);
}
}, },
beforeDestroy() { beforeDestroy() {
this.removeStatusListener(); this.removeStatusListener();
@ -193,8 +195,10 @@ export default {
}, },
showMenuItems(event) { showMenuItems(event) {
const sortedActions = this.openmct.actions._groupAndSortActions(this.menuActionItems); const sortedActions = this.openmct.actions._groupAndSortActions(this.menuActionItems);
if (sortedActions.length) {
const menuItems = this.openmct.menus.actionsToMenuItems(sortedActions, this.actionCollection.objectPath, this.actionCollection.view); const menuItems = this.openmct.menus.actionsToMenuItems(sortedActions, this.actionCollection.objectPath, this.actionCollection.view);
this.openmct.menus.showMenu(event.x, event.y, menuItems); this.openmct.menus.showMenu(event.x, event.y, menuItems);
}
}, },
setStatus(status) { setStatus(status) {
this.status = status; this.status = status;

View File

@ -49,14 +49,12 @@ describe("the inspector", () => {
beforeEach((done) => { beforeEach((done) => {
openmct = createOpenMct(); openmct = createOpenMct();
spyOn(openmct.objects, 'save').and.returnValue(Promise.resolve(true));
openmct.on('start', done); openmct.on('start', done);
openmct.startHeadless(); openmct.startHeadless();
}); });
afterEach(() => { afterEach(() => {
stylesViewComponent.$destroy();
savedStylesViewComponent.$destroy();
return resetApplicationState(openmct); return resetApplicationState(openmct);
}); });
@ -85,7 +83,7 @@ describe("the inspector", () => {
}); });
}); });
it("should allow a saved style to be applied", () => { xit("should allow a saved style to be applied", () => {
spyOn(openmct.editor, 'isEditing').and.returnValue(true); spyOn(openmct.editor, 'isEditing').and.returnValue(true);
selection = mockTelemetryTableSelection; selection = mockTelemetryTableSelection;
@ -99,7 +97,7 @@ describe("the inspector", () => {
styleSelectorComponent.selectStyle(); styleSelectorComponent.selectStyle();
savedStylesViewComponent.$nextTick().then(() => { return savedStylesViewComponent.$nextTick().then(() => {
const styleEditorComponentIndex = stylesViewComponent.$children[0].$children.length - 1; const styleEditorComponentIndex = stylesViewComponent.$children[0].$children.length - 1;
const styleEditorComponent = stylesViewComponent.$children[0].$children[styleEditorComponentIndex]; const styleEditorComponent = stylesViewComponent.$children[0].$children[styleEditorComponentIndex];
const styles = styleEditorComponent.$children.filter(component => component.options.value === mockStyle.color); const styles = styleEditorComponent.$children.filter(component => component.options.value === mockStyle.color);

View File

@ -134,7 +134,7 @@ export default {
actionCollection: { actionCollection: {
type: Object, type: Object,
default: () => { default: () => {
return undefined; return {};
} }
} }
}, },
@ -233,7 +233,6 @@ export default {
}, },
mounted: function () { mounted: function () {
document.addEventListener('click', this.closeViewAndSaveMenu); document.addEventListener('click', this.closeViewAndSaveMenu);
this.promptUserbeforeNavigatingAway = this.promptUserbeforeNavigatingAway.bind(this); this.promptUserbeforeNavigatingAway = this.promptUserbeforeNavigatingAway.bind(this);
window.addEventListener('beforeunload', this.promptUserbeforeNavigatingAway); window.addEventListener('beforeunload', this.promptUserbeforeNavigatingAway);

View File

@ -49,10 +49,6 @@ class ApplicationRouter extends EventEmitter {
this.routes = []; this.routes = [];
this.started = false; this.started = false;
openmct.once('destroy', () => {
this.destroy();
});
this.setHash = _.debounce(this.setHash.bind(this), 300); this.setHash = _.debounce(this.setHash.bind(this), 300);
} }