flipper-plugin
Version:
Flipper Desktop plugin SDK and components
707 lines • 38.7 kB
JavaScript
"use strict";
/**
* 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.DataTable = void 0;
const react_1 = __importStar(require("react"));
const TableRow_1 = require("./TableRow");
const Layout_1 = require("../Layout");
const TableHead_1 = require("./TableHead");
const index_1 = require("../../data-source/index");
const DataTableWithPowerSearchManager_1 = require("./DataTableWithPowerSearchManager");
const styled_1 = __importDefault(require("@emotion/styled"));
const theme_1 = require("../theme");
const PowerSearchTableContextMenu_1 = require("./PowerSearchTableContextMenu");
const antd_1 = require("antd");
const icons_1 = require("@ant-design/icons");
const useAssertStableRef_1 = require("../../utils/useAssertStableRef");
const PluginContext_1 = require("../../plugin/PluginContext");
const lodash_1 = require("lodash");
const useInUnitTest_1 = require("../../utils/useInUnitTest");
const useLatestRef_1 = require("../../utils/useLatestRef");
const PowerSearch_1 = require("../PowerSearch");
const DataTableDefaultPowerSearchOperators_1 = require("./DataTableDefaultPowerSearchOperators");
const powerSearchConfigEntireRow = {
label: 'Row',
key: 'entireRow',
operators: {
searializable_object_contains_any_of: DataTableDefaultPowerSearchOperators_1.dataTablePowerSearchOperators.searializable_object_contains_any_of(),
searializable_object_contains_none_of: DataTableDefaultPowerSearchOperators_1.dataTablePowerSearchOperators.searializable_object_contains_none_of(),
searializable_object_matches_regex: DataTableDefaultPowerSearchOperators_1.dataTablePowerSearchOperators.searializable_object_matches_regex(),
},
useWholeRow: true,
};
const powerSearchConfigIsExtendedConfig = (powerSearchConfig) => !!powerSearchConfig &&
Array.isArray(powerSearchConfig.operators);
const powerSearchConfigIsSimplifiedConfig = (powerSearchConfig) => !!powerSearchConfig &&
typeof powerSearchConfig.type === 'string';
const Searchbar = (0, styled_1.default)(Layout_1.Layout.Horizontal)({
backgroundColor: theme_1.theme.backgroundWash,
paddingBottom: theme_1.theme.space.medium,
});
function DataTable(props) {
const { onRowStyle, onSelect, onCopyRows, onContextMenu } = props;
const dataSource = normalizeDataSourceInput(props);
const dataView = props?.viewId
? dataSource.getAdditionalView(props.viewId)
: dataSource.view;
(0, useAssertStableRef_1.useAssertStableRef)(dataSource, 'dataSource');
(0, useAssertStableRef_1.useAssertStableRef)(onRowStyle, 'onRowStyle');
(0, useAssertStableRef_1.useAssertStableRef)(props.onSelect, 'onRowSelect');
(0, useAssertStableRef_1.useAssertStableRef)(props.onSearchExpressionChange, 'onSearchExpressionChanges');
(0, useAssertStableRef_1.useAssertStableRef)(props.columns, 'columns');
(0, useAssertStableRef_1.useAssertStableRef)(onCopyRows, 'onCopyRows');
(0, useAssertStableRef_1.useAssertStableRef)(onContextMenu, 'onContextMenu');
const isUnitTest = (0, useInUnitTest_1.useInUnitTest)();
// eslint-disable-next-line
const scope = isUnitTest ? '' : (0, PluginContext_1.usePluginInstanceMaybe)()?.definition.id ?? '';
let virtualizerRef = (0, react_1.useRef)();
if (props.virtualizerRef) {
virtualizerRef = props.virtualizerRef;
}
const [tableState, dispatch] = (0, react_1.useReducer)(DataTableWithPowerSearchManager_1.dataTableManagerReducer, undefined, () => (0, DataTableWithPowerSearchManager_1.createInitialState)({
dataSource,
dataView,
defaultColumns: props.columns,
onSelect,
scope,
virtualizerRef,
autoScroll: props.enableAutoScroll,
enablePersistSettings: props.enablePersistSettings,
initialSearchExpression: props.powerSearchInitialState,
}));
const stateRef = (0, react_1.useRef)(tableState);
stateRef.current = tableState;
const searchInputRef = (0, react_1.useRef)(null);
const lastOffset = (0, react_1.useRef)(0);
const dragging = (0, react_1.useRef)(false);
const [tableManager] = (0, react_1.useState)(() => (0, DataTableWithPowerSearchManager_1.createDataTableManager)(dataView, dispatch, stateRef));
// Make sure this is the main table
if (props.tableManagerRef && !props.viewId) {
props.tableManagerRef.current = tableManager;
}
const { columns, selection, searchExpression, sorting } = tableState;
const latestSelectionRef = (0, useLatestRef_1.useLatestRef)(selection);
const latestOnSelectRef = (0, useLatestRef_1.useLatestRef)(onSelect);
(0, react_1.useEffect)(() => {
if (dataView) {
const unsubscribe = dataView.addListener((change) => {
if (change.type === 'update' &&
latestSelectionRef.current.items.has(change.index)) {
latestOnSelectRef.current?.((0, DataTableWithPowerSearchManager_1.getSelectedItem)(dataView, latestSelectionRef.current), (0, DataTableWithPowerSearchManager_1.getSelectedItems)(dataView, latestSelectionRef.current));
}
});
return unsubscribe;
}
}, [dataView, latestSelectionRef, latestOnSelectRef]);
const visibleColumns = (0, react_1.useMemo)(() => columns.filter((column) => column.visible), [columns]);
// Collecting a hashmap of unique values for every column we infer the power search enum labels for (hashmap of hashmaps).
// It could be a hashmap of sets, but then we would need to convert a set to a hashpmap when rendering enum power search term, so it is just more convenient to make it a hashmap of hashmaps
const [inferredPowerSearchEnumLabels, setInferredPowerSearchEnumLabels] = react_1.default.useState({});
react_1.default.useEffect(() => {
const columnKeysToInferOptionsFor = [];
const secondaryIndeciesKeys = new Set(dataSource.secondaryIndicesKeys());
for (const column of columns) {
if ((powerSearchConfigIsExtendedConfig(column.powerSearchConfig) ||
(powerSearchConfigIsSimplifiedConfig(column.powerSearchConfig) &&
column.powerSearchConfig.type === 'enum')) &&
column.powerSearchConfig.inferEnumOptionsFromData) {
if (!secondaryIndeciesKeys.has(column.key)) {
console.warn('inferEnumOptionsFromData work only if the same column key is specified as a DataSource secondary index! See https://fburl.com/code/0waicx6p. Missing index definition!', column.key);
continue;
}
columnKeysToInferOptionsFor.push(column.key);
}
}
if (columnKeysToInferOptionsFor.length > 0) {
const getInferredLabels = () => {
const newInferredLabels = {};
for (const key of columnKeysToInferOptionsFor) {
newInferredLabels[key] = {};
for (const indexValue of dataSource.getAllIndexValues([
key,
]) ?? []) {
// `indexValue` is a stringified JSON in a format of { key: value }
const value = Object.values(JSON.parse(indexValue))[0];
newInferredLabels[key][value] = value;
}
}
return newInferredLabels;
};
setInferredPowerSearchEnumLabels(getInferredLabels());
const unsubscribeIndexUpdates = dataSource.addDataListener('siNewIndexValue', ({ firstOfKind }) => {
if (firstOfKind) {
setInferredPowerSearchEnumLabels(getInferredLabels());
}
});
const unsubscribeDataSourceClear = dataSource.addDataListener('clear', () => {
setInferredPowerSearchEnumLabels(getInferredLabels());
});
return () => {
unsubscribeIndexUpdates();
unsubscribeDataSourceClear();
};
}
}, [columns, dataSource]);
const powerSearchConfig = (0, react_1.useMemo)(() => {
const res = { fields: {} };
if (props.enablePowerSearchWholeRowSearch) {
res.fields.entireRow = powerSearchConfigEntireRow;
}
for (const column of columns) {
if (column.powerSearchConfig === false) {
continue;
}
let useWholeRow = false;
let columnPowerSearchOperators;
// If no power search config provided we treat every input as a string
if (!column.powerSearchConfig) {
columnPowerSearchOperators = [
DataTableDefaultPowerSearchOperators_1.dataTablePowerSearchOperators.string_contains(),
DataTableDefaultPowerSearchOperators_1.dataTablePowerSearchOperators.string_not_contains(),
DataTableDefaultPowerSearchOperators_1.dataTablePowerSearchOperators.string_matches_exactly(),
DataTableDefaultPowerSearchOperators_1.dataTablePowerSearchOperators.string_not_matches_exactly(),
DataTableDefaultPowerSearchOperators_1.dataTablePowerSearchOperators.string_set_contains_any_of(),
DataTableDefaultPowerSearchOperators_1.dataTablePowerSearchOperators.string_set_contains_none_of(),
DataTableDefaultPowerSearchOperators_1.dataTablePowerSearchOperators.string_matches_regex(),
];
}
else if (Array.isArray(column.powerSearchConfig)) {
columnPowerSearchOperators = column.powerSearchConfig;
}
else if (powerSearchConfigIsExtendedConfig(column.powerSearchConfig)) {
columnPowerSearchOperators = column.powerSearchConfig.operators;
useWholeRow = !!column.powerSearchConfig.useWholeRow;
const inferredPowerSearchEnumLabelsForColumn = inferredPowerSearchEnumLabels[column.key];
if (inferredPowerSearchEnumLabelsForColumn &&
column.powerSearchConfig.inferEnumOptionsFromData) {
const allowFreeform = column.powerSearchConfig.allowFreeform ?? true;
columnPowerSearchOperators = columnPowerSearchOperators.map((operator) => ({
...operator,
enumLabels: inferredPowerSearchEnumLabelsForColumn,
allowFreeform,
}));
}
}
else {
switch (column.powerSearchConfig.type) {
case 'date': {
columnPowerSearchOperators = [
DataTableDefaultPowerSearchOperators_1.dataTablePowerSearchOperators.same_as_absolute_date_no_time(),
DataTableDefaultPowerSearchOperators_1.dataTablePowerSearchOperators.older_than_absolute_date_no_time(),
DataTableDefaultPowerSearchOperators_1.dataTablePowerSearchOperators.newer_than_absolute_date_no_time(),
];
break;
}
case 'dateTime': {
columnPowerSearchOperators = [
DataTableDefaultPowerSearchOperators_1.dataTablePowerSearchOperators.older_than_absolute_date(),
DataTableDefaultPowerSearchOperators_1.dataTablePowerSearchOperators.newer_than_absolute_date(),
];
break;
}
case 'string': {
columnPowerSearchOperators = [
DataTableDefaultPowerSearchOperators_1.dataTablePowerSearchOperators.string_matches_exactly(),
DataTableDefaultPowerSearchOperators_1.dataTablePowerSearchOperators.string_not_matches_exactly(),
DataTableDefaultPowerSearchOperators_1.dataTablePowerSearchOperators.string_set_contains_any_of(),
DataTableDefaultPowerSearchOperators_1.dataTablePowerSearchOperators.string_set_contains_none_of(),
DataTableDefaultPowerSearchOperators_1.dataTablePowerSearchOperators.string_matches_regex(),
];
break;
}
case 'int': {
columnPowerSearchOperators = [
DataTableDefaultPowerSearchOperators_1.dataTablePowerSearchOperators.int_equals(),
DataTableDefaultPowerSearchOperators_1.dataTablePowerSearchOperators.int_greater_or_equal(),
DataTableDefaultPowerSearchOperators_1.dataTablePowerSearchOperators.int_greater_than(),
DataTableDefaultPowerSearchOperators_1.dataTablePowerSearchOperators.int_less_or_equal(),
DataTableDefaultPowerSearchOperators_1.dataTablePowerSearchOperators.int_less_than(),
];
break;
}
case 'float': {
columnPowerSearchOperators = [
DataTableDefaultPowerSearchOperators_1.dataTablePowerSearchOperators.float_equals(),
DataTableDefaultPowerSearchOperators_1.dataTablePowerSearchOperators.float_greater_or_equal(),
DataTableDefaultPowerSearchOperators_1.dataTablePowerSearchOperators.float_greater_than(),
DataTableDefaultPowerSearchOperators_1.dataTablePowerSearchOperators.float_less_or_equal(),
DataTableDefaultPowerSearchOperators_1.dataTablePowerSearchOperators.float_less_than(),
];
break;
}
case 'enum': {
let enumLabels;
let allowFreeform = column.powerSearchConfig.allowFreeform;
if (column.powerSearchConfig.inferEnumOptionsFromData) {
enumLabels = inferredPowerSearchEnumLabels[column.key] ?? {};
// Fallback to `true` by default when we use inferred labels
if (allowFreeform === undefined) {
allowFreeform = true;
}
}
else {
enumLabels = column.powerSearchConfig.enumLabels;
}
columnPowerSearchOperators = [
DataTableDefaultPowerSearchOperators_1.dataTablePowerSearchOperators.enum_set_is_any_of(enumLabels, allowFreeform),
DataTableDefaultPowerSearchOperators_1.dataTablePowerSearchOperators.enum_set_is_none_of(enumLabels, allowFreeform),
DataTableDefaultPowerSearchOperators_1.dataTablePowerSearchOperators.enum_set_is_nullish_or_any_of(enumLabels, allowFreeform),
DataTableDefaultPowerSearchOperators_1.dataTablePowerSearchOperators.string_matches_regex(),
];
break;
}
case 'object': {
columnPowerSearchOperators = [
DataTableDefaultPowerSearchOperators_1.dataTablePowerSearchOperators.searializable_object_contains_any_of(),
DataTableDefaultPowerSearchOperators_1.dataTablePowerSearchOperators.searializable_object_contains_none_of(),
DataTableDefaultPowerSearchOperators_1.dataTablePowerSearchOperators.searializable_object_matches_regex(),
];
break;
}
default: {
throw new Error(`Unknown power search config type ${JSON.stringify(column.powerSearchConfig)}`);
}
}
}
const columnFieldConfig = {
label: column.title || column.key,
key: column.key,
operators: columnPowerSearchOperators.reduce((res, operatorConfig) => {
res[operatorConfig.key] = operatorConfig;
return res;
}, {}),
useWholeRow,
};
res.fields[column.key] = columnFieldConfig;
}
return res;
}, [
columns,
props.enablePowerSearchWholeRowSearch,
inferredPowerSearchEnumLabels,
]);
const renderingConfig = (0, react_1.useMemo)(() => {
let startIndex = 0;
return {
columns: visibleColumns,
onMouseEnter(e, _item, index) {
if (dragging.current && e.buttons === 1 && props.enableMultiSelect) {
// by computing range we make sure no intermediate items are missed when scrolling fast
tableManager.addRangeToSelection(startIndex, index);
}
},
onMouseDown(e, _item, index) {
if (!props.enableMultiSelect && e.buttons > 1) {
tableManager.selectItem(index, false, true);
return;
}
if (!dragging.current) {
if (e.buttons > 1) {
// for right click we only want to add if needed, not deselect
tableManager.addRangeToSelection(index, index, false);
}
else if (e.ctrlKey || e.metaKey) {
tableManager.addRangeToSelection(index, index, true);
}
else if (e.shiftKey) {
tableManager.selectItem(index, true, true);
}
else {
tableManager.selectItem(index, false, true);
}
dragging.current = true;
startIndex = index;
function onStopDragSelecting() {
dragging.current = false;
document.removeEventListener('mouseup', onStopDragSelecting);
}
document.addEventListener('mouseup', onStopDragSelecting);
}
},
onRowStyle,
onContextMenu: props.enableContextMenu
? () => {
// using a ref keeps the config stable, so that a new context menu doesn't need
// all rows to be rerendered, but rather shows it conditionally
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return contextMenuRef.current?.();
}
: undefined,
};
}, [
visibleColumns,
tableManager,
onRowStyle,
props.enableContextMenu,
props.enableMultiSelect,
]);
const itemRenderer = (0, react_1.useCallback)(function itemRenderer(record, index, renderContext) {
return (react_1.default.createElement(TableRow_1.TableRow, { key: index, config: renderContext, record: record, itemIndex: index, highlighted: index === selection.current || selection.items.has(index), style: onRowStyle?.(record) }));
}, [selection, onRowStyle]);
/**
* Keyboard / selection handling
*/
const onKeyDown = (0, react_1.useCallback)((e) => {
let handled = true;
const shiftPressed = e.shiftKey;
const outputSize = dataView.size;
const controlPressed = e.ctrlKey;
const windowSize = props.scrollable
? virtualizerRef.current?.virtualItems.length ?? 0
: dataView.size;
if (!windowSize) {
return;
}
switch (e.key) {
case 'ArrowUp':
tableManager.selectItem((idx) => (idx > 0 ? idx - 1 : 0), shiftPressed);
break;
case 'ArrowDown':
tableManager.selectItem((idx) => (idx < outputSize - 1 ? idx + 1 : idx), shiftPressed);
break;
case 'Home':
tableManager.selectItem(0, shiftPressed);
break;
case 'End':
tableManager.selectItem(outputSize - 1, shiftPressed);
break;
case ' ': // yes, that is a space
case 'PageDown':
tableManager.selectItem((idx) => Math.min(outputSize - 1, idx + windowSize - 1), shiftPressed);
break;
case 'PageUp':
tableManager.selectItem((idx) => Math.max(0, idx - windowSize + 1), shiftPressed);
break;
case 'Escape':
tableManager.clearSelection();
break;
case 'f':
if (controlPressed && searchInputRef?.current) {
searchInputRef?.current.focus();
}
break;
default:
handled = false;
}
if (handled) {
e.stopPropagation();
e.preventDefault();
}
}, [dataView, props.scrollable, tableManager]);
const [setFilter] = (0, react_1.useState)(() => (tableState) => {
const selectedEntry = tableState.selection.current >= 0
? dataView.getEntry(tableState.selection.current)
: null;
dataView.setFilter((0, DataTableWithPowerSearchManager_1.computeDataTableFilter)(tableState.searchExpression, DataTableDefaultPowerSearchOperators_1.dataTablePowerSearchOperatorProcessorConfig, props.treatUndefinedValuesAsMatchingFiltering));
dataView.setFilterExpections(tableState.filterExceptions);
// TODO: in the future setFilter effects could be async, at the moment it isn't,
// so we can safely assume the internal state of the dataView is updated with the
// filter changes and try to find the same entry back again
if (selectedEntry) {
const selectionIndex = dataView.getViewIndexOfEntry(selectedEntry);
tableManager.selectItem(selectionIndex, false, false);
// we disable autoScroll as is it can accidentally be annoying if it was never turned off and
// filter causes items to not fill the available space
dispatch({ type: 'setAutoScroll', autoScroll: false });
virtualizerRef.current?.scrollToIndex(selectionIndex, { align: 'center' });
setTimeout(() => {
virtualizerRef.current?.scrollToIndex(selectionIndex, {
align: 'center',
});
}, 0);
}
// TODO: could do the same for multiselections, doesn't seem to be requested so far
});
const [debouncedSetFilter] = (0, react_1.useState)(() => {
// we don't want to trigger filter changes too quickly, as they can be pretty expensive
// and would block the user from entering text in the search bar for example
// (and in the future would really benefit from concurrent mode here :))
// leading is set to true so that an initial filter is immediately applied and a flash of wrong content is prevented
// this also makes clear act faster
return isUnitTest ? setFilter : (0, lodash_1.debounce)(setFilter, 250);
});
(0, react_1.useEffect)(function updateFilter() {
if (!dataView.isFiltered) {
setFilter(tableState);
}
else {
debouncedSetFilter(tableState);
}
},
// Important dep optimization: we don't want to recalc filters if just the width or visibility changes!
// We pass entire state.columns to computeDataTableFilter, but only changes in the filter are a valid cause to compute a new filter function
// eslint-disable-next-line
[
tableState.searchExpression,
// eslint-disable-next-line react-hooks/exhaustive-deps
...tableState.columns.map((c) => c.inversed),
tableState.filterExceptions,
]);
(0, react_1.useEffect)(function updateSorting() {
if (tableState.sorting === undefined) {
dataView.setSortBy(undefined);
dataView.setReversed(false);
}
else {
dataView.setSortBy(tableState.sorting.key);
dataView.setReversed(tableState.sorting.direction === 'desc');
}
}, [dataView, tableState.sorting]);
const isMounted = (0, react_1.useRef)(false);
(0, react_1.useEffect)(function triggerSelection() {
if (isMounted.current) {
onSelect?.((0, DataTableWithPowerSearchManager_1.getSelectedItem)(dataView, tableState.selection), (0, DataTableWithPowerSearchManager_1.getSelectedItems)(dataView, tableState.selection));
}
isMounted.current = true;
}, [onSelect, dataView, tableState.selection]);
// The initialScrollPosition is used to both capture the initial px we want to scroll to,
// and whether we performed that scrolling already (if so, it will be 0)
(0, react_1.useLayoutEffect)(function scrollSelectionIntoView() {
if (tableState.initialOffset) {
virtualizerRef.current?.scrollToOffset(tableState.initialOffset);
dispatch({
type: 'appliedInitialScroll',
});
}
else if (selection && selection.current >= 0) {
dispatch({ type: 'setAutoScroll', autoScroll: false });
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
virtualizerRef.current?.scrollToIndex(selection.current, {
align: 'auto',
});
}
},
// initialOffset is relevant for the first run,
// but should not trigger the efffect in general
// eslint-disable-next-line
[selection]);
/** Range finder */
const [range, setRange] = (0, react_1.useState)('');
const hideRange = (0, react_1.useRef)();
const onRangeChange = (0, react_1.useCallback)((start, end, total, offset) => {
setRange(`${start} - ${end} / ${total}`);
lastOffset.current = offset;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
clearTimeout(hideRange.current);
hideRange.current = setTimeout(() => {
setRange('');
}, 1000);
}, []);
const onUpdateAutoScroll = (0, react_1.useCallback)((autoScroll) => {
if (props.enableAutoScroll) {
dispatch({ type: 'setAutoScroll', autoScroll });
}
}, [props.enableAutoScroll]);
const sidePanelToggle = (0, react_1.useMemo)(() => (react_1.default.createElement(antd_1.Menu.Item, { key: "toggle side by side" },
react_1.default.createElement(Layout_1.Layout.Horizontal, { gap: true, center: true, onClick: (e) => {
e.stopPropagation();
e.preventDefault();
} },
"Side By Side View",
react_1.default.createElement(antd_1.Switch, { checked: tableState.sideBySide, size: "small", onChange: () => {
tableManager.toggleSideBySide();
} })))), [tableManager, tableState.sideBySide]);
/** Context menu */
const contexMenu = isUnitTest
? undefined
: // eslint-disable-next-line
(0, react_1.useCallback)(() => (0, PowerSearchTableContextMenu_1.tableContextMenuFactory)(dataView, dispatch, selection, tableState.columns, visibleColumns, onCopyRows, onContextMenu, props.enableMultiPanels ? sidePanelToggle : undefined), [
dataView,
selection,
tableState.columns,
visibleColumns,
onCopyRows,
onContextMenu,
props.enableMultiPanels,
sidePanelToggle,
]);
const contextMenuRef = (0, react_1.useRef)(contexMenu);
contextMenuRef.current = contexMenu;
(0, react_1.useEffect)(function initialSetup() {
return function cleanup() {
// write current prefs to local storage
(0, DataTableWithPowerSearchManager_1.savePreferences)(stateRef.current, lastOffset.current);
// if the component unmounts, we reset the SFRW pipeline to
// avoid wasting resources in the background
dataView.reset();
if (props.viewId) {
// this is a side panel
dataSource.deleteView(props.viewId);
}
// clean ref && Make sure this is the main table
if (props.tableManagerRef && !props.viewId) {
props.tableManagerRef.current = undefined;
}
};
// one-time setup and cleanup effect, everything in here is asserted to be stable:
// dataSource, tableManager, tableManagerRef
// eslint-disable-next-line
}, []);
const header = (react_1.default.createElement(Layout_1.Layout.Container, null,
props.actionsTop ? react_1.default.createElement(Searchbar, { gap: true }, props.actionsTop) : null,
props.enableSearchbar && (react_1.default.createElement(Searchbar, { grow: true, shrink: true, gap: true, style: { alignItems: 'flex-start' } },
react_1.default.createElement(PowerSearch_1.PowerSearch, { config: powerSearchConfig, searchExpression: searchExpression, onSearchExpressionChange: (newSearchExpression) => {
tableManager.setSearchExpression(newSearchExpression);
props.onSearchExpressionChange?.(newSearchExpression);
}, onConfirmUnknownOption: props.enablePowerSearchWholeRowSearch
? (searchValue) => ({
field: powerSearchConfigEntireRow,
operator: DataTableDefaultPowerSearchOperators_1.dataTablePowerSearchOperators.searializable_object_contains(),
searchValue,
})
: undefined }),
react_1.default.createElement(ActionsPanel, null,
contexMenu && (react_1.default.createElement(antd_1.Dropdown, { overlay: contexMenu, placement: "bottomRight" },
react_1.default.createElement(antd_1.Button, { type: "ghost" },
react_1.default.createElement(icons_1.MenuOutlined, null)))),
props.actionsRight,
props.extraActions)))));
const columnHeaders = (react_1.default.createElement(Layout_1.Layout.Container, null, props.enableColumnHeaders && (react_1.default.createElement(TableHead_1.TableHead, { visibleColumns: visibleColumns, dispatch: dispatch, sorting: sorting, scrollbarSize: props.scrollable
? 0
: 15 /* width on MacOS: TODO, determine dynamically */, isFilterable: false }))));
const emptyRenderer = props.onRenderEmpty === undefined
? createDefaultEmptyRenderer(tableManager)
: props.onRenderEmpty;
let mainSection;
if (props.scrollable) {
const dataSourceRenderer = (react_1.default.createElement(index_1.DataSourceRendererVirtual, { dataView: dataView, autoScroll: tableState.autoScroll && !dragging.current, useFixedRowHeight: !tableState.usesWrapping, defaultRowHeight: TableRow_1.DEFAULT_ROW_HEIGHT, context: renderingConfig, itemRenderer: itemRenderer, onKeyDown: onKeyDown, virtualizerRef: virtualizerRef, onRangeChange: onRangeChange, onUpdateAutoScroll: onUpdateAutoScroll, emptyRenderer: emptyRenderer }));
mainSection = props.enableHorizontalScroll ? (react_1.default.createElement(Layout_1.Layout.Top, null,
header,
react_1.default.createElement(Layout_1.Layout.ScrollContainer, { horizontal: true, vertical: false },
react_1.default.createElement(Layout_1.Layout.Top, null,
columnHeaders,
dataSourceRenderer)))) : (react_1.default.createElement(Layout_1.Layout.Top, null,
react_1.default.createElement("div", null,
header,
columnHeaders),
dataSourceRenderer));
}
else {
mainSection = (react_1.default.createElement(Layout_1.Layout.Container, null,
header,
columnHeaders,
react_1.default.createElement(index_1.DataSourceRendererStatic, { dataView: dataView, useFixedRowHeight: !tableState.usesWrapping, defaultRowHeight: TableRow_1.DEFAULT_ROW_HEIGHT, context: renderingConfig, maxRecords: dataSource.limit, itemRenderer: itemRenderer, onKeyDown: onKeyDown, emptyRenderer: emptyRenderer })));
}
const mainPanel = (react_1.default.createElement(Layout_1.Layout.Container, { grow: props.scrollable, style: { position: 'relative' } },
mainSection,
props.enableAutoScroll && (react_1.default.createElement(AutoScroller, null,
react_1.default.createElement(icons_1.PushpinFilled, { style: {
color: tableState.autoScroll ? theme_1.theme.successColor : undefined,
}, onClick: () => {
dispatch({ type: 'toggleAutoScroll' });
} }))),
range && !isUnitTest && react_1.default.createElement(RangeFinder, null, range)));
return props.enableMultiPanels && tableState.sideBySide ? (
//TODO: Make the panels resizable by having a dynamic maxWidth for Layout.Right/Left possibly?
react_1.default.createElement(Layout_1.Layout.Horizontal, { style: { height: '100%' } },
mainPanel,
react_1.default.createElement(DataTable, { viewId: '1', ...props, enableMultiPanels: false }))) : (mainPanel);
}
exports.DataTable = DataTable;
DataTable.defaultProps = {
scrollable: true,
enableSearchbar: true,
enableAutoScroll: false,
enableHorizontalScroll: true,
enableColumnHeaders: true,
enableMultiSelect: true,
enableContextMenu: true,
enablePersistSettings: true,
onRenderEmpty: undefined,
enablePowerSearchWholeRowSearch: true,
treatUndefinedValuesAsMatchingFiltering: false,
};
/* eslint-disable react-hooks/rules-of-hooks */
function normalizeDataSourceInput(props) {
if (props.dataSource) {
return props.dataSource;
}
if (props.records) {
const [dataSource] = (0, react_1.useState)(() => (0, index_1.createDataSource)(props.records, { key: props.recordsKey }));
(0, react_1.useEffect)(() => {
syncRecordsToDataSource(dataSource, props.records);
}, [dataSource, props.records]);
return dataSource;
}
throw new Error(`Either the 'dataSource' or 'records' prop should be provided to DataTable`);
}
/* eslint-enable */
function syncRecordsToDataSource(ds, records) {
const startTime = Date.now();
ds.clear();
// TODO: optimize in the case we're only dealing with appends or replacements
records.forEach((r) => ds.append(r));
const duration = Math.abs(Date.now() - startTime);
if (duration > 50 || records.length > 500) {
console.warn("The 'records' props is only intended to be used on small datasets. Please use a 'dataSource' instead. See createDataSource for details: https://fbflipper.com/docs/extending/flipper-plugin#createdatasource");
}
}
function createDefaultEmptyRenderer(dataTableManager) {
return (dataView) => (react_1.default.createElement(EmptyTable, { dataView: dataView, dataManager: dataTableManager }));
}
function EmptyTable({ dataView, dataManager, }) {
const resetFilters = (0, react_1.useCallback)(() => {
dataManager?.resetFilters();
}, [dataManager]);
return (react_1.default.createElement(Layout_1.Layout.Container, { center: true, style: { width: '100%', padding: 40, color: theme_1.theme.textColorSecondary } }, dataView?.size === 0 ? (react_1.default.createElement(react_1.default.Fragment, null,
react_1.default.createElement(icons_1.CoffeeOutlined, { style: { fontSize: '2em', margin: 8 } }),
react_1.default.createElement(antd_1.Typography.Text, { type: "secondary" }, "No records yet"))) : (react_1.default.createElement(react_1.default.Fragment, null,
react_1.default.createElement(icons_1.SearchOutlined, { style: { fontSize: '2em', margin: 8 } }),
react_1.default.createElement(antd_1.Typography.Text, { type: "secondary" }, "No records match the current search / filter criteria."),
react_1.default.createElement(antd_1.Typography.Text, null,
react_1.default.createElement(antd_1.Typography.Link, { onClick: resetFilters }, "Reset filters"))))));
}
const ActionsPanel = styled_1.default.div({ display: 'flex', flexWrap: 'wrap', gap: 4 });
const RangeFinder = styled_1.default.div({
backgroundColor: theme_1.theme.backgroundWash,
position: 'absolute',
right: 64,
bottom: 20,
padding: '4px 8px',
color: theme_1.theme.textColorSecondary,
fontSize: '0.8em',
});
const AutoScroller = styled_1.default.div({
backgroundColor: theme_1.theme.backgroundWash,
position: 'absolute',
right: 40,
bottom: 20,
width: 24,
padding: '4px 8px',
color: theme_1.theme.textColorSecondary,
fontSize: '0.8em',
});
//# sourceMappingURL=DataTableWithPowerSearch.js.map