UNPKG

flipper-plugin

Version:

Flipper Desktop plugin SDK and components

707 lines 38.7 kB
"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