mirror of
https://github.com/nasa/openmct.git
synced 2025-01-29 15:43:52 +00:00
Request batch when idle (#7526)
See https://github.com/nasa/openmct/pull/7526 for details
This commit is contained in:
parent
8c2558bfe0
commit
5f0bd10c61
@ -272,7 +272,7 @@ test.describe('Display Layout', () => {
|
||||
expect(trimmedDisplayValue).toBe(formattedTelemetryValue);
|
||||
|
||||
// ensure we can right click on the alpha-numeric widget and view historical data
|
||||
await page.getByLabel('Sine', { exact: true }).click({
|
||||
await page.getByLabel(/Alpha-numeric telemetry value of.*/).click({
|
||||
button: 'right'
|
||||
});
|
||||
await page.getByLabel('View Historical Data').click();
|
||||
|
@ -54,7 +54,9 @@ test.describe('Plots work in Previews', () => {
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
// right click on the plot and select view large
|
||||
await page.getByLabel('Sine', { exact: true }).click({ button: 'right' });
|
||||
await page.getByLabel(/Alpha-numeric telemetry value of.*/).click({
|
||||
button: 'right'
|
||||
});
|
||||
await page.getByLabel('View Historical Data').click();
|
||||
await expect(page.getByLabel('Preview Container').getByLabel('Plot Canvas')).toBeVisible();
|
||||
await page.getByRole('button', { name: 'Close' }).click();
|
||||
|
@ -109,7 +109,7 @@ test.describe('Verify tooltips', () => {
|
||||
|
||||
async function getToolTip(object) {
|
||||
await page.locator('.c-create-button').hover();
|
||||
await page.getByRole('cell', { name: object.name }).hover();
|
||||
await page.getByLabel('lad name').getByText(object.name).hover();
|
||||
let tooltipText = await page.locator('.c-tooltip').textContent();
|
||||
return tooltipText.replace('\n', '').trim();
|
||||
}
|
||||
|
@ -20,7 +20,6 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import installWorker from './WebSocketWorker.js';
|
||||
const DEFAULT_RATE_MS = 1000;
|
||||
/**
|
||||
* Describes the strategy to be used when batching WebSocket messages
|
||||
*
|
||||
@ -51,11 +50,21 @@ const DEFAULT_RATE_MS = 1000;
|
||||
*
|
||||
* @memberof module:openmct.telemetry
|
||||
*/
|
||||
// Shim for Internet Explorer, I mean Safari. It doesn't support requestIdleCallback, but it's in a tech preview, so it will be dropping soon.
|
||||
const requestIdleCallback =
|
||||
// eslint-disable-next-line compat/compat
|
||||
window.requestIdleCallback ?? ((fn, { timeout }) => setTimeout(fn, timeout));
|
||||
const ONE_SECOND = 1000;
|
||||
const FIVE_SECONDS = 5 * ONE_SECOND;
|
||||
|
||||
class BatchingWebSocket extends EventTarget {
|
||||
#worker;
|
||||
#openmct;
|
||||
#showingRateLimitNotification;
|
||||
#rate;
|
||||
#maxBatchSize;
|
||||
#applicationIsInitializing;
|
||||
#maxBatchWait;
|
||||
#firstBatchReceived;
|
||||
|
||||
constructor(openmct) {
|
||||
super();
|
||||
@ -66,7 +75,10 @@ class BatchingWebSocket extends EventTarget {
|
||||
this.#worker = new Worker(workerUrl);
|
||||
this.#openmct = openmct;
|
||||
this.#showingRateLimitNotification = false;
|
||||
this.#rate = DEFAULT_RATE_MS;
|
||||
this.#maxBatchSize = Number.POSITIVE_INFINITY;
|
||||
this.#maxBatchWait = ONE_SECOND;
|
||||
this.#applicationIsInitializing = true;
|
||||
this.#firstBatchReceived = false;
|
||||
|
||||
const routeMessageToHandler = this.#routeMessageToHandler.bind(this);
|
||||
this.#worker.addEventListener('message', routeMessageToHandler);
|
||||
@ -78,6 +90,20 @@ class BatchingWebSocket extends EventTarget {
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
|
||||
openmct.once('start', () => {
|
||||
// An idle callback is a pretty good indication that a complex display is done loading. At that point set the batch size more conservatively.
|
||||
// Force it after 5 seconds if it hasn't happened yet.
|
||||
requestIdleCallback(
|
||||
() => {
|
||||
this.#applicationIsInitializing = false;
|
||||
this.setMaxBatchSize(this.#maxBatchSize);
|
||||
},
|
||||
{
|
||||
timeout: FIVE_SECONDS
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -129,14 +155,6 @@ class BatchingWebSocket extends EventTarget {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* When using batching, sets the rate at which batches of messages are released.
|
||||
* @param {Number} rate the amount of time to wait, in ms, between batches.
|
||||
*/
|
||||
setRate(rate) {
|
||||
this.#rate = rate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Number} maxBatchSize the maximum length of a batch of messages. For example,
|
||||
* the maximum number of telemetry values to batch before dropping them
|
||||
@ -151,12 +169,29 @@ class BatchingWebSocket extends EventTarget {
|
||||
* 15 would probably be a better batch size.
|
||||
*/
|
||||
setMaxBatchSize(maxBatchSize) {
|
||||
this.#maxBatchSize = maxBatchSize;
|
||||
if (!this.#applicationIsInitializing) {
|
||||
this.#sendMaxBatchSizeToWorker(this.#maxBatchSize);
|
||||
}
|
||||
}
|
||||
setMaxBatchWait(wait) {
|
||||
this.#maxBatchWait = wait;
|
||||
this.#sendBatchWaitToWorker(this.#maxBatchWait);
|
||||
}
|
||||
#sendMaxBatchSizeToWorker(maxBatchSize) {
|
||||
this.#worker.postMessage({
|
||||
type: 'setMaxBatchSize',
|
||||
maxBatchSize
|
||||
});
|
||||
}
|
||||
|
||||
#sendBatchWaitToWorker(maxBatchWait) {
|
||||
this.#worker.postMessage({
|
||||
type: 'setMaxBatchWait',
|
||||
maxBatchWait
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect the associated WebSocket. Generally speaking there is no need to call
|
||||
* this manually.
|
||||
@ -169,7 +204,9 @@ class BatchingWebSocket extends EventTarget {
|
||||
|
||||
#routeMessageToHandler(message) {
|
||||
if (message.data.type === 'batch') {
|
||||
if (message.data.batch.dropped === true && !this.#showingRateLimitNotification) {
|
||||
this.start = Date.now();
|
||||
const batch = message.data.batch;
|
||||
if (batch.dropped === true && !this.#showingRateLimitNotification) {
|
||||
const notification = this.#openmct.notifications.alert(
|
||||
'Telemetry dropped due to client rate limiting.',
|
||||
{ hint: 'Refresh individual telemetry views to retrieve dropped telemetry if needed.' }
|
||||
@ -179,16 +216,45 @@ class BatchingWebSocket extends EventTarget {
|
||||
this.#showingRateLimitNotification = false;
|
||||
});
|
||||
}
|
||||
this.dispatchEvent(new CustomEvent('batch', { detail: message.data.batch }));
|
||||
setTimeout(() => {
|
||||
this.#readyForNextBatch();
|
||||
}, this.#rate);
|
||||
|
||||
this.dispatchEvent(new CustomEvent('batch', { detail: batch }));
|
||||
this.#waitUntilIdleAndRequestNextBatch(batch);
|
||||
} else if (message.data.type === 'message') {
|
||||
this.dispatchEvent(new CustomEvent('message', { detail: message.data.message }));
|
||||
} else if (message.data.type === 'reconnected') {
|
||||
this.dispatchEvent(new CustomEvent('reconnected'));
|
||||
} else {
|
||||
throw new Error(`Unknown message type: ${message.data.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
#waitUntilIdleAndRequestNextBatch(batch) {
|
||||
requestIdleCallback(
|
||||
(state) => {
|
||||
if (this.#firstBatchReceived === false) {
|
||||
this.#firstBatchReceived = true;
|
||||
}
|
||||
const now = Date.now();
|
||||
const waitedFor = now - this.start;
|
||||
if (state.didTimeout === true) {
|
||||
if (document.visibilityState === 'visible') {
|
||||
console.warn(`Event loop is too busy to process batch.`);
|
||||
this.#waitUntilIdleAndRequestNextBatch(batch);
|
||||
} else {
|
||||
// After ingesting a telemetry batch, wait until the event loop is idle again before
|
||||
// informing the worker we are ready for another batch.
|
||||
this.#readyForNextBatch();
|
||||
}
|
||||
} else {
|
||||
if (waitedFor > ONE_SECOND) {
|
||||
console.warn(`Warning, batch processing took ${waitedFor}ms`);
|
||||
}
|
||||
this.#readyForNextBatch();
|
||||
}
|
||||
},
|
||||
{ timeout: ONE_SECOND }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default BatchingWebSocket;
|
||||
|
@ -85,6 +85,7 @@ const SUBSCRIBE_STRATEGY = {
|
||||
export default class TelemetryAPI {
|
||||
#isGreedyLAD;
|
||||
#subscribeCache;
|
||||
#hasReturnedFirstData;
|
||||
|
||||
get SUBSCRIBE_STRATEGY() {
|
||||
return SUBSCRIBE_STRATEGY;
|
||||
@ -108,6 +109,7 @@ export default class TelemetryAPI {
|
||||
this.#isGreedyLAD = true;
|
||||
this.BatchingWebSocket = BatchingWebSocket;
|
||||
this.#subscribeCache = {};
|
||||
this.#hasReturnedFirstData = false;
|
||||
}
|
||||
|
||||
abortAllRequests() {
|
||||
@ -383,7 +385,10 @@ export default class TelemetryAPI {
|
||||
arguments[1] = await this.applyRequestInterceptors(domainObject, arguments[1]);
|
||||
try {
|
||||
const telemetry = await provider.request(...arguments);
|
||||
|
||||
if (!this.#hasReturnedFirstData) {
|
||||
this.#hasReturnedFirstData = true;
|
||||
performance.mark('firstHistoricalDataReturned');
|
||||
}
|
||||
return telemetry;
|
||||
} catch (error) {
|
||||
if (error.name !== 'AbortError') {
|
||||
|
@ -21,6 +21,7 @@
|
||||
*****************************************************************************/
|
||||
/* eslint-disable max-classes-per-file */
|
||||
export default function installWorker() {
|
||||
const ONE_SECOND = 1000;
|
||||
const FALLBACK_AND_WAIT_MS = [1000, 5000, 5000, 10000, 10000, 30000];
|
||||
|
||||
/**
|
||||
@ -44,6 +45,13 @@ export default function installWorker() {
|
||||
#currentWaitIndex = 0;
|
||||
#messageCallbacks = [];
|
||||
#wsUrl;
|
||||
#reconnecting = false;
|
||||
#worker;
|
||||
|
||||
constructor(worker) {
|
||||
super();
|
||||
this.#worker = worker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Establish a new WebSocket connection to the given URL
|
||||
@ -62,6 +70,9 @@ export default function installWorker() {
|
||||
this.#isConnecting = true;
|
||||
|
||||
this.#webSocket = new WebSocket(url);
|
||||
//Exposed to e2e tests so that the websocket can be manipulated during tests. Cannot find any other way to do this.
|
||||
// Playwright does not support forcing websocket state changes.
|
||||
this.#worker.currentWebSocket = this.#webSocket;
|
||||
|
||||
const boundConnected = this.#connected.bind(this);
|
||||
this.#webSocket.addEventListener('open', boundConnected);
|
||||
@ -100,12 +111,17 @@ export default function installWorker() {
|
||||
}
|
||||
|
||||
#connected() {
|
||||
console.debug('Websocket connected.');
|
||||
console.info('Websocket connected.');
|
||||
this.#isConnected = true;
|
||||
this.#isConnecting = false;
|
||||
this.#currentWaitIndex = 0;
|
||||
|
||||
this.dispatchEvent(new Event('connected'));
|
||||
if (this.#reconnecting) {
|
||||
this.#worker.postMessage({
|
||||
type: 'reconnected'
|
||||
});
|
||||
this.#reconnecting = false;
|
||||
}
|
||||
|
||||
this.#flushQueue();
|
||||
}
|
||||
@ -138,6 +154,7 @@ export default function installWorker() {
|
||||
if (this.#reconnectTimeoutHandle) {
|
||||
return;
|
||||
}
|
||||
this.#reconnecting = true;
|
||||
|
||||
this.#reconnectTimeoutHandle = setTimeout(() => {
|
||||
this.connect(this.#wsUrl);
|
||||
@ -207,6 +224,9 @@ export default function installWorker() {
|
||||
case 'setMaxBatchSize':
|
||||
this.#messageBatcher.setMaxBatchSize(message.data.maxBatchSize);
|
||||
break;
|
||||
case 'setMaxBatchWait':
|
||||
this.#messageBatcher.setMaxBatchWait(message.data.maxBatchWait);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown message type: ${type}`);
|
||||
}
|
||||
@ -245,7 +265,6 @@ export default function installWorker() {
|
||||
}
|
||||
|
||||
routeMessageToHandler(data) {
|
||||
//Implement batching here
|
||||
if (this.#messageBatcher.shouldBatchMessage(data)) {
|
||||
this.#messageBatcher.addMessageToBatch(data);
|
||||
} else {
|
||||
@ -267,12 +286,15 @@ export default function installWorker() {
|
||||
#maxBatchSize;
|
||||
#readyForNextBatch;
|
||||
#worker;
|
||||
#throttledSendNextBatch;
|
||||
|
||||
constructor(worker) {
|
||||
this.#maxBatchSize = 10;
|
||||
// No dropping telemetry unless we're explicitly told to.
|
||||
this.#maxBatchSize = Number.POSITIVE_INFINITY;
|
||||
this.#readyForNextBatch = false;
|
||||
this.#worker = worker;
|
||||
this.#resetBatch();
|
||||
this.setMaxBatchWait(ONE_SECOND);
|
||||
}
|
||||
#resetBatch() {
|
||||
this.#batch = {};
|
||||
@ -310,23 +332,29 @@ export default function installWorker() {
|
||||
const batchId = this.#batchingStrategy.getBatchIdFromMessage(message);
|
||||
let batch = this.#batch[batchId];
|
||||
if (batch === undefined) {
|
||||
this.#hasBatch = true;
|
||||
batch = this.#batch[batchId] = [message];
|
||||
} else {
|
||||
batch.push(message);
|
||||
}
|
||||
if (batch.length > this.#maxBatchSize) {
|
||||
console.warn(
|
||||
`Exceeded max batch size of ${this.#maxBatchSize} for ${batchId}. Dropping value.`
|
||||
);
|
||||
batch.shift();
|
||||
this.#batch.dropped = this.#batch.dropped || true;
|
||||
this.#batch.dropped = true;
|
||||
}
|
||||
|
||||
if (this.#readyForNextBatch) {
|
||||
this.#sendNextBatch();
|
||||
} else {
|
||||
this.#hasBatch = true;
|
||||
this.#throttledSendNextBatch();
|
||||
}
|
||||
}
|
||||
setMaxBatchSize(maxBatchSize) {
|
||||
this.#maxBatchSize = maxBatchSize;
|
||||
}
|
||||
setMaxBatchWait(maxBatchWait) {
|
||||
this.#throttledSendNextBatch = throttle(this.#sendNextBatch.bind(this), maxBatchWait);
|
||||
}
|
||||
/**
|
||||
* Indicates that client code is ready to receive the next batch of
|
||||
* messages. If a batch is available, it will be immediately sent.
|
||||
@ -335,7 +363,7 @@ export default function installWorker() {
|
||||
*/
|
||||
readyForNextBatch() {
|
||||
if (this.#hasBatch) {
|
||||
this.#sendNextBatch();
|
||||
this.#throttledSendNextBatch();
|
||||
} else {
|
||||
this.#readyForNextBatch = true;
|
||||
}
|
||||
@ -352,7 +380,34 @@ export default function installWorker() {
|
||||
}
|
||||
}
|
||||
|
||||
const websocket = new ResilientWebSocket();
|
||||
function throttle(callback, wait) {
|
||||
let last = 0;
|
||||
let throttling = false;
|
||||
|
||||
return function (...args) {
|
||||
if (throttling) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = performance.now();
|
||||
const timeSinceLast = now - last;
|
||||
|
||||
if (timeSinceLast >= wait) {
|
||||
last = now;
|
||||
callback(...args);
|
||||
} else if (!throttling) {
|
||||
throttling = true;
|
||||
|
||||
setTimeout(() => {
|
||||
last = performance.now();
|
||||
throttling = false;
|
||||
callback(...args);
|
||||
}, wait - timeSinceLast);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const websocket = new ResilientWebSocket(self);
|
||||
const messageBatcher = new MessageBatcher(self);
|
||||
const workerBroker = new WorkerToWebSocketMessageBroker(websocket, messageBatcher);
|
||||
const websocketBroker = new WebSocketToWorkerMessageBroker(messageBatcher, self);
|
||||
@ -363,4 +418,6 @@ export default function installWorker() {
|
||||
websocket.registerMessageCallback((data) => {
|
||||
websocketBroker.routeMessageToHandler(data);
|
||||
});
|
||||
|
||||
self.websocketInstance = websocket;
|
||||
}
|
||||
|
@ -24,11 +24,13 @@
|
||||
<tr
|
||||
ref="tableRow"
|
||||
class="js-lad-table__body__row c-table__selectable-row"
|
||||
aria-label="lad row"
|
||||
@click="clickedRow"
|
||||
@contextmenu.prevent="showContextMenu"
|
||||
>
|
||||
<td
|
||||
ref="tableCell"
|
||||
aria-label="lad name"
|
||||
class="js-first-data"
|
||||
@mouseover.ctrl="showToolTip"
|
||||
@mouseleave="hideToolTip"
|
||||
|
@ -22,7 +22,7 @@
|
||||
|
||||
<template>
|
||||
<div class="c-lad-table-wrapper u-style-receiver js-style-receiver" :class="staleClass">
|
||||
<table class="c-table c-lad-table" :class="applyLayoutClass">
|
||||
<table aria-label="lad table" class="c-table c-lad-table" :class="applyLayoutClass">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
|
@ -22,6 +22,7 @@
|
||||
|
||||
<template>
|
||||
<div
|
||||
aria-label="sub object frame"
|
||||
class="l-layout__frame c-frame"
|
||||
:class="{
|
||||
'no-frame': !item.hasFrame,
|
||||
|
@ -37,24 +37,24 @@
|
||||
:style="styleObject"
|
||||
:data-font-size="item.fontSize"
|
||||
:data-font="item.font"
|
||||
aria-label="Alpha-numeric telemetry"
|
||||
@contextmenu.prevent="showContextMenu"
|
||||
@mouseover.ctrl="showToolTip"
|
||||
@mouseleave="hideToolTip"
|
||||
>
|
||||
<div
|
||||
class="is-status__indicator"
|
||||
:aria-label="`This item is ${status}`"
|
||||
:title="`This item is ${status}`"
|
||||
></div>
|
||||
<div class="is-status__indicator"></div>
|
||||
<div v-if="showLabel" class="c-telemetry-view__label">
|
||||
<div class="c-telemetry-view__label-text">
|
||||
<div
|
||||
class="c-telemetry-view__label-text"
|
||||
:aria-label="`Alpha-numeric telemetry name for ${domainObject.name}`"
|
||||
>
|
||||
{{ domainObject.name }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="showValue"
|
||||
:aria-label="fieldName"
|
||||
:aria-label="`Alpha-numeric telemetry value of ${telemetryValue}`"
|
||||
:title="fieldName"
|
||||
class="c-telemetry-view__value"
|
||||
:class="[telemetryClass]"
|
||||
|
@ -220,6 +220,7 @@
|
||||
lengthAdjust="spacing"
|
||||
text-anchor="middle"
|
||||
dominant-baseline="middle"
|
||||
:aria-label="`gauge value of ${curVal}`"
|
||||
x="50%"
|
||||
y="50%"
|
||||
>
|
||||
|
@ -51,6 +51,6 @@ export default class OpenInNewTab {
|
||||
*/
|
||||
invoke(objectPath, _view, customUrlParams) {
|
||||
const url = objectPathToUrl(this._openmct, objectPath, customUrlParams);
|
||||
window.open(url);
|
||||
window.open(url, undefined, 'noopener');
|
||||
}
|
||||
}
|
||||
|
@ -287,7 +287,7 @@
|
||||
|
||||
<script>
|
||||
import _ from 'lodash';
|
||||
import { toRaw } from 'vue';
|
||||
import { onMounted, ref, toRaw } from 'vue';
|
||||
|
||||
import stalenessMixin from '@/ui/mixins/staleness-mixin';
|
||||
|
||||
@ -295,7 +295,7 @@ import CSVExporter from '../../../exporters/CSVExporter.js';
|
||||
import ProgressBar from '../../../ui/components/ProgressBar.vue';
|
||||
import Search from '../../../ui/components/SearchComponent.vue';
|
||||
import ToggleSwitch from '../../../ui/components/ToggleSwitch.vue';
|
||||
import throttle from '../../../utils/throttle';
|
||||
import { useResizeObserver } from '../../../ui/composables/resize.js';
|
||||
import SizingRow from './SizingRow.vue';
|
||||
import TableColumnHeader from './TableColumnHeader.vue';
|
||||
import TableFooterIndicator from './TableFooterIndicator.vue';
|
||||
@ -303,7 +303,6 @@ import TelemetryTableRow from './TableRow.vue';
|
||||
|
||||
const VISIBLE_ROW_COUNT = 100;
|
||||
const ROW_HEIGHT = 17;
|
||||
const RESIZE_POLL_INTERVAL = 200;
|
||||
const AUTO_SCROLL_TRIGGER_HEIGHT = ROW_HEIGHT * 3;
|
||||
|
||||
export default {
|
||||
@ -354,6 +353,15 @@ export default {
|
||||
}
|
||||
},
|
||||
emits: ['marked-rows-updated', 'filter'],
|
||||
setup() {
|
||||
const root = ref(null);
|
||||
const { size: containerSize, startObserving } = useResizeObserver();
|
||||
onMounted(() => {
|
||||
startObserving(root.value);
|
||||
});
|
||||
|
||||
return { containerSize, root };
|
||||
},
|
||||
data() {
|
||||
let configuration = this.table.configuration.getConfiguration();
|
||||
|
||||
@ -441,6 +449,13 @@ export default {
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
//This should be refactored so that it doesn't require an explicit watch. Should be doable.
|
||||
containerSize: {
|
||||
handler() {
|
||||
this.debouncedRescaleToContainer();
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
loading: {
|
||||
handler(isLoading) {
|
||||
if (isLoading) {
|
||||
@ -500,9 +515,10 @@ export default {
|
||||
this.filterTelemetry = _.debounce(this.filterTelemetry, 500);
|
||||
},
|
||||
mounted() {
|
||||
this.throttledUpdateVisibleRows = _.throttle(this.updateVisibleRows, 1000, { leading: true });
|
||||
this.debouncedRescaleToContainer = _.debounce(this.rescaleToContainer, 300);
|
||||
|
||||
this.csvExporter = new CSVExporter();
|
||||
this.rowsAdded = _.throttle(this.rowsAdded, 200);
|
||||
this.rowsRemoved = _.throttle(this.rowsRemoved, 200);
|
||||
this.scroll = _.throttle(this.scroll, 100);
|
||||
|
||||
if (!this.marking.useAlternateControlBar && !this.enableLegacyToolbar) {
|
||||
@ -515,8 +531,6 @@ export default {
|
||||
});
|
||||
}
|
||||
|
||||
this.updateVisibleRows = throttle(this.updateVisibleRows, 1000);
|
||||
|
||||
this.table.on('object-added', this.addObject);
|
||||
this.table.on('object-removed', this.removeObject);
|
||||
this.table.on('refresh', this.clearRowsAndRerender);
|
||||
@ -526,8 +540,8 @@ export default {
|
||||
|
||||
this.table.tableRows.on('add', this.rowsAdded);
|
||||
this.table.tableRows.on('remove', this.rowsRemoved);
|
||||
this.table.tableRows.on('sort', this.updateVisibleRows);
|
||||
this.table.tableRows.on('filter', this.updateVisibleRows);
|
||||
this.table.tableRows.on('sort', this.throttledUpdateVisibleRows);
|
||||
this.table.tableRows.on('filter', this.throttledUpdateVisibleRows);
|
||||
|
||||
this.openmct.time.on('bounds', this.boundsChanged);
|
||||
|
||||
@ -540,10 +554,10 @@ export default {
|
||||
this.table.configuration.on('change', this.updateConfiguration);
|
||||
|
||||
this.calculateTableSize();
|
||||
this.pollForResize();
|
||||
this.calculateScrollbarWidth();
|
||||
|
||||
this.table.initialize();
|
||||
this.rescaleToContainer();
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.table.off('object-added', this.addObject);
|
||||
@ -555,15 +569,13 @@ export default {
|
||||
|
||||
this.table.tableRows.off('add', this.rowsAdded);
|
||||
this.table.tableRows.off('remove', this.rowsRemoved);
|
||||
this.table.tableRows.off('sort', this.updateVisibleRows);
|
||||
this.table.tableRows.off('filter', this.updateVisibleRows);
|
||||
this.table.tableRows.off('sort', this.throttledUpdateVisibleRows);
|
||||
this.table.tableRows.off('filter', this.throttledUpdateVisibleRows);
|
||||
|
||||
this.table.configuration.off('change', this.updateConfiguration);
|
||||
|
||||
this.openmct.time.off('bounds', this.boundsChanged);
|
||||
|
||||
clearInterval(this.resizePollHandle);
|
||||
|
||||
this.table.configuration.destroy();
|
||||
|
||||
this.table.destroy();
|
||||
@ -684,7 +696,7 @@ export default {
|
||||
this.table.sortBy(this.sortOptions);
|
||||
},
|
||||
scroll() {
|
||||
this.updateVisibleRows();
|
||||
this.throttledUpdateVisibleRows();
|
||||
this.synchronizeScrollX();
|
||||
|
||||
if (this.shouldAutoScroll()) {
|
||||
@ -757,11 +769,11 @@ export default {
|
||||
this.initiateAutoScroll();
|
||||
}
|
||||
|
||||
this.updateVisibleRows();
|
||||
this.throttledUpdateVisibleRows();
|
||||
},
|
||||
rowsRemoved(rows) {
|
||||
this.setHeight();
|
||||
this.updateVisibleRows();
|
||||
this.throttledUpdateVisibleRows();
|
||||
},
|
||||
/**
|
||||
* Calculates height based on total number of rows, and sets table height.
|
||||
@ -880,35 +892,27 @@ export default {
|
||||
dropTargetActive(isActive) {
|
||||
this.isDropTargetActive = isActive;
|
||||
},
|
||||
pollForResize() {
|
||||
let el = this.$refs.root;
|
||||
let width = el.clientWidth;
|
||||
let height = el.clientHeight;
|
||||
rescaleToContainer() {
|
||||
let scrollTop = this.scrollable.scrollTop;
|
||||
|
||||
this.resizePollHandle = setInterval(() => {
|
||||
this.renderWhenVisible(() => {
|
||||
if ((el.clientWidth !== width || el.clientHeight !== height) && this.isAutosizeEnabled) {
|
||||
this.calculateTableSize();
|
||||
// On some resize events scrollTop is reset to 0. Possibly due to a transition we're using?
|
||||
// Need to preserve scroll position in this case.
|
||||
if (this.autoScroll) {
|
||||
this.initiateAutoScroll();
|
||||
} else {
|
||||
this.scrollable.scrollTop = scrollTop;
|
||||
}
|
||||
|
||||
width = el.clientWidth;
|
||||
height = el.clientHeight;
|
||||
this.renderWhenVisible(() => {
|
||||
if (this.isAutosizeEnabled) {
|
||||
this.calculateTableSize();
|
||||
// On some resize events scrollTop is reset to 0. Possibly due to a transition we're using?
|
||||
// Need to preserve scroll position in this case.
|
||||
if (this.autoScroll) {
|
||||
this.initiateAutoScroll();
|
||||
} else {
|
||||
this.scrollable.scrollTop = scrollTop;
|
||||
}
|
||||
}
|
||||
|
||||
scrollTop = this.scrollable.scrollTop;
|
||||
});
|
||||
}, RESIZE_POLL_INTERVAL);
|
||||
scrollTop = this.scrollable.scrollTop;
|
||||
});
|
||||
},
|
||||
clearRowsAndRerender() {
|
||||
this.visibleRows = [];
|
||||
this.$nextTick().then(this.updateVisibleRows);
|
||||
this.$nextTick().then(this.throttledUpdateVisibleRows);
|
||||
},
|
||||
pause(byButton) {
|
||||
if (byButton) {
|
||||
|
@ -35,7 +35,6 @@ import utcMultiTimeFormat from './utcMultiTimeFormat.js';
|
||||
|
||||
const PADDING = 1;
|
||||
const DEFAULT_DURATION_FORMATTER = 'duration';
|
||||
const RESIZE_POLL_INTERVAL = 200;
|
||||
const PIXELS_PER_TICK = 100;
|
||||
const PIXELS_PER_TICK_WIDE = 200;
|
||||
|
||||
@ -92,7 +91,6 @@ export default {
|
||||
|
||||
//Respond to changes in conductor
|
||||
this.openmct.time.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setViewFromTimeSystem);
|
||||
this.resizeTimer = setInterval(this.resize, RESIZE_POLL_INTERVAL);
|
||||
},
|
||||
beforeUnmount() {
|
||||
clearInterval(this.resizeTimer);
|
||||
|
@ -148,7 +148,10 @@ export default {
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.handleNewBounds = _.throttle(this.handleNewBounds, 300);
|
||||
this.handleNewBounds = _.throttle(this.handleNewBounds, 300, {
|
||||
leading: true,
|
||||
trailing: false
|
||||
});
|
||||
this.setTimeSystem(this.copy(this.openmct.time.getTimeSystem()));
|
||||
this.openmct.time.on(TIME_CONTEXT_EVENTS.timeSystemChanged, this.setTimeSystem);
|
||||
this.setTimeContext();
|
||||
|
@ -46,6 +46,8 @@
|
||||
<div
|
||||
ref="objectName"
|
||||
class="c-object-label__name"
|
||||
aria-label="object name"
|
||||
:title="domainObject && domainObject.name"
|
||||
@mouseover.ctrl="showToolTip"
|
||||
@mouseleave="hideToolTip"
|
||||
>
|
||||
|
@ -105,7 +105,9 @@ export default {
|
||||
return this.status ? `is-status--${this.status}` : '';
|
||||
},
|
||||
ariaLabel() {
|
||||
return `${this.isEditing ? 'Preview' : 'Navigate to'} ${this.domainObject.name} ${this.domainObject.type} Object`;
|
||||
return `${this.isEditing ? 'Preview' : 'Navigate to'} ${this.domainObject.name} ${
|
||||
this.domainObject.type
|
||||
} Object`;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
@ -29,12 +29,14 @@
|
||||
import { axisTop } from 'd3-axis';
|
||||
import { scaleLinear, scaleUtc } from 'd3-scale';
|
||||
import { select } from 'd3-selection';
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import utcMultiTimeFormat from '@/plugins/timeConductor/utcMultiTimeFormat';
|
||||
|
||||
import { useResizeObserver } from '../composables/resize';
|
||||
|
||||
//TODO: UI direction needed for the following property values
|
||||
const PADDING = 1;
|
||||
const RESIZE_POLL_INTERVAL = 200;
|
||||
const PIXELS_PER_TICK = 100;
|
||||
const PIXELS_PER_TICK_WIDE = 200;
|
||||
//This offset needs to be re-considered
|
||||
@ -73,6 +75,16 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const axisHolder = ref(null);
|
||||
const { size, startObserving } = useResizeObserver();
|
||||
onMounted(() => {
|
||||
startObserving(axisHolder.value);
|
||||
});
|
||||
return {
|
||||
containerSize: size
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
bounds(newBounds) {
|
||||
this.drawAxis(newBounds, this.timeSystem);
|
||||
@ -82,6 +94,9 @@ export default {
|
||||
},
|
||||
contentHeight() {
|
||||
this.updateNowMarker();
|
||||
},
|
||||
containerSize() {
|
||||
this.resize();
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@ -100,7 +115,7 @@ export default {
|
||||
|
||||
this.setDimensions();
|
||||
this.drawAxis(this.bounds, this.timeSystem);
|
||||
this.resizeTimer = setInterval(this.resize, RESIZE_POLL_INTERVAL);
|
||||
this.resize();
|
||||
},
|
||||
unmounted() {
|
||||
clearInterval(this.resizeTimer);
|
||||
|
Loading…
x
Reference in New Issue
Block a user