mirror of
https://github.com/nasa/openmct.git
synced 2024-12-20 13:43:09 +00:00
initial structure
This commit is contained in:
parent
d74e1b19b6
commit
3159de08b1
@ -234,6 +234,7 @@
|
||||
openmct.install(openmct.plugins.Timelist());
|
||||
openmct.install(openmct.plugins.BarChart());
|
||||
openmct.install(openmct.plugins.ScatterPlot());
|
||||
openmct.install(openmct.plugins.EventTimestripPlugin());
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
openmct.start();
|
||||
});
|
||||
|
93
src/plugins/events/EventTimestripViewProvider.js
Normal file
93
src/plugins/events/EventTimestripViewProvider.js
Normal file
@ -0,0 +1,93 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import mount from 'utils/mount';
|
||||
|
||||
import EventTimeView from './components/EventTimeView.vue';
|
||||
|
||||
export default function EventTimestripViewProvider(openmct) {
|
||||
const type = 'event.time-strip.view';
|
||||
|
||||
function hasEventTelemetry(domainObject) {
|
||||
const metadata = openmct.telemetry.getMetadata(domainObject);
|
||||
if (!metadata) {
|
||||
return false;
|
||||
}
|
||||
const hasDomain = metadata.valuesForHints(['domain']).length > 0;
|
||||
const hasNoRange = !metadata.valuesForHints(['range'])?.length;
|
||||
|
||||
return hasDomain && hasNoRange;
|
||||
}
|
||||
|
||||
return {
|
||||
key: type,
|
||||
name: 'Event Timestrip View',
|
||||
cssClass: 'icon-event',
|
||||
canView: function (domainObject, objectPath) {
|
||||
let isChildOfTimeStrip = objectPath.find((object) => object.type === 'time-strip');
|
||||
|
||||
return (
|
||||
hasEventTelemetry(domainObject) &&
|
||||
isChildOfTimeStrip &&
|
||||
!openmct.router.isNavigatedObject(objectPath)
|
||||
);
|
||||
},
|
||||
view: function (domainObject, objectPath) {
|
||||
let _destroy = null;
|
||||
let component = null;
|
||||
|
||||
return {
|
||||
show: function (element) {
|
||||
const { vNode, destroy } = mount(
|
||||
{
|
||||
el: element,
|
||||
components: {
|
||||
EventTimeView
|
||||
},
|
||||
provide: {
|
||||
openmct: openmct,
|
||||
domainObject: domainObject,
|
||||
objectPath: objectPath
|
||||
},
|
||||
template: '<event-time-view ref="root"></event-time-view>'
|
||||
},
|
||||
{
|
||||
app: openmct.app,
|
||||
element
|
||||
}
|
||||
);
|
||||
_destroy = destroy;
|
||||
component = vNode.componentInstance;
|
||||
},
|
||||
|
||||
destroy: function () {
|
||||
if (_destroy) {
|
||||
_destroy();
|
||||
}
|
||||
},
|
||||
|
||||
getComponent() {
|
||||
return component;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
413
src/plugins/events/components/EventTimeView.vue
Normal file
413
src/plugins/events/components/EventTimeView.vue
Normal file
@ -0,0 +1,413 @@
|
||||
<!--
|
||||
Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
as represented by the Administrator of the National Aeronautics and Space
|
||||
Administration. All rights reserved.
|
||||
|
||||
Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
"License"); you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
License for the specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
Open MCT includes source code licensed under additional open source
|
||||
licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
this source code distribution or the Licensing information page available
|
||||
at runtime from the About dialog for additional information.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div ref="events" class="c-event-tsv c-timeline-holder">
|
||||
<div ref="imageryHolder" class="c-imagery-tsv__contents u-contents"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { scaleLinear, scaleUtc } from 'd3-scale';
|
||||
import _ from 'lodash';
|
||||
import mount from 'utils/mount';
|
||||
|
||||
import SwimLane from '@/ui/components/swim-lane/SwimLane.vue';
|
||||
import { PREVIEW_ACTION_KEY } from '@/ui/preview/PreviewAction.js';
|
||||
|
||||
const PADDING = 1;
|
||||
const ROW_HEIGHT = 100;
|
||||
const IMAGE_SIZE = 85;
|
||||
const IMAGE_WIDTH_THRESHOLD = 25;
|
||||
const CONTAINER_CLASS = 'c-imagery-tsv-container';
|
||||
const NO_ITEMS_CLASS = 'c-imagery-tsv__no-items';
|
||||
const IMAGE_WRAPPER_CLASS = 'c-imagery-tsv__image-wrapper';
|
||||
const ID_PREFIX = 'wrapper-';
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject', 'objectPath'],
|
||||
data() {
|
||||
let timeSystem = this.openmct.time.getTimeSystem();
|
||||
this.metadata = {};
|
||||
this.requestCount = 0;
|
||||
|
||||
return {
|
||||
viewBounds: undefined,
|
||||
height: 0,
|
||||
durationFormatter: undefined,
|
||||
imageHistory: [],
|
||||
timeSystem: timeSystem,
|
||||
keyString: undefined
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
imageHistory: {
|
||||
handler(newHistory, oldHistory) {
|
||||
this.updatePlotImagery();
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.previewAction = this.openmct.actions.getAction(PREVIEW_ACTION_KEY);
|
||||
|
||||
this.canvas = this.$refs.imagery.appendChild(document.createElement('canvas'));
|
||||
this.canvas.height = 0;
|
||||
this.canvasContext = this.canvas.getContext('2d');
|
||||
this.setDimensions();
|
||||
|
||||
this.setScaleAndPlotImagery = this.setScaleAndPlotImagery.bind(this);
|
||||
this.updateViewBounds = this.updateViewBounds.bind(this);
|
||||
this.setTimeContext = this.setTimeContext.bind(this);
|
||||
this.setTimeContext();
|
||||
|
||||
this.updateViewBounds();
|
||||
|
||||
this.resize = _.debounce(this.resize, 400);
|
||||
this.imageryStripResizeObserver = new ResizeObserver(this.resize);
|
||||
this.imageryStripResizeObserver.observe(this.$refs.imagery);
|
||||
|
||||
this.unlisten = this.openmct.objects.observe(this.domainObject, '*', this.observeForChanges);
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (this.imageryStripResizeObserver) {
|
||||
this.imageryStripResizeObserver.disconnect();
|
||||
}
|
||||
|
||||
this.stopFollowingTimeContext();
|
||||
if (this.unlisten) {
|
||||
this.unlisten();
|
||||
}
|
||||
if (this.destroyImageryContainer) {
|
||||
this.destroyImageryContainer();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setTimeContext() {
|
||||
this.stopFollowingTimeContext();
|
||||
this.timeContext = this.openmct.time.getContextForView(this.objectPath);
|
||||
this.timeContext.on('timeSystem', this.setScaleAndPlotImagery);
|
||||
this.timeContext.on('boundsChanged', this.updateViewBounds);
|
||||
},
|
||||
stopFollowingTimeContext() {
|
||||
if (this.timeContext) {
|
||||
this.timeContext.off('timeSystem', this.setScaleAndPlotImagery);
|
||||
this.timeContext.off('boundsChanged', this.updateViewBounds);
|
||||
}
|
||||
},
|
||||
expand(imageTimestamp) {
|
||||
const path = this.objectPath[0];
|
||||
this.previewAction.invoke([path], {
|
||||
timestamp: imageTimestamp,
|
||||
objectPath: this.objectPath
|
||||
});
|
||||
},
|
||||
observeForChanges(mutatedObject) {
|
||||
this.updateViewBounds();
|
||||
},
|
||||
resize() {
|
||||
let clientWidth = this.getClientWidth();
|
||||
if (clientWidth !== this.width) {
|
||||
this.setDimensions();
|
||||
this.updateViewBounds();
|
||||
}
|
||||
},
|
||||
getClientWidth() {
|
||||
let clientWidth = this.$refs.imagery.clientWidth;
|
||||
|
||||
if (!clientWidth) {
|
||||
//this is a hack - need a better way to find the parent of this component
|
||||
let parent = this.openmct.layout.$refs.browseObject.$el;
|
||||
if (parent) {
|
||||
clientWidth = parent.getBoundingClientRect().width;
|
||||
}
|
||||
}
|
||||
|
||||
return clientWidth;
|
||||
},
|
||||
updateViewBounds(bounds, isTick) {
|
||||
this.viewBounds = this.timeContext.getBounds();
|
||||
|
||||
if (this.timeSystem === undefined) {
|
||||
this.timeSystem = this.timeContext.getTimeSystem();
|
||||
}
|
||||
|
||||
this.setScaleAndPlotImagery(this.timeSystem, !isTick);
|
||||
},
|
||||
setScaleAndPlotImagery(timeSystem, clearAllImagery) {
|
||||
if (timeSystem !== undefined) {
|
||||
this.timeSystem = timeSystem;
|
||||
this.timeFormatter = this.getFormatter(this.timeSystem.key);
|
||||
}
|
||||
|
||||
this.setScale(this.timeSystem);
|
||||
this.updatePlotImagery(clearAllImagery);
|
||||
},
|
||||
getFormatter(key) {
|
||||
const metadata = this.openmct.telemetry.getMetadata(this.domainObject);
|
||||
|
||||
let metadataValue = metadata.value(key) || { format: key };
|
||||
let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
|
||||
|
||||
return valueFormatter;
|
||||
},
|
||||
updatePlotImagery(clearAllImagery) {
|
||||
this.clearPreviousImagery(clearAllImagery);
|
||||
if (this.xScale) {
|
||||
this.drawImagery();
|
||||
}
|
||||
},
|
||||
clearPreviousImagery(clearAllImagery) {
|
||||
//TODO: Only clear items that are out of bounds
|
||||
let noItemsEl = this.$el.querySelectorAll(`.${NO_ITEMS_CLASS}`);
|
||||
noItemsEl.forEach((item) => {
|
||||
item.remove();
|
||||
});
|
||||
let imagery = this.$el.querySelectorAll(`.${IMAGE_WRAPPER_CLASS}`);
|
||||
imagery.forEach((imageElm) => {
|
||||
if (clearAllImagery) {
|
||||
imageElm.remove();
|
||||
} else {
|
||||
const id = imageElm.getAttributeNS(null, 'id');
|
||||
if (id) {
|
||||
const timestamp = id.replace(ID_PREFIX, '');
|
||||
if (
|
||||
!this.isImageryInBounds({
|
||||
time: timestamp
|
||||
})
|
||||
) {
|
||||
imageElm.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
setDimensions() {
|
||||
const imageryHolder = this.$refs.imagery;
|
||||
this.width = this.getClientWidth();
|
||||
|
||||
this.height = Math.round(imageryHolder.getBoundingClientRect().height);
|
||||
},
|
||||
setScale(timeSystem) {
|
||||
if (!this.width) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (timeSystem === undefined) {
|
||||
timeSystem = this.timeContext.getTimeSystem();
|
||||
}
|
||||
|
||||
if (timeSystem.isUTCBased) {
|
||||
this.xScale = scaleUtc();
|
||||
this.xScale.domain([new Date(this.viewBounds.start), new Date(this.viewBounds.end)]);
|
||||
} else {
|
||||
this.xScale = scaleLinear();
|
||||
this.xScale.domain([this.viewBounds.start, this.viewBounds.end]);
|
||||
}
|
||||
|
||||
this.xScale.range([PADDING, this.width - PADDING * 2]);
|
||||
},
|
||||
isImageryInBounds(imageObj) {
|
||||
return imageObj.time <= this.viewBounds.end && imageObj.time >= this.viewBounds.start;
|
||||
},
|
||||
getImageryContainer() {
|
||||
let containerHeight = 100;
|
||||
let containerWidth = this.imageHistory.length ? this.width : 200;
|
||||
let imageryContainer;
|
||||
|
||||
let existingContainer = this.$el.querySelector(`.${CONTAINER_CLASS}`);
|
||||
if (existingContainer) {
|
||||
imageryContainer = existingContainer;
|
||||
imageryContainer.style.maxWidth = `${containerWidth}px`;
|
||||
} else {
|
||||
if (this.destroyImageryContainer) {
|
||||
this.destroyImageryContainer();
|
||||
}
|
||||
const { vNode, destroy } = mount(
|
||||
{
|
||||
components: {
|
||||
SwimLane
|
||||
},
|
||||
provide: {
|
||||
openmct: this.openmct
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isNested: true
|
||||
};
|
||||
},
|
||||
template: `<swim-lane :is-nested="isNested" :hide-label="true"><template v-slot:object><div class="c-imagery-tsv-container"></div></template></swim-lane>`
|
||||
},
|
||||
{
|
||||
app: this.openmct.app
|
||||
}
|
||||
);
|
||||
|
||||
this.destroyImageryContainer = destroy;
|
||||
const component = vNode.componentInstance;
|
||||
this.$refs.imageryHolder.appendChild(component.$el);
|
||||
|
||||
imageryContainer = component.$el.querySelector(`.${CONTAINER_CLASS}`);
|
||||
imageryContainer.style.maxWidth = `${containerWidth}px`;
|
||||
imageryContainer.style.height = `${containerHeight}px`;
|
||||
}
|
||||
|
||||
return imageryContainer;
|
||||
},
|
||||
isImageryWidthAcceptable() {
|
||||
// We're calculating if there is enough space between images to show the thumbnails.
|
||||
// This algorithm could probably be enhanced to check the x co-ordinate distance between 2 consecutive images, but
|
||||
// we will go with this for now assuming imagery is not sorted by asc time so it's difficult to calculate.
|
||||
// TODO: Use telemetry.requestCollection to get sorted telemetry
|
||||
const currentStart = this.viewBounds.start;
|
||||
const currentEnd = this.viewBounds.end;
|
||||
const rectX = this.xScale(currentStart);
|
||||
const rectY = this.xScale(currentEnd);
|
||||
const imageContainerWidth = this.imageHistory.length
|
||||
? (rectY - rectX) / this.imageHistory.length
|
||||
: 0;
|
||||
|
||||
return imageContainerWidth < IMAGE_WIDTH_THRESHOLD;
|
||||
},
|
||||
drawImagery() {
|
||||
let imageryContainer = this.getImageryContainer();
|
||||
const showImagePlaceholders = this.isImageryWidthAcceptable();
|
||||
let index = 0;
|
||||
if (this.imageHistory.length) {
|
||||
this.imageHistory.forEach((currentImageObject) => {
|
||||
if (this.isImageryInBounds(currentImageObject)) {
|
||||
this.plotImagery(currentImageObject, showImagePlaceholders, imageryContainer, index);
|
||||
index = index + 1;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.plotNoItems(imageryContainer);
|
||||
}
|
||||
},
|
||||
plotNoItems(containerElement) {
|
||||
let textElement = document.createElement('text');
|
||||
textElement.classList.add(NO_ITEMS_CLASS);
|
||||
textElement.innerHTML = 'No images within timeframe';
|
||||
|
||||
containerElement.appendChild(textElement);
|
||||
},
|
||||
setNSAttributesForElement(element, attributes) {
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object.keys(attributes).forEach((key) => {
|
||||
element.setAttributeNS(null, key, attributes[key]);
|
||||
});
|
||||
},
|
||||
setStyles(element, styles) {
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object.keys(styles).forEach((key) => {
|
||||
element.style[key] = styles[key];
|
||||
});
|
||||
},
|
||||
getImageWrapper(item) {
|
||||
const id = `${ID_PREFIX}${item.time}`;
|
||||
|
||||
return this.$el.querySelector(`.c-imagery-tsv__contents div[id=${id}]`);
|
||||
},
|
||||
plotImagery(item, showImagePlaceholders, containerElement, index) {
|
||||
let existingImageWrapper = this.getImageWrapper(item);
|
||||
//imageWrapper wraps the vertical tick and the image
|
||||
if (existingImageWrapper) {
|
||||
this.updateExistingImageWrapper(existingImageWrapper, item, showImagePlaceholders);
|
||||
} else {
|
||||
let imageWrapper = this.createImageWrapper(index, item, showImagePlaceholders);
|
||||
containerElement.appendChild(imageWrapper);
|
||||
}
|
||||
},
|
||||
setImageDisplay(imageElement, showImagePlaceholders) {
|
||||
if (showImagePlaceholders) {
|
||||
imageElement.style.display = 'none';
|
||||
} else {
|
||||
imageElement.style.display = 'block';
|
||||
}
|
||||
},
|
||||
updateExistingImageWrapper(existingImageWrapper, image, showImagePlaceholders) {
|
||||
//Update the x co-ordinates of the image wrapper and the url of image
|
||||
//this is to avoid tearing down all elements completely and re-drawing them
|
||||
this.setNSAttributesForElement(existingImageWrapper, {
|
||||
'data-show-image-placeholders': showImagePlaceholders
|
||||
});
|
||||
existingImageWrapper.style.left = `${this.xScale(image.time)}px`;
|
||||
|
||||
let imageElement = existingImageWrapper.querySelector('img');
|
||||
this.setNSAttributesForElement(imageElement, {
|
||||
src: image.thumbnailUrl || image.url
|
||||
});
|
||||
this.setImageDisplay(imageElement, showImagePlaceholders);
|
||||
},
|
||||
createImageWrapper(index, image, showImagePlaceholders) {
|
||||
const id = `${ID_PREFIX}${image.time}`;
|
||||
let imageWrapper = document.createElement('div');
|
||||
imageWrapper.ariaLabel = id;
|
||||
imageWrapper.classList.add(IMAGE_WRAPPER_CLASS);
|
||||
imageWrapper.style.left = `${this.xScale(image.time)}px`;
|
||||
this.setNSAttributesForElement(imageWrapper, {
|
||||
id,
|
||||
'data-show-image-placeholders': showImagePlaceholders
|
||||
});
|
||||
//create image vertical tick indicator
|
||||
let imageTickElement = document.createElement('div');
|
||||
imageTickElement.classList.add('c-imagery-tsv__image-handle');
|
||||
imageTickElement.style.width = '2px';
|
||||
imageTickElement.style.height = `${String(ROW_HEIGHT - 10)}px`;
|
||||
imageWrapper.appendChild(imageTickElement);
|
||||
|
||||
//create placeholder - this will also hold the actual image
|
||||
let imagePlaceholder = document.createElement('div');
|
||||
imagePlaceholder.classList.add('c-imagery-tsv__image-placeholder');
|
||||
imagePlaceholder.style.width = `${IMAGE_SIZE}px`;
|
||||
imagePlaceholder.style.height = `${IMAGE_SIZE}px`;
|
||||
imageWrapper.appendChild(imagePlaceholder);
|
||||
|
||||
//create image element
|
||||
let imageElement = document.createElement('img');
|
||||
this.setNSAttributesForElement(imageElement, {
|
||||
src: image.thumbnailUrl || image.url
|
||||
});
|
||||
imageElement.style.width = `${IMAGE_SIZE}px`;
|
||||
imageElement.style.height = `${IMAGE_SIZE}px`;
|
||||
this.setImageDisplay(imageElement, showImagePlaceholders);
|
||||
|
||||
//handle mousedown event to show the image in a large view
|
||||
imageWrapper.addEventListener('mousedown', (e) => {
|
||||
if (e.button === 0) {
|
||||
this.expand(image.time);
|
||||
}
|
||||
});
|
||||
|
||||
imagePlaceholder.appendChild(imageElement);
|
||||
|
||||
return imageWrapper;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
29
src/plugins/events/plugin.js
Normal file
29
src/plugins/events/plugin.js
Normal file
@ -0,0 +1,29 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import EventTimestripViewProvider from './EventTimestripViewProvider.js';
|
||||
|
||||
export default function (options) {
|
||||
return function install(openmct) {
|
||||
openmct.objectViews.addProvider(new EventTimestripViewProvider(openmct));
|
||||
};
|
||||
}
|
@ -38,6 +38,7 @@ import CouchDBSearchFolder from './CouchDBSearchFolder/plugin.js';
|
||||
import DefaultRootName from './defaultRootName/plugin.js';
|
||||
import DeviceClassifier from './DeviceClassifier/plugin.js';
|
||||
import DisplayLayoutPlugin from './displayLayout/plugin.js';
|
||||
import EventTimestripPlugin from './events/plugin.js';
|
||||
import FaultManagementPlugin from './faultManagement/FaultManagementPlugin.js';
|
||||
import Filters from './filters/plugin.js';
|
||||
import FlexibleLayout from './flexibleLayout/plugin.js';
|
||||
@ -176,5 +177,6 @@ plugins.Gauge = GaugePlugin;
|
||||
plugins.Timelist = TimeList;
|
||||
plugins.InspectorViews = InspectorViews;
|
||||
plugins.InspectorDataVisualization = InspectorDataVisualization;
|
||||
plugins.EventTimestripPlugin = EventTimestripPlugin;
|
||||
|
||||
export default plugins;
|
||||
|
Loading…
Reference in New Issue
Block a user