Use DataTable as list base
Summary: Changelog: Standardized DataList component This diff standardizes the DataList component, by reusing the DataList. This is done to be able to take full advantage of all its features like virtualisation, keyboard support, datasource support, etc. Also cleaned up DataTable properties a bit, by prefixing all flags with `enableXXX` and setting clear defaults Reviewed By: passy Differential Revision: D28119721 fbshipit-source-id: b7b241ea18d788bfa035389cc8c6ae7ea95ecadb
This commit is contained in:
committed by
Facebook GitHub Bot
parent
5bf9541e05
commit
d903a862d2
@@ -7,11 +7,29 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {useCallback, memo} from 'react';
|
import React, {
|
||||||
|
useCallback,
|
||||||
|
memo,
|
||||||
|
createRef,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
import {DataFormatter} from './DataFormatter';
|
import {DataFormatter} from './DataFormatter';
|
||||||
import {Layout} from './Layout';
|
import {Layout} from './Layout';
|
||||||
import {theme} from './theme';
|
|
||||||
import {Typography} from 'antd';
|
import {Typography} from 'antd';
|
||||||
|
import {
|
||||||
|
DataTable,
|
||||||
|
DataTableColumn,
|
||||||
|
DataTableProps,
|
||||||
|
ItemRenderer,
|
||||||
|
} from './data-table/DataTable';
|
||||||
|
import {RightOutlined} from '@ant-design/icons';
|
||||||
|
import {theme} from './theme';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import {DataTableManager} from './data-table/DataTableManager';
|
||||||
|
import {Atom, createState} from '../state/atom';
|
||||||
|
import {useAssertStableRef} from '../utils/useAssertStableRef';
|
||||||
|
|
||||||
const {Text} = Typography;
|
const {Text} = Typography;
|
||||||
|
|
||||||
@@ -21,7 +39,7 @@ interface Item {
|
|||||||
description?: string;
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DataListProps<T extends Item> {
|
interface DataListBaseProps<T extends Item> {
|
||||||
/**
|
/**
|
||||||
* Defines the styling of the component. By default shows a list, but alternatively the items can be displayed in a drop down
|
* Defines the styling of the component. By default shows a list, but alternatively the items can be displayed in a drop down
|
||||||
*/
|
*/
|
||||||
@@ -34,11 +52,11 @@ interface DataListProps<T extends Item> {
|
|||||||
/**
|
/**
|
||||||
* The current selection
|
* The current selection
|
||||||
*/
|
*/
|
||||||
value?: string /* | Atom<string>*/;
|
selection: Atom<string | undefined>;
|
||||||
/**
|
/**
|
||||||
* Handler that is fired if selection is changed
|
* Handler that is fired if selection is changed
|
||||||
*/
|
*/
|
||||||
onSelect?(id: string, value: T): void;
|
onSelect?(id: string | undefined, value: T | undefined): void;
|
||||||
className?: string;
|
className?: string;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
/**
|
/**
|
||||||
@@ -48,105 +66,143 @@ interface DataListProps<T extends Item> {
|
|||||||
/**
|
/**
|
||||||
* Custom render function. By default the component will render the `title` in bold and description (if any) below it
|
* Custom render function. By default the component will render the `title` in bold and description (if any) below it
|
||||||
*/
|
*/
|
||||||
onRenderItem?: (item: T, selected: boolean) => React.ReactElement;
|
onRenderItem?: ItemRenderer<T>;
|
||||||
|
/**
|
||||||
|
* Show a right arrow by default
|
||||||
|
*/
|
||||||
|
enableArrow?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DataListProps<T extends Item> = DataListBaseProps<T> &
|
||||||
|
// Some table props are set by DataList instead, so override them
|
||||||
|
Omit<DataTableProps<T>, 'records' | 'dataSource' | 'columns' | 'onSelect'>;
|
||||||
|
|
||||||
export const DataList: React.FC<DataListProps<any>> = function DataList<
|
export const DataList: React.FC<DataListProps<any>> = function DataList<
|
||||||
T extends Item
|
T extends Item
|
||||||
>({
|
>({
|
||||||
// type,
|
selection: baseSelection,
|
||||||
scrollable,
|
|
||||||
value,
|
|
||||||
onSelect,
|
onSelect,
|
||||||
className,
|
className,
|
||||||
style,
|
style,
|
||||||
items,
|
items,
|
||||||
onRenderItem,
|
onRenderItem,
|
||||||
|
enableArrow,
|
||||||
|
...tableProps
|
||||||
}: DataListProps<T>) {
|
}: DataListProps<T>) {
|
||||||
|
// if a tableManagerRef is provided, we piggy back on that same ref
|
||||||
|
// eslint-disable-next-line
|
||||||
|
const tableManagerRef = tableProps.tableManagerRef ?? createRef<undefined | DataTableManager<T>>();
|
||||||
|
|
||||||
|
useAssertStableRef(baseSelection, 'selection');
|
||||||
|
// create local selection atom if none provided
|
||||||
|
// eslint-disable-next-line
|
||||||
|
const selection = baseSelection ?? useState(() => createState<string|undefined>())[0];
|
||||||
|
|
||||||
const handleSelect = useCallback(
|
const handleSelect = useCallback(
|
||||||
(key: string, item: T) => {
|
(item: T | undefined) => {
|
||||||
onSelect?.(key, item);
|
selection.set(item?.id);
|
||||||
},
|
},
|
||||||
[onSelect],
|
[selection],
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderedItems = items.map((item) => (
|
const dataListColumns: DataTableColumn<T>[] = useMemo(
|
||||||
<DataListItemWrapper
|
() => [
|
||||||
item={item}
|
{
|
||||||
key={item.id}
|
key: 'id' as const,
|
||||||
selected={item.id === value}
|
wrap: true,
|
||||||
onRenderItem={onRenderItem as any}
|
onRender(item: T, selected: boolean, index: number) {
|
||||||
onSelect={handleSelect as any}
|
return onRenderItem ? (
|
||||||
/>
|
onRenderItem(item, selected, index)
|
||||||
));
|
) : (
|
||||||
|
<DataListItem
|
||||||
|
title={item.title}
|
||||||
|
description={item.description}
|
||||||
|
enableArrow={enableArrow}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[onRenderItem, enableArrow],
|
||||||
|
);
|
||||||
|
|
||||||
return scrollable ? (
|
useEffect(
|
||||||
<Layout.Container
|
function updateSelection() {
|
||||||
style={style}
|
return selection.subscribe((valueFromAtom) => {
|
||||||
className={className}
|
const m = tableManagerRef.current;
|
||||||
borderTop
|
if (!m) {
|
||||||
borderBottom
|
return;
|
||||||
grow>
|
}
|
||||||
<Layout.ScrollContainer vertical>{renderedItems}</Layout.ScrollContainer>
|
if (!valueFromAtom && m.getSelectedItem()) {
|
||||||
</Layout.Container>
|
m.clearSelection();
|
||||||
) : (
|
} else if (valueFromAtom && m.getSelectedItem()?.id !== valueFromAtom) {
|
||||||
<Layout.Container style={style} className={className} borderTop>
|
// find valueFromAtom in the selection
|
||||||
{renderedItems}
|
m.selectItemById(valueFromAtom);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[selection, tableManagerRef],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout.Container style={style} className={className} grow>
|
||||||
|
<DataTable<any>
|
||||||
|
{...tableProps}
|
||||||
|
tableManagerRef={tableManagerRef}
|
||||||
|
records={items}
|
||||||
|
recordsKey="id"
|
||||||
|
columns={dataListColumns}
|
||||||
|
onSelect={handleSelect}
|
||||||
|
/>
|
||||||
</Layout.Container>
|
</Layout.Container>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
DataList.defaultProps = {
|
DataList.defaultProps = {
|
||||||
type: 'default',
|
type: 'default',
|
||||||
scrollable: false,
|
scrollable: true,
|
||||||
onRenderItem: defaultItemRenderer,
|
enableSearchbar: false,
|
||||||
|
enableColumnHeaders: false,
|
||||||
|
enableArrow: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
function defaultItemRenderer(item: Item, _selected: boolean) {
|
const DataListItem = memo(
|
||||||
return <DataListItem title={item.title} description={item.description} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DataListItemWrapper = memo(
|
|
||||||
({
|
({
|
||||||
item,
|
title,
|
||||||
onRenderItem,
|
description,
|
||||||
onSelect,
|
enableArrow,
|
||||||
selected,
|
|
||||||
}: {
|
}: {
|
||||||
item: Item;
|
// TODO: add icon support
|
||||||
onRenderItem: typeof defaultItemRenderer;
|
title: string;
|
||||||
onSelect: (id: string, item: Item) => void;
|
description?: string;
|
||||||
selected: boolean;
|
enableArrow?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Layout.Container
|
<Layout.Horizontal center grow shrink padv>
|
||||||
pad
|
<Layout.Container grow shrink>
|
||||||
borderBottom
|
<Text strong ellipsis>
|
||||||
key={item.id}
|
{DataFormatter.format(title)}
|
||||||
style={{
|
</Text>
|
||||||
background: selected ? theme.backgroundWash : undefined,
|
{description != null && (
|
||||||
borderLeft: selected
|
<Text type="secondary" ellipsis>
|
||||||
? `4px solid ${theme.primaryColor}`
|
{DataFormatter.format(description)}
|
||||||
: `4px solid transparent`,
|
</Text>
|
||||||
}}
|
)}
|
||||||
onClick={() => {
|
</Layout.Container>
|
||||||
onSelect(item.id, item);
|
{enableArrow && (
|
||||||
}}>
|
<ArrowWrapper>
|
||||||
{onRenderItem(item, selected)}
|
<RightOutlined />
|
||||||
</Layout.Container>
|
</ArrowWrapper>
|
||||||
|
)}
|
||||||
|
</Layout.Horizontal>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const DataListItem = memo(
|
const ArrowWrapper = styled.div({
|
||||||
({title, description}: {title: string; description?: string}) => {
|
flex: 0,
|
||||||
return (
|
paddingLeft: theme.space.small,
|
||||||
<>
|
'.anticon': {
|
||||||
<Text strong>{DataFormatter.format(title)}</Text>
|
lineHeight: '14px',
|
||||||
{description != null && (
|
|
||||||
<Text type="secondary">{DataFormatter.format(description)}</Text>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ const Container = styled.div<ContainerProps>(
|
|||||||
gap: normalizeSpace(gap, theme.space.small),
|
gap: normalizeSpace(gap, theme.space.small),
|
||||||
|
|
||||||
minWidth: shrink ? 0 : undefined,
|
minWidth: shrink ? 0 : undefined,
|
||||||
|
maxWidth: shrink ? '100%' : undefined,
|
||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ export function MasterDetail<T extends object>({
|
|||||||
|
|
||||||
const table = (
|
const table = (
|
||||||
<DataTable<T>
|
<DataTable<T>
|
||||||
autoScroll
|
enableAutoScroll
|
||||||
{...tableProps}
|
{...tableProps}
|
||||||
dataSource={dataSource as any}
|
dataSource={dataSource as any}
|
||||||
records={records!}
|
records={records!}
|
||||||
|
|||||||
@@ -68,8 +68,7 @@ type DataSourceProps<T extends object, C> = {
|
|||||||
offset: number,
|
offset: number,
|
||||||
): void;
|
): void;
|
||||||
onUpdateAutoScroll?(autoScroll: boolean): void;
|
onUpdateAutoScroll?(autoScroll: boolean): void;
|
||||||
emptyRenderer?(dataSource: DataSource<T>): React.ReactElement;
|
emptyRenderer?: null | ((dataSource: DataSource<T>) => React.ReactElement);
|
||||||
_testHeight?: number; // exposed for unit testing only
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -51,30 +51,45 @@ import {useInUnitTest} from '../../utils/useInUnitTest()';
|
|||||||
|
|
||||||
interface DataTableBaseProps<T = any> {
|
interface DataTableBaseProps<T = any> {
|
||||||
columns: DataTableColumn<T>[];
|
columns: DataTableColumn<T>[];
|
||||||
|
enableSearchbar?: boolean;
|
||||||
autoScroll?: boolean;
|
enableAutoScroll?: boolean;
|
||||||
|
enableColumnHeaders?: boolean;
|
||||||
|
enableMultiSelect?: boolean;
|
||||||
|
// if set (the default) will grow and become scrollable. Otherwise will use natural size
|
||||||
|
scrollable?: boolean;
|
||||||
extraActions?: React.ReactElement;
|
extraActions?: React.ReactElement;
|
||||||
onSelect?(record: T | undefined, records: T[]): void;
|
onSelect?(record: T | undefined, records: T[]): void;
|
||||||
onRowStyle?(record: T): CSSProperties | undefined;
|
onRowStyle?(record: T): CSSProperties | undefined;
|
||||||
// multiselect?: true
|
|
||||||
tableManagerRef?: RefObject<DataTableManager<T> | undefined>; // Actually we want a MutableRefObject, but that is not what React.createRef() returns, and we don't want to put the burden on the plugin dev to cast it...
|
tableManagerRef?: RefObject<DataTableManager<T> | undefined>; // Actually we want a MutableRefObject, but that is not what React.createRef() returns, and we don't want to put the burden on the plugin dev to cast it...
|
||||||
onCopyRows?(records: T[]): string;
|
onCopyRows?(records: T[]): string;
|
||||||
onContextMenu?: (selection: undefined | T) => React.ReactElement;
|
onContextMenu?: (selection: undefined | T) => React.ReactElement;
|
||||||
searchbar?: boolean;
|
onRenderEmpty?:
|
||||||
scrollable?: boolean;
|
| null
|
||||||
|
| ((dataSource?: DataSource<T, any, any>) => React.ReactElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ItemRenderer<T> = (
|
||||||
|
item: T,
|
||||||
|
selected: boolean,
|
||||||
|
index: number,
|
||||||
|
) => React.ReactNode;
|
||||||
|
|
||||||
type DataTableInput<T = any> =
|
type DataTableInput<T = any> =
|
||||||
| {dataSource: DataSource<T, any, any>; records?: undefined}
|
|
||||||
| {
|
| {
|
||||||
records: T[];
|
dataSource: DataSource<T, any, any>;
|
||||||
|
records?: undefined;
|
||||||
|
recordsKey?: undefined;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
records: readonly T[];
|
||||||
|
recordsKey?: keyof T;
|
||||||
dataSource?: undefined;
|
dataSource?: undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DataTableColumn<T = any> = {
|
export type DataTableColumn<T = any> = {
|
||||||
key: keyof T & string;
|
key: keyof T & string;
|
||||||
// possible future extension: getValue(row) (and free-form key) to support computed columns
|
// possible future extension: getValue(row) (and free-form key) to support computed columns
|
||||||
onRender?: (row: T) => React.ReactNode;
|
onRender?: (row: T, selected: boolean, index: number) => React.ReactNode;
|
||||||
formatters?: Formatter[] | Formatter;
|
formatters?: Formatter[] | Formatter;
|
||||||
title?: string;
|
title?: string;
|
||||||
width?: number | Percentage | undefined; // undefined: use all remaining width
|
width?: number | Percentage | undefined; // undefined: use all remaining width
|
||||||
@@ -89,7 +104,7 @@ export type DataTableColumn<T = any> = {
|
|||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface RenderContext<T = any> {
|
export interface TableRowRenderContext<T = any> {
|
||||||
columns: DataTableColumn<T>[];
|
columns: DataTableColumn<T>[];
|
||||||
onMouseEnter(
|
onMouseEnter(
|
||||||
e: React.MouseEvent<HTMLDivElement>,
|
e: React.MouseEvent<HTMLDivElement>,
|
||||||
@@ -101,6 +116,7 @@ export interface RenderContext<T = any> {
|
|||||||
item: T,
|
item: T,
|
||||||
itemId: number,
|
itemId: number,
|
||||||
): void;
|
): void;
|
||||||
|
onRowStyle?(item: T): React.CSSProperties | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DataTableProps<T> = DataTableInput<T> & DataTableBaseProps<T>;
|
export type DataTableProps<T> = DataTableInput<T> & DataTableBaseProps<T>;
|
||||||
@@ -116,6 +132,7 @@ export function DataTable<T extends object>(
|
|||||||
useAssertStableRef(props.columns, 'columns');
|
useAssertStableRef(props.columns, 'columns');
|
||||||
useAssertStableRef(onCopyRows, 'onCopyRows');
|
useAssertStableRef(onCopyRows, 'onCopyRows');
|
||||||
useAssertStableRef(onContextMenu, 'onContextMenu');
|
useAssertStableRef(onContextMenu, 'onContextMenu');
|
||||||
|
|
||||||
const isUnitTest = useInUnitTest();
|
const isUnitTest = useInUnitTest();
|
||||||
|
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
@@ -131,7 +148,7 @@ export function DataTable<T extends object>(
|
|||||||
onSelect,
|
onSelect,
|
||||||
scope,
|
scope,
|
||||||
virtualizerRef,
|
virtualizerRef,
|
||||||
autoScroll: props.autoScroll,
|
autoScroll: props.enableAutoScroll,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -154,7 +171,7 @@ export function DataTable<T extends object>(
|
|||||||
[columns],
|
[columns],
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderingConfig = useMemo<RenderContext<T>>(() => {
|
const renderingConfig = useMemo<TableRowRenderContext<T>>(() => {
|
||||||
let startIndex = 0;
|
let startIndex = 0;
|
||||||
return {
|
return {
|
||||||
columns: visibleColumns,
|
columns: visibleColumns,
|
||||||
@@ -185,14 +202,15 @@ export function DataTable<T extends object>(
|
|||||||
document.addEventListener('mouseup', onStopDragSelecting);
|
document.addEventListener('mouseup', onStopDragSelecting);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onRowStyle,
|
||||||
};
|
};
|
||||||
}, [visibleColumns, tableManager]);
|
}, [visibleColumns, tableManager, onRowStyle]);
|
||||||
|
|
||||||
const itemRenderer = useCallback(
|
const itemRenderer = useCallback(
|
||||||
function itemRenderer(
|
function itemRenderer(
|
||||||
record: T,
|
record: T,
|
||||||
index: number,
|
index: number,
|
||||||
renderContext: RenderContext<T>,
|
renderContext: TableRowRenderContext<T>,
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<TableRow
|
<TableRow
|
||||||
@@ -357,11 +375,11 @@ export function DataTable<T extends object>(
|
|||||||
|
|
||||||
const onUpdateAutoScroll = useCallback(
|
const onUpdateAutoScroll = useCallback(
|
||||||
(autoScroll: boolean) => {
|
(autoScroll: boolean) => {
|
||||||
if (props.autoScroll) {
|
if (props.enableAutoScroll) {
|
||||||
dispatch({type: 'setAutoScroll', autoScroll});
|
dispatch({type: 'setAutoScroll', autoScroll});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[props.autoScroll],
|
[props.enableAutoScroll],
|
||||||
);
|
);
|
||||||
|
|
||||||
/** Context menu */
|
/** Context menu */
|
||||||
@@ -409,7 +427,7 @@ export function DataTable<T extends object>(
|
|||||||
|
|
||||||
const header = (
|
const header = (
|
||||||
<Layout.Container>
|
<Layout.Container>
|
||||||
{props.searchbar !== false && (
|
{props.enableSearchbar && (
|
||||||
<TableSearch
|
<TableSearch
|
||||||
searchValue={searchValue}
|
searchValue={searchValue}
|
||||||
useRegex={tableState.useRegex}
|
useRegex={tableState.useRegex}
|
||||||
@@ -418,56 +436,61 @@ export function DataTable<T extends object>(
|
|||||||
extraActions={props.extraActions}
|
extraActions={props.extraActions}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<TableHead
|
{props.enableColumnHeaders && (
|
||||||
visibleColumns={visibleColumns}
|
<TableHead
|
||||||
dispatch={dispatch as any}
|
visibleColumns={visibleColumns}
|
||||||
sorting={sorting}
|
dispatch={dispatch as any}
|
||||||
scrollbarSize={
|
sorting={sorting}
|
||||||
props.scrollable === false
|
scrollbarSize={
|
||||||
? 0
|
props.scrollable
|
||||||
: 15 /* width on MacOS: TODO, determine dynamically */
|
? 0
|
||||||
}
|
: 15 /* width on MacOS: TODO, determine dynamically */
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Layout.Container>
|
||||||
|
);
|
||||||
|
|
||||||
|
const emptyRenderer =
|
||||||
|
props.onRenderEmpty === undefined
|
||||||
|
? props.onRenderEmpty
|
||||||
|
: props.onRenderEmpty;
|
||||||
|
const mainSection = props.scrollable ? (
|
||||||
|
<Layout.Top>
|
||||||
|
{header}
|
||||||
|
<DataSourceRenderer<T, TableRowRenderContext<T>>
|
||||||
|
dataSource={dataSource}
|
||||||
|
autoScroll={tableState.autoScroll && !dragging.current}
|
||||||
|
useFixedRowHeight={!tableState.usesWrapping}
|
||||||
|
defaultRowHeight={DEFAULT_ROW_HEIGHT}
|
||||||
|
context={renderingConfig}
|
||||||
|
itemRenderer={itemRenderer}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
virtualizerRef={virtualizerRef}
|
||||||
|
onRangeChange={onRangeChange}
|
||||||
|
onUpdateAutoScroll={onUpdateAutoScroll}
|
||||||
|
emptyRenderer={emptyRenderer}
|
||||||
|
/>
|
||||||
|
</Layout.Top>
|
||||||
|
) : (
|
||||||
|
<Layout.Container>
|
||||||
|
{header}
|
||||||
|
<StaticDataSourceRenderer<T, TableRowRenderContext<T>>
|
||||||
|
dataSource={dataSource}
|
||||||
|
useFixedRowHeight={!tableState.usesWrapping}
|
||||||
|
defaultRowHeight={DEFAULT_ROW_HEIGHT}
|
||||||
|
context={renderingConfig}
|
||||||
|
itemRenderer={itemRenderer}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
emptyRenderer={emptyRenderer}
|
||||||
/>
|
/>
|
||||||
</Layout.Container>
|
</Layout.Container>
|
||||||
);
|
);
|
||||||
|
|
||||||
const mainSection =
|
|
||||||
props.scrollable !== false ? (
|
|
||||||
<Layout.Top>
|
|
||||||
{header}
|
|
||||||
<DataSourceRenderer<T, RenderContext<T>>
|
|
||||||
dataSource={dataSource}
|
|
||||||
autoScroll={tableState.autoScroll && !dragging.current}
|
|
||||||
useFixedRowHeight={!tableState.usesWrapping}
|
|
||||||
defaultRowHeight={DEFAULT_ROW_HEIGHT}
|
|
||||||
context={renderingConfig}
|
|
||||||
itemRenderer={itemRenderer}
|
|
||||||
onKeyDown={onKeyDown}
|
|
||||||
virtualizerRef={virtualizerRef}
|
|
||||||
onRangeChange={onRangeChange}
|
|
||||||
onUpdateAutoScroll={onUpdateAutoScroll}
|
|
||||||
emptyRenderer={emptyRenderer}
|
|
||||||
/>
|
|
||||||
</Layout.Top>
|
|
||||||
) : (
|
|
||||||
<Layout.Container>
|
|
||||||
{header}
|
|
||||||
<StaticDataSourceRenderer<T, RenderContext<T>>
|
|
||||||
dataSource={dataSource}
|
|
||||||
useFixedRowHeight={!tableState.usesWrapping}
|
|
||||||
defaultRowHeight={DEFAULT_ROW_HEIGHT}
|
|
||||||
context={renderingConfig}
|
|
||||||
itemRenderer={itemRenderer}
|
|
||||||
onKeyDown={onKeyDown}
|
|
||||||
emptyRenderer={emptyRenderer}
|
|
||||||
/>
|
|
||||||
</Layout.Container>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout.Container grow={props.scrollable !== false}>
|
<Layout.Container grow={props.scrollable}>
|
||||||
{mainSection}
|
{mainSection}
|
||||||
{props.autoScroll && (
|
{props.enableAutoScroll && (
|
||||||
<AutoScroller>
|
<AutoScroller>
|
||||||
<PushpinFilled
|
<PushpinFilled
|
||||||
style={{
|
style={{
|
||||||
@@ -484,6 +507,15 @@ export function DataTable<T extends object>(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DataTable.defaultProps = {
|
||||||
|
scrollable: true,
|
||||||
|
enableSearchbar: true,
|
||||||
|
enableAutoScroll: false,
|
||||||
|
enableColumnHeaders: true,
|
||||||
|
eanbleMultiSelect: true,
|
||||||
|
onRenderEmpty: emptyRenderer,
|
||||||
|
} as Partial<DataTableProps<any>>;
|
||||||
|
|
||||||
/* eslint-disable react-hooks/rules-of-hooks */
|
/* eslint-disable react-hooks/rules-of-hooks */
|
||||||
function normalizeDataSourceInput<T>(props: DataTableInput<T>): DataSource<T> {
|
function normalizeDataSourceInput<T>(props: DataTableInput<T>): DataSource<T> {
|
||||||
if (props.dataSource) {
|
if (props.dataSource) {
|
||||||
@@ -491,7 +523,7 @@ function normalizeDataSourceInput<T>(props: DataTableInput<T>): DataSource<T> {
|
|||||||
}
|
}
|
||||||
if (props.records) {
|
if (props.records) {
|
||||||
const [dataSource] = useState(() => {
|
const [dataSource] = useState(() => {
|
||||||
const ds = new DataSource<T>(undefined);
|
const ds = new DataSource<T>(props.recordsKey);
|
||||||
syncRecordsToDataSource(ds, props.records);
|
syncRecordsToDataSource(ds, props.records);
|
||||||
return ds;
|
return ds;
|
||||||
});
|
});
|
||||||
@@ -507,7 +539,7 @@ function normalizeDataSourceInput<T>(props: DataTableInput<T>): DataSource<T> {
|
|||||||
}
|
}
|
||||||
/* eslint-enable */
|
/* eslint-enable */
|
||||||
|
|
||||||
function syncRecordsToDataSource<T>(ds: DataSource<T>, records: T[]) {
|
function syncRecordsToDataSource<T>(ds: DataSource<T>, records: readonly T[]) {
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
ds.clear();
|
ds.clear();
|
||||||
// TODO: optimize in the case we're only dealing with appends or replacements
|
// TODO: optimize in the case we're only dealing with appends or replacements
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ type DataSourceProps<T extends object, C> = {
|
|||||||
defaultRowHeight: number;
|
defaultRowHeight: number;
|
||||||
onKeyDown?: React.KeyboardEventHandler<HTMLDivElement>;
|
onKeyDown?: React.KeyboardEventHandler<HTMLDivElement>;
|
||||||
onUpdateAutoScroll?(autoScroll: boolean): void;
|
onUpdateAutoScroll?(autoScroll: boolean): void;
|
||||||
emptyRenderer?(dataSource: DataSource<T>): React.ReactElement;
|
emptyRenderer?: null | ((dataSource: DataSource<T>) => React.ReactElement);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
import React, {CSSProperties, memo} from 'react';
|
import React, {CSSProperties, memo} from 'react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import {theme} from '../theme';
|
import {theme} from '../theme';
|
||||||
import type {RenderContext} from './DataTable';
|
import type {TableRowRenderContext} from './DataTable';
|
||||||
import {Width} from '../../utils/widthUtils';
|
import {Width} from '../../utils/widthUtils';
|
||||||
import {DataFormatter} from '../DataFormatter';
|
import {DataFormatter} from '../DataFormatter';
|
||||||
|
|
||||||
@@ -92,37 +92,35 @@ const TableBodyColumnContainer = styled.div<{
|
|||||||
}));
|
}));
|
||||||
TableBodyColumnContainer.displayName = 'TableRow:TableBodyColumnContainer';
|
TableBodyColumnContainer.displayName = 'TableRow:TableBodyColumnContainer';
|
||||||
|
|
||||||
type Props = {
|
type TableRowProps<T> = {
|
||||||
config: RenderContext<any>;
|
config: TableRowRenderContext<any>;
|
||||||
highlighted: boolean;
|
highlighted: boolean;
|
||||||
record: any;
|
record: T;
|
||||||
itemIndex: number;
|
itemIndex: number;
|
||||||
style?: CSSProperties;
|
style?: CSSProperties;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TableRow = memo(function TableRow({
|
export const TableRow = memo(function TableRow<T>({
|
||||||
record,
|
record,
|
||||||
itemIndex,
|
itemIndex,
|
||||||
highlighted,
|
highlighted,
|
||||||
style,
|
|
||||||
config,
|
config,
|
||||||
}: Props) {
|
}: TableRowProps<T>) {
|
||||||
return (
|
return (
|
||||||
<TableBodyRowContainer
|
<TableBodyRowContainer
|
||||||
highlighted={highlighted}
|
highlighted={highlighted}
|
||||||
data-key={record.key}
|
|
||||||
onMouseDown={(e) => {
|
onMouseDown={(e) => {
|
||||||
config.onMouseDown(e, record, itemIndex);
|
config.onMouseDown(e, record, itemIndex);
|
||||||
}}
|
}}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
config.onMouseEnter(e, record, itemIndex);
|
config.onMouseEnter(e, record, itemIndex);
|
||||||
}}
|
}}
|
||||||
style={style}>
|
style={config.onRowStyle?.(record)}>
|
||||||
{config.columns
|
{config.columns
|
||||||
.filter((col) => col.visible)
|
.filter((col) => col.visible)
|
||||||
.map((col) => {
|
.map((col) => {
|
||||||
const value = (col as any).onRender
|
const value = col.onRender
|
||||||
? (col as any).onRender(record)
|
? (col as any).onRender(record, highlighted, itemIndex) // TODO: ever used?
|
||||||
: DataFormatter.format((record as any)[col.key], col.formatters);
|
: DataFormatter.format((record as any)[col.key], col.formatters);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -445,7 +445,7 @@ export function Component() {
|
|||||||
records={sidebarRows(id)}
|
records={sidebarRows(id)}
|
||||||
columns={cpuSidebarColumns}
|
columns={cpuSidebarColumns}
|
||||||
scrollable={false}
|
scrollable={false}
|
||||||
searchbar={false}
|
enableSearchbar={false}
|
||||||
/>
|
/>
|
||||||
</Layout.Container>
|
</Layout.Container>
|
||||||
</DetailSidebar>
|
</DetailSidebar>
|
||||||
@@ -511,7 +511,7 @@ export function Component() {
|
|||||||
scrollable={false}
|
scrollable={false}
|
||||||
onSelect={setSelected}
|
onSelect={setSelected}
|
||||||
onRowStyle={getRowStyle}
|
onRowStyle={getRowStyle}
|
||||||
searchbar={false}
|
enableSearchbar={false}
|
||||||
/>
|
/>
|
||||||
{renderCPUSidebar()}
|
{renderCPUSidebar()}
|
||||||
{renderThermalSidebar()}
|
{renderThermalSidebar()}
|
||||||
|
|||||||
@@ -63,12 +63,12 @@ export default React.memo((props: {structure: Structure}) => {
|
|||||||
<DataTable<{[key: string]: Value}>
|
<DataTable<{[key: string]: Value}>
|
||||||
records={rowObjs}
|
records={rowObjs}
|
||||||
columns={columnObjs}
|
columns={columnObjs}
|
||||||
searchbar={false}
|
enableSearchbar={false}
|
||||||
/>
|
/>
|
||||||
<DataTable<{[key: string]: Value}>
|
<DataTable<{[key: string]: Value}>
|
||||||
records={indexRowObjs}
|
records={indexRowObjs}
|
||||||
columns={indexColumnObjs}
|
columns={indexColumnObjs}
|
||||||
searchbar={false}
|
enableSearchbar={false}
|
||||||
/>
|
/>
|
||||||
</Layout.Top>
|
</Layout.Top>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -226,7 +226,7 @@ export function Component() {
|
|||||||
<DataTable<ExtendedLogEntry>
|
<DataTable<ExtendedLogEntry>
|
||||||
dataSource={plugin.rows}
|
dataSource={plugin.rows}
|
||||||
columns={plugin.columns}
|
columns={plugin.columns}
|
||||||
autoScroll
|
enableAutoScroll
|
||||||
onRowStyle={getRowStyle}
|
onRowStyle={getRowStyle}
|
||||||
extraActions={
|
extraActions={
|
||||||
plugin.isConnected ? (
|
plugin.isConnected ? (
|
||||||
|
|||||||
@@ -254,7 +254,8 @@ The benefit of `useValue(instance.rows)` over using `rows.get()`, is that the fi
|
|||||||
Since both `usePlugin` and `useValue` are hooks, they usual React rules for them apply; they need to be called unconditionally.
|
Since both `usePlugin` and `useValue` are hooks, they usual React rules for them apply; they need to be called unconditionally.
|
||||||
So it is recommended to put them at the top of your component body.
|
So it is recommended to put them at the top of your component body.
|
||||||
Both hooks can not only be used in the root `Component`, but also in any other component in your plugin component tree.
|
Both hooks can not only be used in the root `Component`, but also in any other component in your plugin component tree.
|
||||||
So it is not necessary to grab all the data at the root, or pass down the `instance` to all child components.
|
So it is not necessary to grab all the data at the root and pass it down using props.
|
||||||
|
Using `useValue` as deep in the component tree as possible will benefit performance.
|
||||||
|
|
||||||
Finally (`(4)`) we render the data we have. The details have been left out here, as from here it is just idiomatic React code.
|
Finally (`(4)`) we render the data we have. The details have been left out here, as from here it is just idiomatic React code.
|
||||||
The source of the other `MammalCard` component can be found [here](https://github.com/facebook/flipper/blob/master/desktop/plugins/public/seamammals/src/index.tsx#L113-L165).
|
The source of the other `MammalCard` component can be found [here](https://github.com/facebook/flipper/blob/master/desktop/plugins/public/seamammals/src/index.tsx#L113-L165).
|
||||||
|
|||||||
Reference in New Issue
Block a user