Compare commits
17 Commits
condition-
...
fix-view-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
085f6d3352 | ||
|
|
905e397d3f | ||
|
|
1891551430 | ||
|
|
75c4f0171f | ||
|
|
e70a636073 | ||
|
|
03abb5e5de | ||
|
|
ac20c01233 | ||
|
|
b8ded0a16e | ||
|
|
b68f79f427 | ||
|
|
221d10d3e6 | ||
|
|
22d32eed1d | ||
|
|
5d656f0963 | ||
|
|
201d622b85 | ||
|
|
3571004f5c | ||
|
|
16249c3790 | ||
|
|
5377f0d0b3 | ||
|
|
15778b00a0 |
@@ -71,10 +71,10 @@ define(
|
||||
openmct.editor.cancel();
|
||||
}
|
||||
|
||||
function isFirstViewEditable(domainObject) {
|
||||
let firstView = openmct.objectViews.get(domainObject)[0];
|
||||
function isFirstViewEditable(domainObject, objectPath) {
|
||||
let firstView = openmct.objectViews.get(domainObject, objectPath)[0];
|
||||
|
||||
return firstView && firstView.canEdit && firstView.canEdit(domainObject);
|
||||
return firstView && firstView.canEdit && firstView.canEdit(domainObject, objectPath);
|
||||
}
|
||||
|
||||
function navigateAndEdit(object) {
|
||||
@@ -88,7 +88,7 @@ define(
|
||||
|
||||
window.location.href = url;
|
||||
|
||||
if (isFirstViewEditable(object.useCapability('adapter'))) {
|
||||
if (isFirstViewEditable(object.useCapability('adapter'), objectPath)) {
|
||||
openmct.editor.edit();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ define(
|
||||
this.$q = $q;
|
||||
}
|
||||
|
||||
LocatingObjectDecorator.prototype.getObjects = function (ids) {
|
||||
LocatingObjectDecorator.prototype.getObjects = function (ids, abortSignal) {
|
||||
var $q = this.$q,
|
||||
$log = this.$log,
|
||||
objectService = this.objectService,
|
||||
@@ -79,7 +79,7 @@ define(
|
||||
});
|
||||
}
|
||||
|
||||
return objectService.getObjects([id]).then(attachContext);
|
||||
return objectService.getObjects([id], abortSignal).then(attachContext);
|
||||
}
|
||||
|
||||
ids.forEach(function (id) {
|
||||
|
||||
@@ -80,12 +80,15 @@ define([
|
||||
* @param {Function} [filter] if provided, will be called for every
|
||||
* potential modelResult. If it returns false, the model result will be
|
||||
* excluded from the search results.
|
||||
* @param {AbortController.signal} abortSignal (optional) can pass in an abortSignal to cancel any
|
||||
* downstream fetch requests.
|
||||
* @returns {Promise} A Promise for a search result object.
|
||||
*/
|
||||
SearchAggregator.prototype.query = function (
|
||||
inputText,
|
||||
maxResults,
|
||||
filter
|
||||
filter,
|
||||
abortSignal
|
||||
) {
|
||||
|
||||
var aggregator = this,
|
||||
@@ -120,7 +123,7 @@ define([
|
||||
modelResults = aggregator.applyFilter(modelResults, filter);
|
||||
modelResults = aggregator.removeDuplicates(modelResults);
|
||||
|
||||
return aggregator.asObjectResults(modelResults);
|
||||
return aggregator.asObjectResults(modelResults, abortSignal);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -193,16 +196,19 @@ define([
|
||||
* Convert modelResults to objectResults by fetching them from the object
|
||||
* service.
|
||||
*
|
||||
* @param {Object} modelResults an object containing the results from the search
|
||||
* @param {AbortController.signal} abortSignal (optional) abort signal to cancel any
|
||||
* downstream fetch requests
|
||||
* @returns {Promise} for an objectResults object.
|
||||
*/
|
||||
SearchAggregator.prototype.asObjectResults = function (modelResults) {
|
||||
SearchAggregator.prototype.asObjectResults = function (modelResults, abortSignal) {
|
||||
var objectIds = modelResults.hits.map(function (modelResult) {
|
||||
return modelResult.id;
|
||||
});
|
||||
|
||||
return this
|
||||
.objectService
|
||||
.getObjects(objectIds)
|
||||
.getObjects(objectIds, abortSignal)
|
||||
.then(function (objects) {
|
||||
|
||||
var objectResults = {
|
||||
|
||||
@@ -32,7 +32,7 @@ define([], function () {
|
||||
if (Object.prototype.hasOwnProperty.call(view, 'provider')) {
|
||||
const domainObject = legacyObject.useCapability('adapter');
|
||||
|
||||
return view.provider.canView(domainObject);
|
||||
return view.provider.canView(domainObject, this.openmct.router.path);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -139,10 +139,12 @@ define([
|
||||
});
|
||||
};
|
||||
|
||||
ObjectServiceProvider.prototype.superSecretFallbackSearch = function (query, options) {
|
||||
ObjectServiceProvider.prototype.superSecretFallbackSearch = function (query, abortSignal) {
|
||||
const searchService = this.$injector.get('searchService');
|
||||
|
||||
return searchService.query(query);
|
||||
// need to pass the abortSignal down, so need to
|
||||
// pass in undefined for maxResults and filter on query
|
||||
return searchService.query(query, undefined, undefined, abortSignal);
|
||||
};
|
||||
|
||||
// Injects new object API as a decorator so that it hijacks all requests.
|
||||
@@ -150,13 +152,13 @@ define([
|
||||
function LegacyObjectAPIInterceptor(openmct, ROOTS, instantiate, topic, objectService) {
|
||||
const eventEmitter = openmct.objects.eventEmitter;
|
||||
|
||||
this.getObjects = function (keys) {
|
||||
this.getObjects = function (keys, abortSignal) {
|
||||
const results = {};
|
||||
|
||||
const promises = keys.map(function (keyString) {
|
||||
const key = utils.parseKeyString(keyString);
|
||||
|
||||
return openmct.objects.get(key)
|
||||
return openmct.objects.get(key, abortSignal)
|
||||
.then(function (object) {
|
||||
object = utils.toOldFormat(object);
|
||||
results[keyString] = instantiate(object, keyString);
|
||||
|
||||
@@ -29,9 +29,22 @@ describe('The ActionCollection', () => {
|
||||
let mockApplicableActions;
|
||||
let mockObjectPath;
|
||||
let mockView;
|
||||
let mockIdentifierService;
|
||||
|
||||
beforeEach(() => {
|
||||
openmct = createOpenMct();
|
||||
openmct.$injector = jasmine.createSpyObj('$injector', ['get']);
|
||||
mockIdentifierService = jasmine.createSpyObj(
|
||||
'identifierService',
|
||||
['parse']
|
||||
);
|
||||
mockIdentifierService.parse.and.returnValue({
|
||||
getSpace: () => {
|
||||
return '';
|
||||
}
|
||||
});
|
||||
|
||||
openmct.$injector.get.and.returnValue(mockIdentifierService);
|
||||
mockObjectPath = [
|
||||
{
|
||||
name: 'mock folder',
|
||||
|
||||
@@ -154,11 +154,12 @@ ObjectAPI.prototype.addProvider = function (namespace, provider) {
|
||||
* @method get
|
||||
* @memberof module:openmct.ObjectProvider#
|
||||
* @param {string} key the key for the domain object to load
|
||||
* @param {AbortController.signal} abortSignal (optional) signal to abort fetch requests
|
||||
* @returns {Promise} a promise which will resolve when the domain object
|
||||
* has been saved, or be rejected if it cannot be saved
|
||||
*/
|
||||
|
||||
ObjectAPI.prototype.get = function (identifier) {
|
||||
ObjectAPI.prototype.get = function (identifier, abortSignal) {
|
||||
let keystring = this.makeKeyString(identifier);
|
||||
if (this.cache[keystring] !== undefined) {
|
||||
return this.cache[keystring];
|
||||
@@ -175,15 +176,12 @@ ObjectAPI.prototype.get = function (identifier) {
|
||||
throw new Error('Provider does not support get!');
|
||||
}
|
||||
|
||||
let objectPromise = provider.get(identifier);
|
||||
let objectPromise = provider.get(identifier, abortSignal);
|
||||
this.cache[keystring] = objectPromise;
|
||||
|
||||
return objectPromise.then(result => {
|
||||
delete this.cache[keystring];
|
||||
const interceptors = this.listGetInterceptors(identifier, result);
|
||||
interceptors.forEach(interceptor => {
|
||||
result = interceptor.invoke(identifier, result);
|
||||
});
|
||||
result = this.applyGetInterceptors(identifier, result);
|
||||
|
||||
return result;
|
||||
});
|
||||
@@ -200,19 +198,24 @@ ObjectAPI.prototype.get = function (identifier) {
|
||||
* @method search
|
||||
* @memberof module:openmct.ObjectAPI#
|
||||
* @param {string} query the term to search for
|
||||
* @param {Object} options search options
|
||||
* @param {AbortController.signal} abortSignal (optional) signal to cancel downstream fetch requests
|
||||
* @returns {Array.<Promise.<module:openmct.DomainObject>>}
|
||||
* an array of promises returned from each object provider's search function
|
||||
* each resolving to domain objects matching provided search query and options.
|
||||
*/
|
||||
ObjectAPI.prototype.search = function (query, options) {
|
||||
ObjectAPI.prototype.search = function (query, abortSignal) {
|
||||
const searchPromises = Object.values(this.providers)
|
||||
.filter(provider => provider.search !== undefined)
|
||||
.map(provider => provider.search(query, options));
|
||||
.map(provider => provider.search(query, abortSignal));
|
||||
|
||||
searchPromises.push(this.fallbackProvider.superSecretFallbackSearch(query, options)
|
||||
searchPromises.push(this.fallbackProvider.superSecretFallbackSearch(query, abortSignal)
|
||||
.then(results => results.hits
|
||||
.map(hit => utils.toNewFormat(hit.object.getModel(), hit.object.getId()))));
|
||||
.map(hit => {
|
||||
let domainObject = utils.toNewFormat(hit.object.getModel(), hit.object.getId());
|
||||
domainObject = this.applyGetInterceptors(domainObject.identifier, domainObject);
|
||||
|
||||
return domainObject;
|
||||
})));
|
||||
|
||||
return searchPromises;
|
||||
};
|
||||
@@ -228,29 +231,13 @@ ObjectAPI.prototype.search = function (query, options) {
|
||||
* @returns {Promise.<MutableDomainObject>} a promise that will resolve with a MutableDomainObject if
|
||||
* the object can be mutated.
|
||||
*/
|
||||
ObjectAPI.prototype.getMutable = function (idOrKeyString) {
|
||||
if (!this.supportsMutation(idOrKeyString)) {
|
||||
throw new Error(`Object "${this.makeKeyString(idOrKeyString)}" does not support mutation.`);
|
||||
ObjectAPI.prototype.getMutable = function (identifier) {
|
||||
if (!this.supportsMutation(identifier)) {
|
||||
throw new Error(`Object "${this.makeKeyString(identifier)}" does not support mutation.`);
|
||||
}
|
||||
|
||||
return this.get(idOrKeyString).then((object) => {
|
||||
const mutableDomainObject = this._toMutable(object);
|
||||
|
||||
// Check if provider supports realtime updates
|
||||
let identifier = utils.parseKeyString(idOrKeyString);
|
||||
let provider = this.getProvider(identifier);
|
||||
|
||||
if (provider !== undefined
|
||||
&& provider.observe !== undefined) {
|
||||
let unobserve = provider.observe(identifier, (updatedModel) => {
|
||||
mutableDomainObject.$refresh(updatedModel);
|
||||
});
|
||||
mutableDomainObject.$on('$destroy', () => {
|
||||
unobserve();
|
||||
});
|
||||
}
|
||||
|
||||
return mutableDomainObject;
|
||||
return this.get(identifier).then((object) => {
|
||||
return this._toMutable(object);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -354,6 +341,19 @@ ObjectAPI.prototype.listGetInterceptors = function (identifier, object) {
|
||||
return this.interceptorRegistry.getInterceptors(identifier, object);
|
||||
};
|
||||
|
||||
/**
|
||||
* Inovke interceptors if applicable for a given domain object.
|
||||
* @private
|
||||
*/
|
||||
ObjectAPI.prototype.applyGetInterceptors = function (identifier, domainObject) {
|
||||
const interceptors = this.listGetInterceptors(identifier, domainObject);
|
||||
interceptors.forEach(interceptor => {
|
||||
domainObject = interceptor.invoke(identifier, domainObject);
|
||||
});
|
||||
|
||||
return domainObject;
|
||||
};
|
||||
|
||||
/**
|
||||
* Modify a domain object.
|
||||
* @param {module:openmct.DomainObject} object the object to mutate
|
||||
@@ -389,11 +389,29 @@ ObjectAPI.prototype.mutate = function (domainObject, path, value) {
|
||||
* @private
|
||||
*/
|
||||
ObjectAPI.prototype._toMutable = function (object) {
|
||||
let mutableObject;
|
||||
|
||||
if (object.isMutable) {
|
||||
return object;
|
||||
mutableObject = object;
|
||||
} else {
|
||||
return MutableDomainObject.createMutable(object, this.eventEmitter);
|
||||
mutableObject = MutableDomainObject.createMutable(object, this.eventEmitter);
|
||||
}
|
||||
|
||||
// Check if provider supports realtime updates
|
||||
let identifier = utils.parseKeyString(mutableObject.identifier);
|
||||
let provider = this.getProvider(identifier);
|
||||
|
||||
if (provider !== undefined
|
||||
&& provider.observe !== undefined) {
|
||||
let unobserve = provider.observe(identifier, (updatedModel) => {
|
||||
mutableObject.$refresh(updatedModel);
|
||||
});
|
||||
mutableObject.$on('$destroy', () => {
|
||||
unobserve();
|
||||
});
|
||||
}
|
||||
|
||||
return mutableObject;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export default function (couchPlugin, searchFilter) {
|
||||
export default function (folderName, couchPlugin, searchFilter) {
|
||||
return function install(openmct) {
|
||||
const couchProvider = couchPlugin.couchProvider;
|
||||
|
||||
@@ -15,7 +15,7 @@ export default function (couchPlugin, searchFilter) {
|
||||
return Promise.resolve({
|
||||
identifier,
|
||||
type: 'folder',
|
||||
name: "CouchDB Documents"
|
||||
name: folderName || "CouchDB Documents"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ describe('the plugin', function () {
|
||||
let couchPlugin = openmct.plugins.CouchDB(testPath);
|
||||
openmct.install(couchPlugin);
|
||||
|
||||
openmct.install(new CouchDBSearchFolderPlugin(couchPlugin, {
|
||||
openmct.install(new CouchDBSearchFolderPlugin('CouchDB Documents', couchPlugin, {
|
||||
"selector": {
|
||||
"model": {
|
||||
"type": "plan"
|
||||
|
||||
@@ -98,7 +98,7 @@ describe("The LAD Table", () => {
|
||||
});
|
||||
|
||||
it("should provide a table view only for lad table objects", () => {
|
||||
let applicableViews = openmct.objectViews.get(mockObj.ladTable);
|
||||
let applicableViews = openmct.objectViews.get(mockObj.ladTable, []);
|
||||
|
||||
let ladTableView = applicableViews.find(
|
||||
(viewProvider) => viewProvider.key === ladTableKey
|
||||
@@ -185,7 +185,7 @@ describe("The LAD Table", () => {
|
||||
end: bounds.end
|
||||
});
|
||||
|
||||
applicableViews = openmct.objectViews.get(mockObj.ladTable);
|
||||
applicableViews = openmct.objectViews.get(mockObj.ladTable, []);
|
||||
ladTableViewProvider = applicableViews.find((viewProvider) => viewProvider.key === ladTableKey);
|
||||
ladTableView = ladTableViewProvider.view(mockObj.ladTable, [mockObj.ladTable]);
|
||||
ladTableView.show(child, true);
|
||||
@@ -296,7 +296,7 @@ describe("The LAD Table Set", () => {
|
||||
});
|
||||
|
||||
it("should provide a lad table set view only for lad table set objects", () => {
|
||||
let applicableViews = openmct.objectViews.get(mockObj.ladTableSet);
|
||||
let applicableViews = openmct.objectViews.get(mockObj.ladTableSet, []);
|
||||
|
||||
let ladTableSetView = applicableViews.find(
|
||||
(viewProvider) => viewProvider.key === ladTableSetKey
|
||||
@@ -391,7 +391,7 @@ describe("The LAD Table Set", () => {
|
||||
end: bounds.end
|
||||
});
|
||||
|
||||
applicableViews = openmct.objectViews.get(mockObj.ladTableSet);
|
||||
applicableViews = openmct.objectViews.get(mockObj.ladTableSet, []);
|
||||
ladTableSetViewProvider = applicableViews.find((viewProvider) => viewProvider.key === ladTableSetKey);
|
||||
ladTableSetView = ladTableSetViewProvider.view(mockObj.ladTableSet, [mockObj.ladTableSet]);
|
||||
ladTableSetView.show(child, true);
|
||||
|
||||
@@ -67,11 +67,11 @@ describe("AutoflowTabularPlugin", () => {
|
||||
});
|
||||
|
||||
it("applies its view to the type from options", () => {
|
||||
expect(provider.canView(testObject)).toBe(true);
|
||||
expect(provider.canView(testObject, [])).toBe(true);
|
||||
});
|
||||
|
||||
it("does not apply to other types", () => {
|
||||
expect(provider.canView({ type: 'foo' })).toBe(false);
|
||||
expect(provider.canView({ type: 'foo' }, [])).toBe(false);
|
||||
});
|
||||
|
||||
describe("provides a view which", () => {
|
||||
|
||||
@@ -136,7 +136,7 @@ describe('the plugin', function () {
|
||||
}
|
||||
};
|
||||
|
||||
const applicableViews = openmct.objectViews.get(testViewObject);
|
||||
const applicableViews = openmct.objectViews.get(testViewObject, []);
|
||||
let conditionSetView = applicableViews.find((viewProvider) => viewProvider.key === 'conditionSet.view');
|
||||
expect(conditionSetView).toBeDefined();
|
||||
});
|
||||
|
||||
@@ -83,7 +83,7 @@ describe('the plugin', function () {
|
||||
}
|
||||
};
|
||||
|
||||
const applicableViews = openmct.objectViews.get(testViewObject);
|
||||
const applicableViews = openmct.objectViews.get(testViewObject, []);
|
||||
let displayLayoutViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'layout.view');
|
||||
expect(displayLayoutViewProvider).toBeDefined();
|
||||
});
|
||||
|
||||
@@ -271,11 +271,6 @@ export default {
|
||||
});
|
||||
},
|
||||
removeFromComposition(identifier) {
|
||||
let keystring = this.openmct.objects.makeKeyString(identifier);
|
||||
|
||||
this.identifierMap[keystring] = undefined;
|
||||
delete this.identifierMap[keystring];
|
||||
|
||||
this.composition.remove({identifier});
|
||||
},
|
||||
setSelectionToParent() {
|
||||
@@ -355,6 +350,9 @@ export default {
|
||||
removeChildObject(identifier) {
|
||||
let removeIdentifier = this.openmct.objects.makeKeyString(identifier);
|
||||
|
||||
this.identifierMap[removeIdentifier] = undefined;
|
||||
delete this.identifierMap[removeIdentifier];
|
||||
|
||||
this.containers.forEach(container => {
|
||||
container.frames = container.frames.filter(frame => {
|
||||
let frameIdentifier = this.openmct.objects.makeKeyString(frame.domainObjectIdentifier);
|
||||
|
||||
@@ -124,6 +124,7 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="showSunHeading"
|
||||
class="c-sun"
|
||||
:style="sunHeadingStyle"
|
||||
></div>
|
||||
@@ -229,6 +230,9 @@ export default {
|
||||
transform: `rotate(${ rotation }deg)`
|
||||
};
|
||||
},
|
||||
showSunHeading() {
|
||||
return this.sunHeading !== undefined;
|
||||
},
|
||||
sunHeadingStyle() {
|
||||
const rotation = rotate(this.north, this.sunHeading);
|
||||
|
||||
|
||||
@@ -129,16 +129,16 @@
|
||||
:class="{'is-paused': isPaused}"
|
||||
@scroll="handleScroll"
|
||||
>
|
||||
<div v-for="(datum, index) in imageHistory"
|
||||
:key="datum.url + datum[timeKey]"
|
||||
<div v-for="(image, index) in imageHistory"
|
||||
:key="image.url + image.time"
|
||||
class="c-imagery__thumb c-thumb"
|
||||
:class="{ selected: focusedImageIndex === index && isPaused }"
|
||||
@click="setFocusedImage(index, thumbnailClick)"
|
||||
>
|
||||
<img class="c-thumb__image"
|
||||
:src="formatImageUrl(datum)"
|
||||
:src="image.url"
|
||||
>
|
||||
<div class="c-thumb__timestamp">{{ formatTime(datum) }}</div>
|
||||
<div class="c-thumb__timestamp">{{ image.formattedTime }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -271,11 +271,27 @@ export default {
|
||||
|
||||
if (this.relatedTelemetry.hasRelatedTelemetry) {
|
||||
isFresh = true;
|
||||
for (let key of this.spacecraftKeys) {
|
||||
for (let key of this.spacecraftPositionKeys) {
|
||||
if (this.relatedTelemetry[key] && latest[key] && focused[key]) {
|
||||
if (!this.relatedTelemetry[key].comparisonFunction(latest[key], focused[key])) {
|
||||
isFresh = false;
|
||||
}
|
||||
isFresh = isFresh && Boolean(this.relatedTelemetry[key].comparisonFunction(latest[key], focused[key]));
|
||||
} else {
|
||||
isFresh = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return isFresh;
|
||||
},
|
||||
isSpacecraftOrientationFresh() {
|
||||
let isFresh = undefined;
|
||||
let latest = this.latestRelatedTelemetry;
|
||||
let focused = this.focusedImageRelatedTelemetry;
|
||||
|
||||
if (this.relatedTelemetry.hasRelatedTelemetry) {
|
||||
isFresh = true;
|
||||
for (let key of this.spacecraftOrientationKeys) {
|
||||
if (this.relatedTelemetry[key] && latest[key] && focused[key]) {
|
||||
isFresh = isFresh && Boolean(this.relatedTelemetry[key].comparisonFunction(latest[key], focused[key]));
|
||||
} else {
|
||||
isFresh = false;
|
||||
}
|
||||
@@ -293,12 +309,10 @@ export default {
|
||||
isFresh = true;
|
||||
|
||||
// camera freshness relies on spacecraft position freshness
|
||||
if (this.isSpacecraftPositionFresh) {
|
||||
if (this.isSpacecraftPositionFresh && this.isSpacecraftOrientationFresh) {
|
||||
for (let key of this.cameraKeys) {
|
||||
if (this.relatedTelemetry[key] && latest[key] && focused[key]) {
|
||||
if (!this.relatedTelemetry[key].comparisonFunction(latest[key], focused[key])) {
|
||||
isFresh = false;
|
||||
}
|
||||
isFresh = isFresh && Boolean(this.relatedTelemetry[key].comparisonFunction(latest[key], focused[key]));
|
||||
} else {
|
||||
isFresh = false;
|
||||
}
|
||||
@@ -333,7 +347,8 @@ export default {
|
||||
this.imageFormatter = this.openmct.telemetry.getValueFormatter(this.imageHints);
|
||||
|
||||
// related telemetry keys
|
||||
this.spacecraftKeys = ['heading', 'roll', 'pitch'];
|
||||
this.spacecraftPositionKeys = ['positionX', 'positionY', 'positionZ'];
|
||||
this.spacecraftOrientationKeys = ['heading', 'roll', 'pitch'];
|
||||
this.cameraKeys = ['cameraPan', 'cameraTilt'];
|
||||
this.sunKeys = ['sunOrientation'];
|
||||
|
||||
@@ -380,7 +395,7 @@ export default {
|
||||
// unsubscribe from related telemetry
|
||||
if (this.relatedTelemetry.hasRelatedTelemetry) {
|
||||
for (let key of this.relatedTelemetry.keys) {
|
||||
if (this.relatedTelemetry[key].unsubscribe) {
|
||||
if (this.relatedTelemetry[key] && this.relatedTelemetry[key].unsubscribe) {
|
||||
this.relatedTelemetry[key].unsubscribe();
|
||||
}
|
||||
}
|
||||
@@ -391,7 +406,7 @@ export default {
|
||||
this.relatedTelemetry = new RelatedTelemetry(
|
||||
this.openmct,
|
||||
this.domainObject,
|
||||
[...this.spacecraftKeys, ...this.cameraKeys, ...this.sunKeys]
|
||||
[...this.spacecraftPositionKeys, ...this.spacecraftOrientationKeys, ...this.cameraKeys, ...this.sunKeys]
|
||||
);
|
||||
|
||||
if (this.relatedTelemetry.hasRelatedTelemetry) {
|
||||
@@ -466,7 +481,7 @@ export default {
|
||||
}
|
||||
},
|
||||
trackLatestRelatedTelemetry() {
|
||||
[...this.spacecraftKeys, ...this.cameraKeys, ...this.sunKeys].forEach(key => {
|
||||
[...this.spacecraftPositionKeys, ...this.spacecraftOrientationKeys, ...this.cameraKeys, ...this.sunKeys].forEach(key => {
|
||||
if (this.relatedTelemetry[key] && this.relatedTelemetry[key].subscribe) {
|
||||
this.relatedTelemetry[key].subscribe((datum) => {
|
||||
let valueKey = this.relatedTelemetry[key].realtime.valueKey;
|
||||
@@ -631,7 +646,12 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
this.imageHistory.push(datum);
|
||||
let image = { ...datum };
|
||||
image.formattedTime = this.formatTime(datum);
|
||||
image.url = this.formatImageUrl(datum);
|
||||
image.time = datum[this.timeKey];
|
||||
|
||||
this.imageHistory.push(image);
|
||||
|
||||
if (setFocused) {
|
||||
this.setFocusedImage(this.imageHistory.length - 1);
|
||||
|
||||
@@ -68,12 +68,14 @@ export default class RelatedTelemetry {
|
||||
|
||||
await Promise.all(
|
||||
this.keys.map(async (key) => {
|
||||
if (this[key].historical) {
|
||||
await this._initializeHistorical(key);
|
||||
}
|
||||
if (this[key]) {
|
||||
if (this[key].historical) {
|
||||
await this._initializeHistorical(key);
|
||||
}
|
||||
|
||||
if (this[key].realtime && this[key].realtime.telemetryObjectId) {
|
||||
await this._intializeRealtime(key);
|
||||
if (this[key].realtime && this[key].realtime.telemetryObjectId) {
|
||||
await this._intializeRealtime(key);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
@@ -153,7 +155,7 @@ export default class RelatedTelemetry {
|
||||
destroy() {
|
||||
this._openmct.time.off('timeSystem', this._timeSystemChange);
|
||||
for (let key of this.keys) {
|
||||
if (this[key].unsubscribe) {
|
||||
if (this[key] && this[key].unsubscribe) {
|
||||
this[key].unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,7 +235,7 @@ describe("The Imagery View Layout", () => {
|
||||
});
|
||||
|
||||
it("should provide an imagery view only for imagery producing objects", () => {
|
||||
let applicableViews = openmct.objectViews.get(imageryObject);
|
||||
let applicableViews = openmct.objectViews.get(imageryObject, []);
|
||||
let imageryView = applicableViews.find(
|
||||
viewProvider => viewProvider.key === imageryKey
|
||||
);
|
||||
@@ -265,7 +265,7 @@ describe("The Imagery View Layout", () => {
|
||||
end: bounds.end + 100
|
||||
});
|
||||
|
||||
applicableViews = openmct.objectViews.get(imageryObject);
|
||||
applicableViews = openmct.objectViews.get(imageryObject, []);
|
||||
imageryViewProvider = applicableViews.find(viewProvider => viewProvider.key === imageryKey);
|
||||
imageryView = imageryViewProvider.view(imageryObject);
|
||||
imageryView.show(child);
|
||||
|
||||
@@ -101,7 +101,7 @@ describe("Notebook plugin:", () => {
|
||||
creatable: true
|
||||
};
|
||||
|
||||
const applicableViews = openmct.objectViews.get(notebookViewObject);
|
||||
const applicableViews = openmct.objectViews.get(notebookViewObject, []);
|
||||
notebookViewProvider = applicableViews.find(viewProvider => viewProvider.key === notebookObject.key);
|
||||
notebookView = notebookViewProvider.view(notebookViewObject);
|
||||
|
||||
|
||||
@@ -56,11 +56,24 @@ const notebookStorage = {
|
||||
}
|
||||
};
|
||||
|
||||
let openmct = createOpenMct();
|
||||
let openmct;
|
||||
let mockIdentifierService;
|
||||
|
||||
describe('Notebook Storage:', () => {
|
||||
beforeEach((done) => {
|
||||
openmct = createOpenMct();
|
||||
openmct.$injector = jasmine.createSpyObj('$injector', ['get']);
|
||||
mockIdentifierService = jasmine.createSpyObj(
|
||||
'identifierService',
|
||||
['parse']
|
||||
);
|
||||
mockIdentifierService.parse.and.returnValue({
|
||||
getSpace: () => {
|
||||
return '';
|
||||
}
|
||||
});
|
||||
|
||||
openmct.$injector.get.and.returnValue(mockIdentifierService);
|
||||
window.localStorage.setItem('notebook-storage', null);
|
||||
openmct.objects.addProvider('', jasmine.createSpyObj('mockNotebookProvider', [
|
||||
'create',
|
||||
|
||||
@@ -57,11 +57,20 @@ export default class CouchObjectProvider {
|
||||
return options;
|
||||
}
|
||||
|
||||
request(subPath, method, value) {
|
||||
return fetch(this.url + '/' + subPath, {
|
||||
method: method,
|
||||
body: JSON.stringify(value)
|
||||
}).then(response => response.json())
|
||||
request(subPath, method, body, signal) {
|
||||
let fetchOptions = {
|
||||
method,
|
||||
body,
|
||||
signal
|
||||
};
|
||||
|
||||
// stringify body if needed
|
||||
if (fetchOptions.body) {
|
||||
fetchOptions.body = JSON.stringify(fetchOptions.body);
|
||||
}
|
||||
|
||||
return fetch(this.url + '/' + subPath, fetchOptions)
|
||||
.then(response => response.json())
|
||||
.then(function (response) {
|
||||
return response;
|
||||
}, function () {
|
||||
@@ -121,8 +130,8 @@ export default class CouchObjectProvider {
|
||||
}
|
||||
}
|
||||
|
||||
get(identifier) {
|
||||
return this.request(identifier.key, "GET").then(this.getModel.bind(this));
|
||||
get(identifier, abortSignal) {
|
||||
return this.request(identifier.key, "GET", undefined, abortSignal).then(this.getModel.bind(this));
|
||||
}
|
||||
|
||||
async getObjectsByFilter(filter) {
|
||||
@@ -145,7 +154,8 @@ export default class CouchObjectProvider {
|
||||
|
||||
const reader = response.body.getReader();
|
||||
let completed = false;
|
||||
|
||||
let decoder = new TextDecoder("utf-8");
|
||||
let decodedChunk = '';
|
||||
while (!completed) {
|
||||
const {done, value} = await reader.read();
|
||||
//done is true when we lose connection with the provider
|
||||
@@ -156,23 +166,24 @@ export default class CouchObjectProvider {
|
||||
if (value) {
|
||||
let chunk = new Uint8Array(value.length);
|
||||
chunk.set(value, 0);
|
||||
const decodedChunk = new TextDecoder("utf-8").decode(chunk);
|
||||
try {
|
||||
const json = JSON.parse(decodedChunk);
|
||||
if (json) {
|
||||
let docs = json.docs;
|
||||
docs.forEach(doc => {
|
||||
let object = this.getModel(doc);
|
||||
if (object) {
|
||||
objects.push(object);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
//do nothing
|
||||
}
|
||||
const partial = decoder.decode(chunk, {stream: !completed});
|
||||
decodedChunk = decodedChunk + partial;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const json = JSON.parse(decodedChunk);
|
||||
if (json) {
|
||||
let docs = json.docs;
|
||||
docs.forEach(doc => {
|
||||
let object = this.getModel(doc);
|
||||
if (object) {
|
||||
objects.push(object);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
//do nothing
|
||||
}
|
||||
|
||||
return objects;
|
||||
@@ -311,7 +322,8 @@ export default class CouchObjectProvider {
|
||||
this.enqueueObject(key, model, intermediateResponse);
|
||||
this.objectQueue[key].pending = true;
|
||||
const queued = this.objectQueue[key].dequeue();
|
||||
this.request(key, "PUT", new CouchDocument(key, queued.model)).then((response) => {
|
||||
let document = new CouchDocument(key, queued.model);
|
||||
this.request(key, "PUT", document).then((response) => {
|
||||
this.checkResponse(response, queued.intermediateResponse);
|
||||
});
|
||||
|
||||
@@ -322,7 +334,8 @@ export default class CouchObjectProvider {
|
||||
if (!this.objectQueue[key].pending) {
|
||||
this.objectQueue[key].pending = true;
|
||||
const queued = this.objectQueue[key].dequeue();
|
||||
this.request(key, "PUT", new CouchDocument(key, queued.model, this.objectQueue[key].rev)).then((response) => {
|
||||
let document = new CouchDocument(key, queued.model, this.objectQueue[key].rev);
|
||||
this.request(key, "PUT", document).then((response) => {
|
||||
this.checkResponse(response, queued.intermediateResponse);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div ref="plan"
|
||||
class="c-plan"
|
||||
class="c-plan c-timeline-holder"
|
||||
>
|
||||
<template v-if="viewBounds && !options.compact">
|
||||
<swim-lane>
|
||||
@@ -22,10 +22,10 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as d3Selection from 'd3-selection';
|
||||
import * as d3Scale from 'd3-scale';
|
||||
import TimelineAxis from "../../ui/components/TimeSystemAxis.vue";
|
||||
import SwimLane from "@/ui/components/swim-lane/SwimLane.vue";
|
||||
import { getValidatedPlan } from "./util";
|
||||
import Vue from "vue";
|
||||
|
||||
//TODO: UI direction needed for the following property values
|
||||
@@ -38,9 +38,8 @@ const RESIZE_POLL_INTERVAL = 200;
|
||||
const ROW_HEIGHT = 25;
|
||||
const LINE_HEIGHT = 12;
|
||||
const MAX_TEXT_WIDTH = 300;
|
||||
const EDGE_ROUNDING = 10;
|
||||
const DEFAULT_COLOR = 'yellow';
|
||||
const DEFAULT_TEXT_COLOR = 'white';
|
||||
const EDGE_ROUNDING = 5;
|
||||
const DEFAULT_COLOR = '#cc9922';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -72,7 +71,7 @@ export default {
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.validateJSON(this.domainObject.selectFile.body);
|
||||
this.getPlanData(this.domainObject);
|
||||
|
||||
this.canvas = this.$refs.plan.appendChild(document.createElement('canvas'));
|
||||
this.canvas.height = 0;
|
||||
@@ -85,7 +84,7 @@ export default {
|
||||
this.resizeTimer = setInterval(this.resize, RESIZE_POLL_INTERVAL);
|
||||
this.unlisten = this.openmct.objects.observe(this.domainObject, '*', this.observeForChanges);
|
||||
},
|
||||
destroyed() {
|
||||
beforeDestroy() {
|
||||
clearInterval(this.resizeTimer);
|
||||
this.openmct.time.off("timeSystem", this.setScaleAndPlotActivities);
|
||||
this.openmct.time.off("bounds", this.updateViewBounds);
|
||||
@@ -95,7 +94,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
observeForChanges(mutatedObject) {
|
||||
this.validateJSON(mutatedObject.selectFile.body);
|
||||
this.getPlanData(mutatedObject);
|
||||
this.setScaleAndPlotActivities();
|
||||
},
|
||||
resize() {
|
||||
@@ -118,14 +117,8 @@ export default {
|
||||
|
||||
return clientWidth - 200;
|
||||
},
|
||||
validateJSON(jsonString) {
|
||||
try {
|
||||
this.json = JSON.parse(jsonString);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
getPlanData(domainObject) {
|
||||
this.planData = getValidatedPlan(domainObject);
|
||||
},
|
||||
updateViewBounds() {
|
||||
this.viewBounds = this.openmct.time.bounds();
|
||||
@@ -148,7 +141,8 @@ export default {
|
||||
}
|
||||
},
|
||||
clearPreviousActivities() {
|
||||
d3Selection.selectAll(".c-plan__contents > div").remove();
|
||||
let activities = this.$el.querySelectorAll(".c-plan__contents > div");
|
||||
activities.forEach(activity => activity.remove());
|
||||
},
|
||||
setDimensions() {
|
||||
const planHolder = this.$refs.plan;
|
||||
@@ -231,14 +225,14 @@ export default {
|
||||
return (currentRow || 0);
|
||||
},
|
||||
calculatePlanLayout() {
|
||||
let groups = Object.keys(this.json);
|
||||
let groups = Object.keys(this.planData);
|
||||
this.groupActivities = {};
|
||||
|
||||
groups.forEach((key, index) => {
|
||||
let activitiesByRow = {};
|
||||
let currentRow = 0;
|
||||
|
||||
let activities = this.json[key];
|
||||
let activities = this.planData[key];
|
||||
activities.forEach((activity) => {
|
||||
if (this.isActivityInBounds(activity)) {
|
||||
const currentStart = Math.max(this.viewBounds.start, activity.start);
|
||||
@@ -251,6 +245,13 @@ export default {
|
||||
//TODO: Fix bug for SVG where the rectWidth is not proportional to the canvas measuredWidth of the text
|
||||
const activityNameFitsRect = (rectWidth >= activityNameWidth);
|
||||
const textStart = (activityNameFitsRect ? rectX : rectY) + TEXT_LEFT_PADDING;
|
||||
const color = activity.color || DEFAULT_COLOR;
|
||||
let textColor = '';
|
||||
if (activity.textColor) {
|
||||
textColor = activity.textColor;
|
||||
} else if (activityNameFitsRect) {
|
||||
textColor = this.getContrastingColor(color);
|
||||
}
|
||||
|
||||
let textLines = this.getActivityDisplayText(this.canvasContext, activity.name, activityNameFitsRect);
|
||||
const textWidth = textStart + this.getTextWidth(textLines[0]) + TEXT_LEFT_PADDING;
|
||||
@@ -269,8 +270,8 @@ export default {
|
||||
|
||||
activitiesByRow[currentRow].push({
|
||||
activity: {
|
||||
color: activity.color || DEFAULT_COLOR,
|
||||
textColor: activity.textColor || DEFAULT_TEXT_COLOR,
|
||||
color: color,
|
||||
textColor: textColor,
|
||||
name: activity.name,
|
||||
exceeds: {
|
||||
start: this.xScale(this.viewBounds.start) > this.xScale(activity.start),
|
||||
@@ -279,6 +280,7 @@ export default {
|
||||
},
|
||||
textLines: textLines,
|
||||
textStart: textStart,
|
||||
textClass: activityNameFitsRect ? "" : "activity-label--outside-rect",
|
||||
textY: textY,
|
||||
start: rectX,
|
||||
end: activityNameFitsRect ? rectY : textStart + textWidth,
|
||||
@@ -381,9 +383,9 @@ export default {
|
||||
this.setNSAttributesForElement(textElement, {
|
||||
x: "10",
|
||||
y: "20",
|
||||
class: "no-activities"
|
||||
class: "activity-label--outside-rect"
|
||||
});
|
||||
textElement.innerHTML = 'No activities at this time';
|
||||
textElement.innerHTML = 'No activities within timeframe';
|
||||
|
||||
svgElement.appendChild(textElement);
|
||||
},
|
||||
@@ -423,11 +425,14 @@ export default {
|
||||
width = width + EDGE_ROUNDING;
|
||||
}
|
||||
|
||||
width = Math.max(width, 1); // Set width to a minimum of 1
|
||||
|
||||
// rx: don't round corners if the width of the rect is smaller than the rounding radius
|
||||
this.setNSAttributesForElement(rectElement, {
|
||||
class: 'activity-bounds',
|
||||
x: item.activity.exceeds.start ? item.start - EDGE_ROUNDING : item.start,
|
||||
y: row,
|
||||
rx: EDGE_ROUNDING,
|
||||
rx: (width < EDGE_ROUNDING * 2) ? 0 : EDGE_ROUNDING,
|
||||
width: width,
|
||||
height: String(ROW_HEIGHT),
|
||||
fill: activity.color
|
||||
@@ -438,7 +443,7 @@ export default {
|
||||
item.textLines.forEach((line, index) => {
|
||||
let textElement = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||
this.setNSAttributesForElement(textElement, {
|
||||
class: 'activity-label',
|
||||
class: `activity-label ${item.textClass}`,
|
||||
x: item.textStart,
|
||||
y: item.textY + (index * LINE_HEIGHT),
|
||||
fill: activity.textColor
|
||||
@@ -449,6 +454,29 @@ export default {
|
||||
svgElement.appendChild(textElement);
|
||||
});
|
||||
// this.addForeignElement(svgElement, activity.name, item.textStart, item.textY - LINE_HEIGHT);
|
||||
},
|
||||
cutHex(h, start, end) {
|
||||
const hStr = (h.charAt(0) === '#') ? h.substring(1, 7) : h;
|
||||
|
||||
return parseInt(hStr.substring(start, end), 16);
|
||||
},
|
||||
getContrastingColor(hexColor) {
|
||||
// https://codepen.io/davidhalford/pen/ywEva/
|
||||
// TODO: move this into a general utility function?
|
||||
const cThreshold = 130;
|
||||
|
||||
if (hexColor.indexOf('#') === -1) {
|
||||
// We weren't given a hex color
|
||||
return "#ff0000";
|
||||
}
|
||||
|
||||
const hR = this.cutHex(hexColor, 0, 2);
|
||||
const hG = this.cutHex(hexColor, 2, 4);
|
||||
const hB = this.cutHex(hexColor, 4, 6);
|
||||
|
||||
const cBrightness = ((hR * 299) + (hG * 587) + (hB * 114)) / 1000;
|
||||
|
||||
return cBrightness > cThreshold ? "#000000" : "#ffffff";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,21 +1,16 @@
|
||||
.c-plan {
|
||||
|
||||
@include abs();
|
||||
|
||||
svg {
|
||||
text-rendering: geometricPrecision;
|
||||
|
||||
.activity-label, .no-activities {
|
||||
text {
|
||||
stroke: none;
|
||||
}
|
||||
|
||||
.no-activities {
|
||||
fill: #383838;
|
||||
}
|
||||
|
||||
.activity-bounds {
|
||||
fill-opacity: 0.5;
|
||||
}
|
||||
.activity-label {
|
||||
&--outside-rect {
|
||||
fill: $colorBodyFg !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
canvas {
|
||||
|
||||
@@ -82,7 +82,7 @@ describe('the plugin', function () {
|
||||
type: "plan"
|
||||
};
|
||||
|
||||
const applicableViews = openmct.objectViews.get(testViewObject);
|
||||
const applicableViews = openmct.objectViews.get(testViewObject, []);
|
||||
let planView = applicableViews.find((viewProvider) => viewProvider.key === 'plan.view');
|
||||
expect(planView).toBeDefined();
|
||||
});
|
||||
@@ -135,7 +135,7 @@ describe('the plugin', function () {
|
||||
}
|
||||
};
|
||||
|
||||
const applicableViews = openmct.objectViews.get(planDomainObject);
|
||||
const applicableViews = openmct.objectViews.get(planDomainObject, []);
|
||||
planView = applicableViews.find((viewProvider) => viewProvider.key === 'plan.view');
|
||||
let view = planView.view(planDomainObject, mockObjectPath);
|
||||
view.show(child, true);
|
||||
|
||||
15
src/plugins/plan/util.js
Normal file
15
src/plugins/plan/util.js
Normal file
@@ -0,0 +1,15 @@
|
||||
export function getValidatedPlan(domainObject) {
|
||||
let body = domainObject.selectFile.body;
|
||||
let json = {};
|
||||
if (typeof body === 'string') {
|
||||
try {
|
||||
json = JSON.parse(body);
|
||||
} catch (e) {
|
||||
return json;
|
||||
}
|
||||
} else {
|
||||
json = body;
|
||||
}
|
||||
|
||||
return json;
|
||||
}
|
||||
@@ -32,12 +32,12 @@ export default function OverlayPlotViewProvider(openmct) {
|
||||
key: 'plot-overlay',
|
||||
name: 'Overlay Plot',
|
||||
cssClass: 'icon-telemetry',
|
||||
canView(domainObject) {
|
||||
return domainObject.type === 'telemetry.plot.overlay';
|
||||
canView(domainObject, objectPath) {
|
||||
return isCompactView(objectPath) && domainObject.type === 'telemetry.plot.overlay';
|
||||
},
|
||||
|
||||
canEdit(domainObject) {
|
||||
return domainObject.type === 'telemetry.plot.overlay';
|
||||
canEdit(domainObject, objectPath) {
|
||||
return isCompactView(objectPath) && domainObject.type === 'telemetry.plot.overlay';
|
||||
},
|
||||
|
||||
view: function (domainObject, objectPath) {
|
||||
|
||||
@@ -47,8 +47,8 @@ export default function PlotViewProvider(openmct) {
|
||||
key: 'plot-simple',
|
||||
name: 'Plot',
|
||||
cssClass: 'icon-telemetry',
|
||||
canView(domainObject) {
|
||||
return hasTelemetry(domainObject, openmct);
|
||||
canView(domainObject, objectPath) {
|
||||
return isCompactView(objectPath) && hasTelemetry(domainObject, openmct);
|
||||
},
|
||||
|
||||
view: function (domainObject, objectPath) {
|
||||
|
||||
@@ -33,8 +33,27 @@ describe("the plugin", function () {
|
||||
let openmct;
|
||||
let telemetryPromise;
|
||||
let cleanupFirst;
|
||||
let mockObjectPath;
|
||||
|
||||
beforeEach((done) => {
|
||||
mockObjectPath = [
|
||||
{
|
||||
name: 'mock folder',
|
||||
type: 'fake-folder',
|
||||
identifier: {
|
||||
key: 'mock-folder',
|
||||
namespace: ''
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'mock parent folder',
|
||||
type: 'time-strip',
|
||||
identifier: {
|
||||
key: 'mock-parent-folder',
|
||||
namespace: ''
|
||||
}
|
||||
}
|
||||
];
|
||||
const testTelemetry = [
|
||||
{
|
||||
'utc': 1,
|
||||
@@ -134,7 +153,7 @@ describe("the plugin", function () {
|
||||
}
|
||||
};
|
||||
|
||||
const applicableViews = openmct.objectViews.get(testTelemetryObject);
|
||||
const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath);
|
||||
let plotView = applicableViews.find((viewProvider) => viewProvider.key === "plot-simple");
|
||||
expect(plotView).toBeDefined();
|
||||
});
|
||||
@@ -150,7 +169,7 @@ describe("the plugin", function () {
|
||||
}
|
||||
};
|
||||
|
||||
const applicableViews = openmct.objectViews.get(testTelemetryObject);
|
||||
const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath);
|
||||
let plotView = applicableViews.find((viewProvider) => viewProvider.key === "plot-overlay");
|
||||
expect(plotView).toBeDefined();
|
||||
});
|
||||
@@ -166,7 +185,7 @@ describe("the plugin", function () {
|
||||
}
|
||||
};
|
||||
|
||||
const applicableViews = openmct.objectViews.get(testTelemetryObject);
|
||||
const applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath);
|
||||
let plotView = applicableViews.find((viewProvider) => viewProvider.key === "plot-stacked");
|
||||
expect(plotView).toBeDefined();
|
||||
});
|
||||
@@ -218,7 +237,7 @@ describe("the plugin", function () {
|
||||
}
|
||||
};
|
||||
|
||||
applicableViews = openmct.objectViews.get(testTelemetryObject);
|
||||
applicableViews = openmct.objectViews.get(testTelemetryObject, mockObjectPath);
|
||||
plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === "plot-simple");
|
||||
plotView = plotViewProvider.view(testTelemetryObject, [testTelemetryObject]);
|
||||
plotView.show(child, true);
|
||||
|
||||
@@ -32,12 +32,12 @@ export default function StackedPlotViewProvider(openmct) {
|
||||
key: 'plot-stacked',
|
||||
name: 'Stacked Plot',
|
||||
cssClass: 'icon-telemetry',
|
||||
canView(domainObject) {
|
||||
return domainObject.type === 'telemetry.plot.stacked';
|
||||
canView(domainObject, objectPath) {
|
||||
return isCompactView(objectPath) && domainObject.type === 'telemetry.plot.stacked';
|
||||
},
|
||||
|
||||
canEdit(domainObject) {
|
||||
return domainObject.type === 'telemetry.plot.stacked';
|
||||
canEdit(domainObject, objectPath) {
|
||||
return isCompactView(objectPath) && domainObject.type === 'telemetry.plot.stacked';
|
||||
},
|
||||
|
||||
view: function (domainObject, objectPath) {
|
||||
|
||||
@@ -103,7 +103,7 @@ describe("the plugin", () => {
|
||||
}
|
||||
};
|
||||
|
||||
const applicableViews = openmct.objectViews.get(testTelemetryObject);
|
||||
const applicableViews = openmct.objectViews.get(testTelemetryObject, []);
|
||||
let tableView = applicableViews.find((viewProvider) => viewProvider.key === 'table');
|
||||
expect(tableView).toBeDefined();
|
||||
});
|
||||
@@ -174,7 +174,7 @@ describe("the plugin", () => {
|
||||
|
||||
openmct.router.path = [testTelemetryObject];
|
||||
|
||||
applicableViews = openmct.objectViews.get(testTelemetryObject);
|
||||
applicableViews = openmct.objectViews.get(testTelemetryObject, []);
|
||||
tableViewProvider = applicableViews.find((viewProvider) => viewProvider.key === 'table');
|
||||
tableView = tableViewProvider.view(testTelemetryObject, [testTelemetryObject]);
|
||||
tableView.show(child, true);
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
<swim-lane :icon-class="item.type.definition.cssClass"
|
||||
:min-height="item.height"
|
||||
:show-ucontents="item.domainObject.type === 'plan'"
|
||||
:span-rows="item.domainObject.type === 'plan'"
|
||||
:span-rows-count="item.rowCount"
|
||||
>
|
||||
<template slot="label">
|
||||
{{ item.domainObject.name }}
|
||||
@@ -78,6 +78,7 @@
|
||||
import ObjectView from '@/ui/components/ObjectView.vue';
|
||||
import TimelineAxis from '../../ui/components/TimeSystemAxis.vue';
|
||||
import SwimLane from "@/ui/components/swim-lane/SwimLane.vue";
|
||||
import { getValidatedPlan } from "../plan/util";
|
||||
|
||||
const unknownObjectType = {
|
||||
definition: {
|
||||
@@ -86,9 +87,9 @@ const unknownObjectType = {
|
||||
}
|
||||
};
|
||||
|
||||
function getViewKey(domainObject, openmct) {
|
||||
function getViewKey(domainObject, objectPath, openmct) {
|
||||
let viewKey = '';
|
||||
const plotView = openmct.objectViews.get(domainObject).find((view) => {
|
||||
const plotView = openmct.objectViews.get(domainObject, objectPath).find((view) => {
|
||||
return view.key.startsWith('plot-') && view.key !== 'plot-single';
|
||||
});
|
||||
if (plotView) {
|
||||
@@ -116,6 +117,8 @@ export default {
|
||||
this.composition.off('add', this.addItem);
|
||||
this.composition.off('remove', this.removeItem);
|
||||
this.composition.off('reorder', this.reorder);
|
||||
this.openmct.time.off("bounds", this.updateViewBounds);
|
||||
|
||||
},
|
||||
mounted() {
|
||||
if (this.composition) {
|
||||
@@ -126,13 +129,18 @@ export default {
|
||||
}
|
||||
|
||||
this.getTimeSystems();
|
||||
this.openmct.time.on("bounds", this.updateViewBounds);
|
||||
},
|
||||
methods: {
|
||||
addItem(domainObject) {
|
||||
let type = this.openmct.types.get(domainObject.type) || unknownObjectType;
|
||||
let keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
|
||||
let objectPath = [domainObject].concat(this.objectPath.slice());
|
||||
let viewKey = getViewKey(domainObject, this.openmct);
|
||||
let viewKey = getViewKey(domainObject, objectPath, this.openmct);
|
||||
let rowCount = 0;
|
||||
if (domainObject.type === 'plan') {
|
||||
rowCount = Object.keys(getValidatedPlan(domainObject)).length;
|
||||
}
|
||||
|
||||
let height = domainObject.type === 'telemetry.plot.stacked' ? `${domainObject.composition.length * 100}px` : '100px';
|
||||
let item = {
|
||||
@@ -141,6 +149,7 @@ export default {
|
||||
type,
|
||||
keyString,
|
||||
viewKey,
|
||||
rowCount,
|
||||
height
|
||||
};
|
||||
|
||||
@@ -174,6 +183,12 @@ export default {
|
||||
|
||||
//TODO: Some kind of translation via an offset? of current bounds to target timeSystem
|
||||
return currentBounds;
|
||||
},
|
||||
updateViewBounds(bounds) {
|
||||
let currentTimeSystem = this.timeSystems.find(item => item.timeSystem.key === this.openmct.time.timeSystem().key);
|
||||
if (currentTimeSystem) {
|
||||
currentTimeSystem.bounds = bounds;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -22,14 +22,34 @@
|
||||
|
||||
import { createOpenMct, resetApplicationState } from "utils/testing";
|
||||
import TimelinePlugin from "./plugin";
|
||||
import Vue from 'vue';
|
||||
|
||||
describe('the plugin', function () {
|
||||
let objectDef;
|
||||
let element;
|
||||
let child;
|
||||
let openmct;
|
||||
let mockObjectPath;
|
||||
|
||||
beforeEach((done) => {
|
||||
mockObjectPath = [
|
||||
{
|
||||
name: 'mock folder',
|
||||
type: 'fake-folder',
|
||||
identifier: {
|
||||
key: 'mock-folder',
|
||||
namespace: ''
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'mock parent folder',
|
||||
type: 'time-strip',
|
||||
identifier: {
|
||||
key: 'mock-parent-folder',
|
||||
namespace: ''
|
||||
}
|
||||
}
|
||||
];
|
||||
const appHolder = document.createElement('div');
|
||||
appHolder.style.width = '640px';
|
||||
appHolder.style.height = '480px';
|
||||
@@ -47,7 +67,7 @@ describe('the plugin', function () {
|
||||
child.style.height = '480px';
|
||||
element.appendChild(child);
|
||||
|
||||
openmct.time.bounds({
|
||||
openmct.time.timeSystem('utc', {
|
||||
start: 1597160002854,
|
||||
end: 1597181232854
|
||||
});
|
||||
@@ -75,18 +95,32 @@ describe('the plugin', function () {
|
||||
it('is creatable', () => {
|
||||
expect(objectDef.creatable).toEqual(mockObject.creatable);
|
||||
});
|
||||
});
|
||||
|
||||
it('provides a timeline view', () => {
|
||||
describe('the view', () => {
|
||||
let timelineView;
|
||||
|
||||
beforeEach((done) => {
|
||||
const testViewObject = {
|
||||
id: "test-object",
|
||||
type: "time-strip"
|
||||
};
|
||||
|
||||
const applicableViews = openmct.objectViews.get(testViewObject);
|
||||
let timelineView = applicableViews.find((viewProvider) => viewProvider.key === 'time-strip.view');
|
||||
const applicableViews = openmct.objectViews.get(testViewObject, mockObjectPath);
|
||||
timelineView = applicableViews.find((viewProvider) => viewProvider.key === 'time-strip.view');
|
||||
let view = timelineView.view(testViewObject, element);
|
||||
view.show(child, true);
|
||||
Vue.nextTick(done);
|
||||
});
|
||||
|
||||
it('provides a view', () => {
|
||||
expect(timelineView).toBeDefined();
|
||||
});
|
||||
|
||||
it('displays a time axis', () => {
|
||||
const el = element.querySelector('.c-timesystem-axis');
|
||||
expect(el).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
.c-timeline-holder {
|
||||
@include abs();
|
||||
}
|
||||
|
||||
.c-timeline {
|
||||
|
||||
}
|
||||
@include abs();
|
||||
overflow-x: hidden;
|
||||
}
|
||||
@@ -34,7 +34,7 @@
|
||||
@import "../ui/components/object-label.scss";
|
||||
@import "../ui/components/progress-bar.scss";
|
||||
@import "../ui/components/search.scss";
|
||||
@import "../ui/components/swim-lane/swim-lane.scss";
|
||||
@import "../ui/components/swim-lane/swimlane.scss";
|
||||
@import "../ui/components/toggle-switch.scss";
|
||||
@import "../ui/components/timesystem-axis.scss";
|
||||
@import "../ui/inspector/elements.scss";
|
||||
|
||||
@@ -320,7 +320,8 @@ export default {
|
||||
let provider = this.openmct.objectViews.getByProviderKey(this.getViewKey());
|
||||
|
||||
if (!provider) {
|
||||
provider = this.openmct.objectViews.get(this.domainObject)[0];
|
||||
let objectPath = this.currentObjectPath || this.objectPath;
|
||||
provider = this.openmct.objectViews.get(this.domainObject, objectPath)[0];
|
||||
if (!provider) {
|
||||
return;
|
||||
}
|
||||
@@ -329,10 +330,11 @@ export default {
|
||||
return provider;
|
||||
},
|
||||
editIfEditable(event) {
|
||||
let objectPath = this.currentObjectPath || this.objectPath;
|
||||
let provider = this.getViewProvider();
|
||||
if (provider
|
||||
&& provider.canEdit
|
||||
&& provider.canEdit(this.domainObject)
|
||||
&& provider.canEdit(this.domainObject, objectPath)
|
||||
&& this.isEditingAllowed()
|
||||
&& !this.openmct.editor.isEditing()) {
|
||||
this.openmct.editor.edit();
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<template>
|
||||
<div class="u-contents"
|
||||
:class="{'c-swim-lane': !isNested}"
|
||||
:class="{'c-swimlane': !isNested}"
|
||||
>
|
||||
|
||||
<div class="c-swim-lane__lane-label c-object-label"
|
||||
:class="{'c-swim-lane__lane-label--span-rows': spanRows, 'c-swim-lane__lane-label--span-cols': (!spanRows && !isNested)}"
|
||||
<div class="c-swimlane__lane-label c-object-label"
|
||||
:class="{'c-swimlane__lane-label--span-cols': (!spanRowsCount && !isNested)}"
|
||||
:style="gridRowSpan"
|
||||
>
|
||||
<div v-if="iconClass"
|
||||
class="c-object-label__type-icon"
|
||||
@@ -17,7 +18,7 @@
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="c-swim-lane__lane-object"
|
||||
<div class="c-swimlane__lane-object"
|
||||
:style="{'min-height': minHeight}"
|
||||
:class="{'u-contents': showUcontents}"
|
||||
data-selectable
|
||||
@@ -55,10 +56,19 @@ export default {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
spanRows: {
|
||||
type: Boolean,
|
||||
spanRowsCount: {
|
||||
type: Number,
|
||||
default() {
|
||||
return false;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
gridRowSpan() {
|
||||
if (this.spanRowsCount) {
|
||||
return `grid-row: span ${this.spanRowsCount}`;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
.c-swim-lane {
|
||||
display: grid;
|
||||
grid-template-columns: 100px 100px 1fr;
|
||||
grid-column-gap: 1px;
|
||||
grid-row-gap: 1px;
|
||||
width: 100%;
|
||||
|
||||
[class*='__lane-label'] {
|
||||
background: rgba($colorBodyFg, 0.2); // TODO: convert to theme constant
|
||||
color: $colorBodyFg; // TODO: convert to theme constant
|
||||
padding: $interiorMarginSm;
|
||||
}
|
||||
|
||||
[class*='--span-cols'] {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
[class*='--span-rows'] {
|
||||
grid-row: span 4;
|
||||
}
|
||||
|
||||
&__lane-object {
|
||||
.c-plan {
|
||||
display: contents;
|
||||
}
|
||||
}
|
||||
}
|
||||
26
src/ui/components/swim-lane/swimlane.scss
Normal file
26
src/ui/components/swim-lane/swimlane.scss
Normal file
@@ -0,0 +1,26 @@
|
||||
.c-swimlane {
|
||||
display: grid;
|
||||
grid-template-columns: 100px 100px 1fr;
|
||||
grid-column-gap: 1px;
|
||||
grid-row-gap: 1px;
|
||||
margin-bottom: 1px;
|
||||
width: 100%;
|
||||
|
||||
[class*='__lane-label'] {
|
||||
background: rgba($colorBodyFg, 0.2);
|
||||
color: $colorBodyFg;
|
||||
padding: $interiorMarginSm;
|
||||
}
|
||||
|
||||
[class*='--span-cols'] {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
&__lane-object {
|
||||
background: rgba(black, 0.1);
|
||||
|
||||
.c-plan {
|
||||
display: contents;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,35 +1,42 @@
|
||||
.c-timesystem-axis {
|
||||
$h: 30px;
|
||||
height: $h;
|
||||
$h: 30px;
|
||||
height: $h;
|
||||
|
||||
svg {
|
||||
text-rendering: geometricPrecision;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
svg {
|
||||
$lineC: rgba($colorBodyFg, 0.3) !important;
|
||||
text-rendering: geometricPrecision;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
text:not(.activity) {
|
||||
// Tick labels
|
||||
fill: $colorBodyFg;
|
||||
paint-order: stroke;
|
||||
font-weight: bold;
|
||||
stroke: $colorBodyBg;
|
||||
stroke-linecap: butt;
|
||||
stroke-linejoin: bevel;
|
||||
stroke-width: 6px;
|
||||
.domain {
|
||||
stroke: $lineC;
|
||||
}
|
||||
|
||||
.tick {
|
||||
line {
|
||||
stroke: $lineC;
|
||||
}
|
||||
|
||||
text {
|
||||
// Tick labels
|
||||
fill: $colorBodyFg;
|
||||
paint-order: stroke;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nowMarker {
|
||||
width: 2px;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
background: gray;
|
||||
.nowMarker {
|
||||
width: 2px;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
background: gray;
|
||||
|
||||
& .icon-arrow-down {
|
||||
font-size: large;
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
left: -8px;
|
||||
& .icon-arrow-down {
|
||||
font-size: large;
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
left: -8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
96
src/ui/inspector/ElementItem.vue
Normal file
96
src/ui/inspector/ElementItem.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<li
|
||||
draggable="true"
|
||||
@dragstart="emitDragStartEvent"
|
||||
@dragenter="onDragenter"
|
||||
@dragover="onDragover"
|
||||
@dragleave="onDragleave"
|
||||
@drop="emitDropEvent"
|
||||
>
|
||||
<div
|
||||
class="c-tree__item c-elements-pool__item"
|
||||
:class="{
|
||||
'is-context-clicked': contextClickActive,
|
||||
'hover': hover
|
||||
}"
|
||||
>
|
||||
<span
|
||||
class="c-elements-pool__grippy c-grippy c-grippy--vertical-drag"
|
||||
></span>
|
||||
<object-label
|
||||
:domain-object="elementObject"
|
||||
:object-path="[elementObject, parentObject]"
|
||||
@context-click-active="setContextClickState"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ObjectLabel from '../components/ObjectLabel.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ObjectLabel
|
||||
},
|
||||
props: {
|
||||
index: {
|
||||
type: Number,
|
||||
required: true,
|
||||
default: () => {
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
elementObject: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
parentObject: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
allowDrop: {
|
||||
type: Boolean
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
contextClickActive: false,
|
||||
hover: false
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onDragover(event) {
|
||||
event.preventDefault();
|
||||
},
|
||||
emitDropEvent(event) {
|
||||
this.$emit('drop-custom', this.index);
|
||||
this.hover = false;
|
||||
},
|
||||
emitDragStartEvent(event) {
|
||||
this.$emit('dragstart-custom', this.index);
|
||||
},
|
||||
onDragenter(event) {
|
||||
if (this.allowDrop) {
|
||||
this.hover = true;
|
||||
this.dragElement = event.target.parentElement;
|
||||
}
|
||||
},
|
||||
onDragleave(event) {
|
||||
if (event.target.parentElement === this.dragElement) {
|
||||
this.hover = false;
|
||||
delete this.dragElement;
|
||||
}
|
||||
},
|
||||
setContextClickState(state) {
|
||||
this.contextClickActive = state;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -8,34 +8,22 @@
|
||||
/>
|
||||
<div
|
||||
class="c-elements-pool__elements"
|
||||
:class="{'is-dragging': isDragging}"
|
||||
>
|
||||
<ul
|
||||
v-if="elements.length > 0"
|
||||
id="inspector-elements-tree"
|
||||
class="c-tree c-elements-pool__tree"
|
||||
>
|
||||
<li
|
||||
<element-item
|
||||
v-for="(element, index) in elements"
|
||||
:key="element.identifier.key"
|
||||
@drop="moveTo(index)"
|
||||
@dragover="allowDrop"
|
||||
>
|
||||
<div
|
||||
class="c-tree__item c-elements-pool__item"
|
||||
draggable="true"
|
||||
@dragstart="moveFrom(index)"
|
||||
>
|
||||
<span
|
||||
v-if="elements.length > 1 && isEditing"
|
||||
class="c-elements-pool__grippy c-grippy c-grippy--vertical-drag"
|
||||
></span>
|
||||
<object-label
|
||||
:domain-object="element"
|
||||
:object-path="[element, parentObject]"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
:index="index"
|
||||
:element-object="element"
|
||||
:parent-object="parentObject"
|
||||
:allow-drop="allowDrop"
|
||||
@dragstart-custom="moveFrom(index)"
|
||||
@drop-custom="moveTo(index)"
|
||||
/>
|
||||
<li
|
||||
class="js-last-place"
|
||||
@drop="moveToIndex(elements.length)"
|
||||
@@ -51,12 +39,12 @@
|
||||
<script>
|
||||
import _ from 'lodash';
|
||||
import Search from '../components/search.vue';
|
||||
import ObjectLabel from '../components/ObjectLabel.vue';
|
||||
import ElementItem from './ElementItem.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
'Search': Search,
|
||||
'ObjectLabel': ObjectLabel
|
||||
'ElementItem': ElementItem
|
||||
},
|
||||
inject: ['openmct'],
|
||||
data() {
|
||||
@@ -65,8 +53,9 @@ export default {
|
||||
isEditing: this.openmct.editor.isEditing(),
|
||||
parentObject: undefined,
|
||||
currentSearch: '',
|
||||
isDragging: false,
|
||||
selection: []
|
||||
selection: [],
|
||||
contextClickTracker: {},
|
||||
allowDrop: false
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
@@ -148,20 +137,15 @@ export default {
|
||||
&& element.name.toLowerCase().search(this.currentSearch) !== -1;
|
||||
});
|
||||
},
|
||||
allowDrop(event) {
|
||||
event.preventDefault();
|
||||
},
|
||||
moveTo(moveToIndex) {
|
||||
this.composition.reorder(this.moveFromIndex, moveToIndex);
|
||||
if (this.allowDrop) {
|
||||
this.composition.reorder(this.moveFromIndex, moveToIndex);
|
||||
this.allowDrop = false;
|
||||
}
|
||||
},
|
||||
moveFrom(index) {
|
||||
this.isDragging = true;
|
||||
this.allowDrop = true;
|
||||
this.moveFromIndex = index;
|
||||
document.addEventListener('dragend', this.hideDragStyling);
|
||||
},
|
||||
hideDragStyling() {
|
||||
this.isDragging = false;
|
||||
document.removeEventListener('dragend', this.hideDragStyling);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -29,7 +29,7 @@
|
||||
handle="before"
|
||||
label="Elements"
|
||||
>
|
||||
<elements />
|
||||
<elements-pool />
|
||||
</pane>
|
||||
</multipane>
|
||||
<multipane
|
||||
@@ -55,7 +55,7 @@
|
||||
<script>
|
||||
import multipane from '../layout/multipane.vue';
|
||||
import pane from '../layout/pane.vue';
|
||||
import Elements from './Elements.vue';
|
||||
import ElementsPool from './ElementsPool.vue';
|
||||
import Location from './Location.vue';
|
||||
import Properties from './Properties.vue';
|
||||
import ObjectName from './ObjectName.vue';
|
||||
@@ -71,7 +71,7 @@ export default {
|
||||
SavedStylesInspectorView,
|
||||
multipane,
|
||||
pane,
|
||||
Elements,
|
||||
ElementsPool,
|
||||
Properties,
|
||||
ObjectName,
|
||||
Location,
|
||||
|
||||
@@ -15,9 +15,6 @@
|
||||
&__elements {
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
&.is-dragging {
|
||||
li { opacity: 0.2; }
|
||||
}
|
||||
}
|
||||
|
||||
.c-grippy {
|
||||
@@ -27,8 +24,16 @@
|
||||
transform: translateY(-2px);
|
||||
width: $d; height: $d;
|
||||
}
|
||||
|
||||
&.is-context-clicked {
|
||||
box-shadow: inset $colorItemTreeSelectedBg 0 0 0 1px;
|
||||
}
|
||||
|
||||
.hover {
|
||||
background-color: $colorItemTreeSelectedBg;
|
||||
}
|
||||
}
|
||||
|
||||
.js-last-place {
|
||||
height: 10px;
|
||||
}
|
||||
}
|
||||
@@ -159,10 +159,14 @@ export default {
|
||||
return this.views.filter(v => v.key === this.viewKey)[0] || {};
|
||||
},
|
||||
views() {
|
||||
if (this.domainObject && (this.openmct.router.started !== true)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this
|
||||
.openmct
|
||||
.objectViews
|
||||
.get(this.domainObject)
|
||||
.get(this.domainObject, this.openmct.router.path)
|
||||
.map((p) => {
|
||||
return {
|
||||
key: p.key,
|
||||
@@ -197,7 +201,7 @@ export default {
|
||||
if (currentViewKey !== undefined) {
|
||||
let currentViewProvider = this.openmct.objectViews.getByProviderKey(currentViewKey);
|
||||
|
||||
return currentViewProvider.canEdit && currentViewProvider.canEdit(this.domainObject);
|
||||
return currentViewProvider.canEdit && currentViewProvider.canEdit(this.domainObject, this.openmct.router.path);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -235,6 +235,12 @@ export default {
|
||||
},
|
||||
watch: {
|
||||
syncTreeNavigation() {
|
||||
// if there is an abort controller, then a search is in progress and will need to be canceled
|
||||
if (this.abortController) {
|
||||
this.abortController.abort();
|
||||
delete this.abortController;
|
||||
}
|
||||
|
||||
this.searchValue = '';
|
||||
|
||||
if (!this.openmct.router.path) {
|
||||
@@ -685,35 +691,55 @@ export default {
|
||||
// clear any previous search results
|
||||
this.searchResultItems = [];
|
||||
|
||||
const promises = this.openmct.objects.search(this.searchValue)
|
||||
// an abort controller will be passed in that will be used
|
||||
// to cancel an active searches if necessary
|
||||
this.abortController = new AbortController();
|
||||
const abortSignal = this.abortController.signal;
|
||||
|
||||
const promises = this.openmct.objects.search(this.searchValue, abortSignal)
|
||||
.map(promise => promise
|
||||
.then(results => this.aggregateSearchResults(results)));
|
||||
.then(results => this.aggregateSearchResults(results, abortSignal)));
|
||||
|
||||
Promise.all(promises).then(() => {
|
||||
this.searchLoading = false;
|
||||
}).catch(reason => {
|
||||
// search aborted
|
||||
}).finally(() => {
|
||||
if (this.abortController) {
|
||||
delete this.abortController;
|
||||
}
|
||||
});
|
||||
},
|
||||
async aggregateSearchResults(results) {
|
||||
async aggregateSearchResults(results, abortSignal) {
|
||||
for (const result of results) {
|
||||
const objectPath = await this.openmct.objects.getOriginalPath(result.identifier);
|
||||
if (!abortSignal.aborted) {
|
||||
const objectPath = await this.openmct.objects.getOriginalPath(result.identifier);
|
||||
|
||||
// removing the item itself, as the path we pass to buildTreeItem is a parent path
|
||||
objectPath.shift();
|
||||
// removing the item itself, as the path we pass to buildTreeItem is a parent path
|
||||
objectPath.shift();
|
||||
|
||||
// if root, remove, we're not using in object path for tree
|
||||
let lastObject = objectPath.length ? objectPath[objectPath.length - 1] : false;
|
||||
if (lastObject && lastObject.type === 'root') {
|
||||
objectPath.pop();
|
||||
// if root, remove, we're not using in object path for tree
|
||||
let lastObject = objectPath.length ? objectPath[objectPath.length - 1] : false;
|
||||
if (lastObject && lastObject.type === 'root') {
|
||||
objectPath.pop();
|
||||
}
|
||||
|
||||
// we reverse the objectPath in the tree, so have to do it here first,
|
||||
// since this one is already in the correct direction
|
||||
let resultObject = this.buildTreeItem(result, objectPath.reverse());
|
||||
|
||||
this.searchResultItems.push(resultObject);
|
||||
}
|
||||
|
||||
// we reverse the objectPath in the tree, so have to do it here first,
|
||||
// since this one is already in the correct direction
|
||||
let resultObject = this.buildTreeItem(result, objectPath.reverse());
|
||||
|
||||
this.searchResultItems.push(resultObject);
|
||||
}
|
||||
},
|
||||
searchTree(value) {
|
||||
// if an abort controller exists, regardless of the value passed in,
|
||||
// there is an active search that should be cancled
|
||||
if (this.abortController) {
|
||||
this.abortController.abort();
|
||||
delete this.abortController;
|
||||
}
|
||||
|
||||
this.searchValue = value;
|
||||
this.searchLoading = true;
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ export default {
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.views = this.openmct.objectViews.get(this.domainObject).map((view) => {
|
||||
this.views = this.openmct.objectViews.get(this.domainObject, this.objectPath).map((view) => {
|
||||
view.callBack = () => {
|
||||
return this.setView(view);
|
||||
};
|
||||
|
||||
@@ -39,10 +39,16 @@ define(['EventEmitter'], function (EventEmitter) {
|
||||
/**
|
||||
* @private for platform-internal use
|
||||
* @param {*} item the object to be viewed
|
||||
* @param {array} objectPath - The current contextual object path of the view object
|
||||
* eg current domainObject is located under MyItems which is under Root
|
||||
* @returns {module:openmct.ViewProvider[]} any providers
|
||||
* which can provide views of this object
|
||||
*/
|
||||
ViewRegistry.prototype.get = function (item) {
|
||||
ViewRegistry.prototype.get = function (item, objectPath) {
|
||||
if (objectPath === undefined) {
|
||||
throw "objectPath must be provided to get applicable views for an object";
|
||||
}
|
||||
|
||||
function byPriority(providerA, providerB) {
|
||||
let priorityA = providerA.priority ? providerA.priority(item) : DEFAULT_VIEW_PRIORITY;
|
||||
let priorityB = providerB.priority ? providerB.priority(item) : DEFAULT_VIEW_PRIORITY;
|
||||
@@ -52,7 +58,7 @@ define(['EventEmitter'], function (EventEmitter) {
|
||||
|
||||
return this.getAllProviders()
|
||||
.filter(function (provider) {
|
||||
return provider.canView(item);
|
||||
return provider.canView(item, objectPath);
|
||||
}).sort(byPriority);
|
||||
};
|
||||
|
||||
@@ -181,6 +187,8 @@ define(['EventEmitter'], function (EventEmitter) {
|
||||
* @memberof module:openmct.ViewProvider#
|
||||
* @param {module:openmct.DomainObject} domainObject the domain object
|
||||
* to be viewed
|
||||
* @param {array} objectPath - The current contextual object path of the view object
|
||||
* eg current domainObject is located under MyItems which is under Root
|
||||
* @returns {boolean} 'true' if the view applies to the provided object,
|
||||
* otherwise 'false'.
|
||||
*/
|
||||
@@ -201,6 +209,8 @@ define(['EventEmitter'], function (EventEmitter) {
|
||||
* @memberof module:openmct.ViewProvider#
|
||||
* @param {module:openmct.DomainObject} domainObject the domain object
|
||||
* to be edited
|
||||
* @param {array} objectPath - The current contextual object path of the view object
|
||||
* eg current domainObject is located under MyItems which is under Root
|
||||
* @returns {boolean} 'true' if the view can be used to edit the provided object,
|
||||
* otherwise 'false'.
|
||||
*/
|
||||
|
||||
@@ -100,13 +100,13 @@ define([
|
||||
|
||||
document.title = browseObject.name; //change document title to current object in main view
|
||||
|
||||
if (currentProvider && currentProvider.canView(browseObject)) {
|
||||
if (currentProvider && currentProvider.canView(browseObject, openmct.router.path)) {
|
||||
viewObject(browseObject, currentProvider);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let defaultProvider = openmct.objectViews.get(browseObject)[0];
|
||||
let defaultProvider = openmct.objectViews.get(browseObject, openmct.router.path)[0];
|
||||
if (defaultProvider) {
|
||||
openmct.router.updateParams({
|
||||
view: defaultProvider.key
|
||||
|
||||
Reference in New Issue
Block a user