flipper-plugin
Version:
Flipper Desktop plugin SDK and components
270 lines • 11.9 kB
JavaScript
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.useTableRedraw = exports.RedrawContext = exports.DataSourceRendererVirtual = void 0;
const react_1 = __importStar(require("react"));
const react_virtual_1 = require("react-virtual");
const observe_rect_1 = __importDefault(require("@reach/observe-rect"));
// how fast we update if updates are low-prio (e.g. out of window and not super significant)
const LOW_PRIO_UPDATE = 1000; //ms
const HIGH_PRIO_UPDATE = 40; // 25fps
const SMALL_DATASET = 1000; // what we consider a small dataset, for which we keep all updates snappy
var UpdatePrio;
(function (UpdatePrio) {
UpdatePrio[UpdatePrio["NONE"] = 0] = "NONE";
UpdatePrio[UpdatePrio["LOW"] = 1] = "LOW";
UpdatePrio[UpdatePrio["HIGH"] = 2] = "HIGH";
})(UpdatePrio || (UpdatePrio = {}));
/**
* This component is UI agnostic, and just takes care of virtualizing the provided dataSource, and render it as efficiently a possibible,
* de priorizing off screen updates etc.
*/
exports.DataSourceRendererVirtual = (0, react_1.memo)(function DataSourceRendererVirtual({ dataView, defaultRowHeight, useFixedRowHeight, context, itemRenderer, autoScroll, onKeyDown, virtualizerRef, onRangeChange, onUpdateAutoScroll, emptyRenderer, }) {
/**
* Virtualization
*/
// render scheduling
const renderPending = (0, react_1.useRef)(UpdatePrio.NONE);
const lastRender = (0, react_1.useRef)(Date.now());
const [, setForceUpdate] = (0, react_1.useState)(0);
const forceHeightRecalculation = (0, react_1.useRef)(0);
const parentRef = react_1.default.useRef(null);
const isUnitTest = useInUnitTest();
const virtualizer = (0, react_virtual_1.useVirtual)({
size: dataView.size,
parentRef,
useObserver: isUnitTest ? () => ({ height: 500, width: 1000 }) : undefined,
// eslint-disable-next-line
estimateSize: (0, react_1.useCallback)(() => defaultRowHeight, [forceHeightRecalculation.current, defaultRowHeight]),
// TODO: optimise by using setting a keyExtractor if DataSource is keyed
overscan: 0,
});
if (virtualizerRef) {
virtualizerRef.current = virtualizer;
}
const redraw = (0, react_1.useCallback)(() => {
forceHeightRecalculation.current++;
setForceUpdate((x) => x + 1);
}, []);
(0, react_1.useEffect)(function subscribeToDataSource() {
const forceUpdate = () => {
if (unmounted) {
return;
}
timeoutHandle = undefined;
setForceUpdate((x) => x + 1);
};
let unmounted = false;
let timeoutHandle = undefined;
function rerender(prio, invalidateHeights = false) {
if (invalidateHeights && !useFixedRowHeight) {
// the height of some existing rows might have changed
forceHeightRecalculation.current++;
}
if (isUnitTest) {
// test environment, update immediately
forceUpdate();
return;
}
if (renderPending.current >= prio) {
// already scheduled an update with equal or higher prio
return;
}
renderPending.current = Math.max(renderPending.current, prio);
if (prio === UpdatePrio.LOW) {
// Possible optimization: make DEBOUNCE depend on how big the relative change is, and how far from the current window
if (!timeoutHandle) {
timeoutHandle = setTimeout(forceUpdate, LOW_PRIO_UPDATE);
}
}
else {
// High, drop low prio timeout
if (timeoutHandle) {
clearTimeout(timeoutHandle);
timeoutHandle = undefined;
}
if (lastRender.current < Date.now() - HIGH_PRIO_UPDATE) {
forceUpdate(); // trigger render now
}
else {
// debounced
timeoutHandle = setTimeout(forceUpdate, HIGH_PRIO_UPDATE);
}
}
}
const unsubscribe = dataView.addListener((event) => {
switch (event.type) {
case 'reset':
rerender(UpdatePrio.HIGH, true);
break;
case 'shift':
if (dataView.size < SMALL_DATASET) {
rerender(UpdatePrio.HIGH, false);
}
else if (event.location === 'in' ||
// to support smooth tailing we want to render on records directly at the end of the window immediately as well
(event.location === 'after' &&
event.delta > 0 &&
event.index === dataView.windowEnd)) {
rerender(UpdatePrio.HIGH, false);
}
else {
// optimization: we don't want to listen to every count change, especially after window
// and in some cases before window
rerender(UpdatePrio.LOW, false);
}
break;
case 'update':
// in visible range, so let's force update
rerender(UpdatePrio.HIGH, true);
break;
}
});
return () => {
unmounted = true;
unsubscribe();
};
}, [setForceUpdate, useFixedRowHeight, isUnitTest, dataView]);
(0, react_1.useEffect)(() => {
// initial virtualization is incorrect because the parent ref is not yet set, so trigger render after mount
setForceUpdate((x) => x + 1);
}, [setForceUpdate]);
(0, react_1.useLayoutEffect)(function updateWindow() {
const start = virtualizer.virtualItems[0]?.index ?? 0;
const end = start + virtualizer.virtualItems.length;
if (start !== dataView.windowStart && !autoScroll) {
onRangeChange?.(start, end, dataView.size, parentRef.current?.scrollTop ?? 0);
}
dataView.setWindow(start, end);
});
/**
* Scrolling
*/
const onScroll = (0, react_1.useCallback)(() => {
const elem = parentRef.current;
if (!elem) {
return;
}
const fromEnd = elem.scrollHeight - elem.scrollTop - elem.clientHeight;
if (autoScroll && fromEnd > 1) {
onUpdateAutoScroll?.(false);
}
else if (!autoScroll && fromEnd < 1) {
onUpdateAutoScroll?.(true);
}
}, [onUpdateAutoScroll, autoScroll]);
(0, react_1.useLayoutEffect)(function scrollToEnd() {
if (autoScroll) {
virtualizer.scrollToIndex(dataView.size - 1,
/* smooth is not typed by react-virtual, but passed on to the DOM as it should*/
{
align: 'end',
behavior: 'smooth',
});
}
});
/**
* Render finalization
*/
renderPending.current = UpdatePrio.NONE;
lastRender.current = Date.now();
/**
* Observer parent height
*/
(0, react_1.useEffect)(function redrawOnResize() {
if (!parentRef.current) {
return;
}
let lastWidth = 0;
const observer = (0, observe_rect_1.default)(parentRef.current, (rect) => {
if (lastWidth !== rect.width) {
lastWidth = rect.width;
redraw();
}
});
observer.observe();
return () => observer.unobserve();
}, [redraw]);
/**
* Rendering
*/
return (react_1.default.createElement(exports.RedrawContext.Provider, { value: redraw },
react_1.default.createElement("div", { ref: parentRef, onScroll: onScroll, style: tableContainerStyle },
virtualizer.virtualItems.length === 0
? emptyRenderer?.(dataView)
: null,
react_1.default.createElement("div", { style: {
...tableWindowStyle,
height: virtualizer.totalSize,
}, onKeyDown: onKeyDown, tabIndex: 0 }, virtualizer.virtualItems.map((virtualRow) => {
const value = dataView.get(virtualRow.index);
if (value === undefined) {
console.error(`DataSourceRendererVirtual -> unexpected out-of-bound value. Data view has ${dataView.size} items. Requested ${virtualRow.index}. Virtuaslizer has ${virtualizer.virtualItems.length} items.`);
throw new Error(`DataSourceRendererVirtual -> unexpected out-of-bound value. Data view has ${dataView.size} items. Requested ${virtualRow.index}. Virtuaslizer has ${virtualizer.virtualItems.length} items.`);
}
// the position properties always change, so they are not part of the TableRow to avoid invalidating the memoized render always.
// Also all row containers are renderd as part of same component to have 'less react' framework code in between*/}
return (react_1.default.createElement("div", { key: virtualRow.index, style: {
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: useFixedRowHeight ? virtualRow.size : undefined,
transform: `translateY(${virtualRow.start}px)`,
}, ref: useFixedRowHeight ? undefined : virtualRow.measureRef }, itemRenderer(value, virtualRow.index, context)));
})))));
});
const tableContainerStyle = {
overflowY: 'auto',
overflowX: 'hidden',
display: 'flex',
// because: https://stackoverflow.com/questions/37386244/what-does-flex-1-mean
flex: `1 1 0`,
};
const tableWindowStyle = {
position: 'relative',
width: '100%',
};
exports.RedrawContext = (0, react_1.createContext)(undefined);
function useTableRedraw() {
return (0, react_1.useContext)(exports.RedrawContext);
}
exports.useTableRedraw = useTableRedraw;
function useInUnitTest() {
// N.B. Not reusing flipper-common here, since data-source is published as separate package
return typeof process !== 'undefined' && process?.env?.NODE_ENV === 'test';
}
//# sourceMappingURL=DataSourceRendererVirtual.js.map
;