Initial logs with datasource / datatable setup
Summary: First rudementary setup of DataTable component that follows a data source. Initially used react-virtuose library, but it performed really badly by doing expensive layout shifts and having troublesome scroll handling. Switched to react-virtual library, which is a bit more level, but much more efficient, and the source code is actually understandable :) Features: - hook up to window events of datasource - high and low prio rendering, based on where the change is happening (should be optimized further) - sticky scrolling support - initial column configuration (custom rendering, styling, columns etc will follow in next diffs) Reviewed By: nikoant Differential Revision: D26175665 fbshipit-source-id: 224be13b1b32d35e7e01c1dc4198811e2af31102
This commit is contained in:
committed by
Facebook GitHub Bot
parent
5b76a0c717
commit
86ad413669
@@ -13,7 +13,8 @@
|
|||||||
"@emotion/react": "^11.1.1",
|
"@emotion/react": "^11.1.1",
|
||||||
"immer": "^8.0.1",
|
"immer": "^8.0.1",
|
||||||
"lodash": "^4.17.20",
|
"lodash": "^4.17.20",
|
||||||
"react-element-to-jsx-string": "^14.3.2"
|
"react-element-to-jsx-string": "^14.3.2",
|
||||||
|
"react-virtual": "^2.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^26.0.20",
|
"@types/jest": "^26.0.20",
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ test('Correct top level API exposed', () => {
|
|||||||
// Note, all `exposedAPIs` should be documented in `flipper-plugin.mdx`
|
// Note, all `exposedAPIs` should be documented in `flipper-plugin.mdx`
|
||||||
expect(exposedAPIs.sort()).toMatchInlineSnapshot(`
|
expect(exposedAPIs.sort()).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
|
"DataSource",
|
||||||
|
"DataTable",
|
||||||
"Layout",
|
"Layout",
|
||||||
"NUX",
|
"NUX",
|
||||||
"TestUtils",
|
"TestUtils",
|
||||||
@@ -52,6 +54,7 @@ test('Correct top level API exposed', () => {
|
|||||||
expect(exposedTypes.sort()).toMatchInlineSnapshot(`
|
expect(exposedTypes.sort()).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
"Atom",
|
"Atom",
|
||||||
|
"DataTableColumn",
|
||||||
"DefaultKeyboardAction",
|
"DefaultKeyboardAction",
|
||||||
"Device",
|
"Device",
|
||||||
"DeviceLogEntry",
|
"DeviceLogEntry",
|
||||||
|
|||||||
@@ -71,7 +71,9 @@ export {
|
|||||||
} from './utils/Logger';
|
} from './utils/Logger';
|
||||||
export {Idler} from './utils/Idler';
|
export {Idler} from './utils/Idler';
|
||||||
|
|
||||||
export {createDataSource} from './state/datasource/DataSource';
|
export {createDataSource, DataSource} from './state/datasource/DataSource';
|
||||||
|
|
||||||
|
export {DataTable, DataTableColumn} from './ui/datatable/DataTable';
|
||||||
|
|
||||||
// It's not ideal that this exists in flipper-plugin sources directly,
|
// It's not ideal that this exists in flipper-plugin sources directly,
|
||||||
// but is the least pain for plugin authors.
|
// but is the least pain for plugin authors.
|
||||||
|
|||||||
@@ -70,9 +70,10 @@ type OutputChange =
|
|||||||
newCount: number;
|
newCount: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: remove class, export interface instead
|
||||||
export class DataSource<
|
export class DataSource<
|
||||||
T,
|
T,
|
||||||
KEY extends keyof T,
|
KEY extends keyof T = any,
|
||||||
KEY_TYPE extends string | number | never = ExtractKeyType<T, KEY>
|
KEY_TYPE extends string | number | never = ExtractKeyType<T, KEY>
|
||||||
> {
|
> {
|
||||||
private nextId = 0;
|
private nextId = 0;
|
||||||
@@ -267,6 +268,9 @@ export class DataSource<
|
|||||||
setOutputChangeListener(
|
setOutputChangeListener(
|
||||||
listener: typeof DataSource['prototype']['outputChangeListener'],
|
listener: typeof DataSource['prototype']['outputChangeListener'],
|
||||||
) {
|
) {
|
||||||
|
if (this.outputChangeListener && listener) {
|
||||||
|
console.warn('outputChangeListener already set');
|
||||||
|
}
|
||||||
this.outputChangeListener = listener;
|
this.outputChangeListener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,11 +307,14 @@ export class DataSource<
|
|||||||
* The clear operation removes any records stored, but will keep the current view preferences such as sorting and filtering
|
* The clear operation removes any records stored, but will keep the current view preferences such as sorting and filtering
|
||||||
*/
|
*/
|
||||||
clear() {
|
clear() {
|
||||||
|
this.windowStart = 0;
|
||||||
|
this.windowEnd = 0;
|
||||||
this._records = [];
|
this._records = [];
|
||||||
this._recordsById = new Map();
|
this._recordsById = new Map();
|
||||||
this.idToIndex = new Map();
|
this.idToIndex = new Map();
|
||||||
this.dataUpdateQueue = [];
|
this.dataUpdateQueue = [];
|
||||||
this.output = [];
|
this.output = [];
|
||||||
|
this.notifyReset(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -372,6 +379,13 @@ export class DataSource<
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
notifyReset(count: number) {
|
||||||
|
this.outputChangeListener?.({
|
||||||
|
type: 'reset',
|
||||||
|
newCount: count,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
processEvents() {
|
processEvents() {
|
||||||
const events = this.dataUpdateQueue.splice(0);
|
const events = this.dataUpdateQueue.splice(0);
|
||||||
events.forEach(this.processEvent);
|
events.forEach(this.processEvent);
|
||||||
|
|||||||
272
desktop/flipper-plugin/src/ui/datatable/DataTable.tsx
Normal file
272
desktop/flipper-plugin/src/ui/datatable/DataTable.tsx
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*
|
||||||
|
* @format
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, {
|
||||||
|
memo,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
useLayoutEffect,
|
||||||
|
} from 'react';
|
||||||
|
import {TableRow, DEFAULT_ROW_HEIGHT} from './TableRow';
|
||||||
|
import {Property} from 'csstype';
|
||||||
|
import {DataSource} from '../../state/datasource/DataSource';
|
||||||
|
import {useVirtual} from 'react-virtual';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import {theme} from '../theme';
|
||||||
|
|
||||||
|
// how fast we update if updates are low-prio (e.g. out of window and not super significant)
|
||||||
|
const DEBOUNCE = 500; //ms
|
||||||
|
|
||||||
|
interface DataTableProps<T extends object> {
|
||||||
|
columns: DataTableColumn<T>[];
|
||||||
|
dataSource: DataSource<T, any, any>;
|
||||||
|
zebra?: boolean;
|
||||||
|
autoScroll?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DataTableColumn<T> = (
|
||||||
|
| {
|
||||||
|
// existing data
|
||||||
|
key: keyof T;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
// derived data / custom rendering
|
||||||
|
key: string;
|
||||||
|
onRender?: (row: T) => React.ReactNode;
|
||||||
|
}
|
||||||
|
) & {
|
||||||
|
label?: string;
|
||||||
|
width?: number | '*';
|
||||||
|
wrap?: boolean;
|
||||||
|
align?: Property.JustifyContent;
|
||||||
|
defaultVisible?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface RenderingConfig<T extends object> {
|
||||||
|
columns: DataTableColumn<T>[];
|
||||||
|
zebra: boolean;
|
||||||
|
onMouseDown: (e: React.MouseEvent, row: T) => void;
|
||||||
|
onMouseEnter: (e: React.MouseEvent, row: T) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum UpdatePrio {
|
||||||
|
NONE,
|
||||||
|
LOW,
|
||||||
|
HIGH,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DataTable: <T extends object>(
|
||||||
|
props: DataTableProps<T>,
|
||||||
|
) => React.ReactElement = memo(function DataSourceRenderer(
|
||||||
|
props: DataTableProps<any>,
|
||||||
|
) {
|
||||||
|
const {dataSource} = props;
|
||||||
|
|
||||||
|
const renderingConfig = useMemo(() => {
|
||||||
|
return {
|
||||||
|
columns: props.columns,
|
||||||
|
zebra: props.zebra !== false,
|
||||||
|
onMouseDown() {
|
||||||
|
// TODO:
|
||||||
|
},
|
||||||
|
onMouseEnter() {
|
||||||
|
// TODO:
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}, [props.columns, props.zebra]);
|
||||||
|
|
||||||
|
const usesWrapping = useMemo(() => props.columns.some((col) => col.wrap), [
|
||||||
|
props.columns,
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Virtualization
|
||||||
|
*/
|
||||||
|
// render scheduling
|
||||||
|
const renderPending = useRef(UpdatePrio.NONE);
|
||||||
|
const lastRender = useRef(Date.now());
|
||||||
|
const setForceUpdate = useState(0)[1];
|
||||||
|
|
||||||
|
const parentRef = React.useRef<null | HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const virtualizer = useVirtual({
|
||||||
|
size: dataSource.output.length,
|
||||||
|
parentRef,
|
||||||
|
// eslint-disable-next-line
|
||||||
|
estimateSize: useCallback(() => DEFAULT_ROW_HEIGHT, []),
|
||||||
|
overscan: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
function subscribeToDataSource() {
|
||||||
|
const forceUpdate = () => {
|
||||||
|
if (unmounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setForceUpdate((x) => x + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
let unmounted = false;
|
||||||
|
let timeoutHandle: NodeJS.Timeout | undefined = undefined;
|
||||||
|
|
||||||
|
function rerender(prio: 1 | 2) {
|
||||||
|
if (renderPending.current >= prio) {
|
||||||
|
// already scheduled an update with equal or higher prio
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
renderPending.current = prio;
|
||||||
|
if (prio === UpdatePrio.LOW) {
|
||||||
|
// TODO: make DEBOUNCE depend on how big the relative change is
|
||||||
|
timeoutHandle = setTimeout(forceUpdate, DEBOUNCE);
|
||||||
|
} else {
|
||||||
|
// High
|
||||||
|
if (timeoutHandle) {
|
||||||
|
clearTimeout(timeoutHandle);
|
||||||
|
}
|
||||||
|
requestAnimationFrame(forceUpdate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dataSource.setOutputChangeListener((event) => {
|
||||||
|
switch (event.type) {
|
||||||
|
case 'reset':
|
||||||
|
rerender(UpdatePrio.HIGH);
|
||||||
|
break;
|
||||||
|
case 'shift':
|
||||||
|
// console.log(event.type, event.location);
|
||||||
|
if (event.location === 'in') {
|
||||||
|
rerender(UpdatePrio.HIGH);
|
||||||
|
} else {
|
||||||
|
// optimization: we don't want to listen to every count change, especially after window
|
||||||
|
// and in some cases before window
|
||||||
|
rerender(UpdatePrio.LOW);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'update':
|
||||||
|
// in visible range, so let's force update
|
||||||
|
rerender(UpdatePrio.HIGH);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unmounted = true;
|
||||||
|
dataSource.setOutputChangeListener(undefined);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[dataSource, setForceUpdate],
|
||||||
|
);
|
||||||
|
|
||||||
|
useLayoutEffect(function updateWindow() {
|
||||||
|
const start = virtualizer.virtualItems[0]?.index ?? 0;
|
||||||
|
const end = start + virtualizer.virtualItems.length;
|
||||||
|
dataSource.setWindow(start, end);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scrolling
|
||||||
|
*/
|
||||||
|
// if true, scroll if new items are appended
|
||||||
|
const followOutput = useRef(false);
|
||||||
|
// if true, the next scroll event will be fired as result of a size change,
|
||||||
|
// ignore it
|
||||||
|
const suppressScroll = useRef(false);
|
||||||
|
suppressScroll.current = true;
|
||||||
|
|
||||||
|
const onScroll = useCallback(() => {
|
||||||
|
// scroll event is firing as a result of painting new items?
|
||||||
|
if (suppressScroll.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const elem = parentRef.current!;
|
||||||
|
// make bottom 1/3 of screen sticky
|
||||||
|
if (elem.scrollTop < elem.scrollHeight - elem.clientHeight * 1.3) {
|
||||||
|
followOutput.current = false;
|
||||||
|
} else {
|
||||||
|
followOutput.current = true;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useLayoutEffect(function scrollToEnd() {
|
||||||
|
if (followOutput.current) {
|
||||||
|
virtualizer.scrollToIndex(
|
||||||
|
dataSource.output.length - 1,
|
||||||
|
/* smooth is not typed by react-virtual, but passed on to the DOM as it should*/
|
||||||
|
{
|
||||||
|
align: 'end',
|
||||||
|
behavior: 'smooth',
|
||||||
|
} as any,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render finalization
|
||||||
|
*/
|
||||||
|
useEffect(function renderCompleted() {
|
||||||
|
suppressScroll.current = false;
|
||||||
|
renderPending.current = UpdatePrio.NONE;
|
||||||
|
lastRender.current = Date.now();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rendering
|
||||||
|
*/
|
||||||
|
return (
|
||||||
|
<TableContainer onScroll={onScroll} ref={parentRef}>
|
||||||
|
<TableWindow height={virtualizer.totalSize}>
|
||||||
|
{virtualizer.virtualItems.map((virtualRow) => (
|
||||||
|
// the position properties always change, so they are not part of the TableRow to avoid invalidating the memoized render always.
|
||||||
|
// Also all row containers are renderd as part of same component to have 'less react' framework code in between*/}
|
||||||
|
<div
|
||||||
|
key={virtualRow.index}
|
||||||
|
className={virtualRow.index % 2 ? 'ListItemOdd' : 'ListItemEven'}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: '100%',
|
||||||
|
backgroundColor:
|
||||||
|
renderingConfig.zebra && virtualRow.index % 2
|
||||||
|
? theme.backgroundWash
|
||||||
|
: theme.backgroundDefault,
|
||||||
|
height: usesWrapping ? undefined : virtualRow.size,
|
||||||
|
transform: `translateY(${virtualRow.start}px)`,
|
||||||
|
}}
|
||||||
|
ref={usesWrapping ? virtualRow.measureRef : undefined}>
|
||||||
|
{
|
||||||
|
<TableRow
|
||||||
|
key={virtualRow.index}
|
||||||
|
config={renderingConfig}
|
||||||
|
row={dataSource.getItem(virtualRow.index)}
|
||||||
|
highlighted={false}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</TableWindow>
|
||||||
|
</TableContainer>
|
||||||
|
);
|
||||||
|
}) as any;
|
||||||
|
|
||||||
|
const TableContainer = styled.div({
|
||||||
|
overflowY: 'scroll',
|
||||||
|
overflowX: 'hidden',
|
||||||
|
display: 'flex',
|
||||||
|
flex: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const TableWindow = styled.div<{height: number}>(({height}) => ({
|
||||||
|
height,
|
||||||
|
position: 'relative',
|
||||||
|
width: '100%',
|
||||||
|
}));
|
||||||
122
desktop/flipper-plugin/src/ui/datatable/TableRow.tsx
Normal file
122
desktop/flipper-plugin/src/ui/datatable/TableRow.tsx
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*
|
||||||
|
* @format
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, {memo} from 'react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import {Property} from 'csstype';
|
||||||
|
import {theme} from 'flipper-plugin';
|
||||||
|
import {RenderingConfig} from './DataTable';
|
||||||
|
|
||||||
|
// heuristic for row estimation, should match any future styling updates
|
||||||
|
export const DEFAULT_ROW_HEIGHT = 24;
|
||||||
|
|
||||||
|
type TableBodyRowContainerProps = {
|
||||||
|
highlighted?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const backgroundColor = (props: TableBodyRowContainerProps) => {
|
||||||
|
if (props.highlighted) {
|
||||||
|
return theme.primaryColor;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TableBodyRowContainer = styled.div<TableBodyRowContainerProps>(
|
||||||
|
(props) => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
backgroundColor: backgroundColor(props),
|
||||||
|
color: props.highlighted ? theme.white : theme.primaryColor,
|
||||||
|
'& *': {
|
||||||
|
color: props.highlighted ? `${theme.white} !important` : undefined,
|
||||||
|
},
|
||||||
|
'& img': {
|
||||||
|
backgroundColor: props.highlighted
|
||||||
|
? `${theme.white} !important`
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
minHeight: DEFAULT_ROW_HEIGHT,
|
||||||
|
overflow: 'hidden',
|
||||||
|
width: '100%',
|
||||||
|
flexShrink: 0,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
TableBodyRowContainer.displayName = 'TableRow:TableBodyRowContainer';
|
||||||
|
|
||||||
|
const TableBodyColumnContainer = styled.div<{
|
||||||
|
width?: any;
|
||||||
|
multiline?: boolean;
|
||||||
|
justifyContent: Property.JustifyContent;
|
||||||
|
}>((props) => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexShrink: props.width === 'flex' ? 1 : 0,
|
||||||
|
flexGrow: props.width === 'flex' ? 1 : 0,
|
||||||
|
overflow: 'hidden',
|
||||||
|
padding: `0 ${theme.space.small}px`,
|
||||||
|
verticalAlign: 'top',
|
||||||
|
whiteSpace: props.multiline ? 'normal' : 'nowrap',
|
||||||
|
wordWrap: props.multiline ? 'break-word' : 'normal',
|
||||||
|
width: props.width === 'flex' ? undefined : props.width,
|
||||||
|
justifyContent: props.justifyContent,
|
||||||
|
}));
|
||||||
|
TableBodyColumnContainer.displayName = 'TableRow:TableBodyColumnContainer';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
config: RenderingConfig<any>;
|
||||||
|
highlighted: boolean;
|
||||||
|
row: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TableRow = memo(function TableRow(props: Props) {
|
||||||
|
const {config, highlighted, row} = props;
|
||||||
|
return (
|
||||||
|
<TableBodyRowContainer highlighted={highlighted} data-key={row.key}>
|
||||||
|
{config.columns.map((col) => {
|
||||||
|
const value = (col as any).onRender
|
||||||
|
? (col as any).onRender(row)
|
||||||
|
: (row as any)[col.key] ?? '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableBodyColumnContainer
|
||||||
|
key={col.key as string}
|
||||||
|
multiline={col.wrap}
|
||||||
|
justifyContent={col.align ? col.align : 'flex-start'}
|
||||||
|
width={normaliseColumnWidth(col.width)}>
|
||||||
|
{value}
|
||||||
|
</TableBodyColumnContainer>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBodyRowContainer>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
function normaliseColumnWidth(
|
||||||
|
width: string | number | null | undefined | '*',
|
||||||
|
): number | string {
|
||||||
|
if (width == null || width === '*') {
|
||||||
|
// default
|
||||||
|
return 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPercentage(width)) {
|
||||||
|
// percentage eg. 50%
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof width === 'number') {
|
||||||
|
// pixel width
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new TypeError(`Unknown value ${width} for table column width`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPercentage(width: any): boolean {
|
||||||
|
return typeof width === 'string' && width[width.length - 1] === '%';
|
||||||
|
}
|
||||||
@@ -1936,6 +1936,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@oclif/screen/-/screen-1.0.4.tgz#b740f68609dfae8aa71c3a6cab15d816407ba493"
|
resolved "https://registry.yarnpkg.com/@oclif/screen/-/screen-1.0.4.tgz#b740f68609dfae8aa71c3a6cab15d816407ba493"
|
||||||
integrity sha512-60CHpq+eqnTxLZQ4PGHYNwUX572hgpMHGPtTWMjdTMsAvlm69lZV/4ly6O3sAYkomo4NggGcomrDpBe34rxUqw==
|
integrity sha512-60CHpq+eqnTxLZQ4PGHYNwUX572hgpMHGPtTWMjdTMsAvlm69lZV/4ly6O3sAYkomo4NggGcomrDpBe34rxUqw==
|
||||||
|
|
||||||
|
"@reach/observe-rect@^1.1.0":
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@reach/observe-rect/-/observe-rect-1.2.0.tgz#d7a6013b8aafcc64c778a0ccb83355a11204d3b2"
|
||||||
|
integrity sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ==
|
||||||
|
|
||||||
"@sindresorhus/is@^0.14.0":
|
"@sindresorhus/is@^0.14.0":
|
||||||
version "0.14.0"
|
version "0.14.0"
|
||||||
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea"
|
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea"
|
||||||
@@ -10637,6 +10642,14 @@ react-transition-group@^4.4.1:
|
|||||||
loose-envify "^1.4.0"
|
loose-envify "^1.4.0"
|
||||||
prop-types "^15.6.2"
|
prop-types "^15.6.2"
|
||||||
|
|
||||||
|
react-virtual@^2.4.0:
|
||||||
|
version "2.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-virtual/-/react-virtual-2.4.0.tgz#151d39a91b311f684229235604fe75f1d096cd12"
|
||||||
|
integrity sha512-D5hy0XM+SN2pPUCPXIEVs1aCcGgvkLVw8sTikeKQF4jW4ZS1vIDyGUKFMvW1ZS3Yysw5yFCSBWl8yc7IEsGPag==
|
||||||
|
dependencies:
|
||||||
|
"@reach/observe-rect" "^1.1.0"
|
||||||
|
ts-toolbelt "^6.4.2"
|
||||||
|
|
||||||
react-virtualized-auto-sizer@^1.0.2:
|
react-virtualized-auto-sizer@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.2.tgz#a61dd4f756458bbf63bd895a92379f9b70f803bd"
|
resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.2.tgz#a61dd4f756458bbf63bd895a92379f9b70f803bd"
|
||||||
@@ -12233,6 +12246,11 @@ ts-node@^8, ts-node@^8.8.1:
|
|||||||
source-map-support "^0.5.6"
|
source-map-support "^0.5.6"
|
||||||
yn "3.1.1"
|
yn "3.1.1"
|
||||||
|
|
||||||
|
ts-toolbelt@^6.4.2:
|
||||||
|
version "6.15.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/ts-toolbelt/-/ts-toolbelt-6.15.5.tgz#cb3b43ed725cb63644782c64fbcad7d8f28c0a83"
|
||||||
|
integrity sha512-FZIXf1ksVyLcfr7M317jbB67XFJhOO1YqdTcuGaq9q5jLUoTikukZ+98TPjKiP2jC5CgmYdWWYs0s2nLSU0/1A==
|
||||||
|
|
||||||
tsconfig-paths@^3.9.0:
|
tsconfig-paths@^3.9.0:
|
||||||
version "3.9.0"
|
version "3.9.0"
|
||||||
resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz#098547a6c4448807e8fcb8eae081064ee9a3c90b"
|
resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz#098547a6c4448807e8fcb8eae081064ee9a3c90b"
|
||||||
|
|||||||
@@ -439,6 +439,7 @@ console.log(rows.get().length) // 2
|
|||||||
```
|
```
|
||||||
|
|
||||||
### createDataSource
|
### createDataSource
|
||||||
|
### DataSource
|
||||||
|
|
||||||
Coming soon.
|
Coming soon.
|
||||||
|
|
||||||
@@ -500,6 +501,10 @@ See [Tracked](#tracked) for more info.
|
|||||||
Layout elements can be used to organize the screen layout.
|
Layout elements can be used to organize the screen layout.
|
||||||
See `View > Flipper Style Guide` inside the Flipper application for more details.
|
See `View > Flipper Style Guide` inside the Flipper application for more details.
|
||||||
|
|
||||||
|
### DataTable
|
||||||
|
|
||||||
|
Coming soon.
|
||||||
|
|
||||||
### NUX
|
### NUX
|
||||||
|
|
||||||
An element that can be used to provide a New User eXperience: Hints that give a one time introduction to new features to the current user.
|
An element that can be used to provide a New User eXperience: Hints that give a one time introduction to new features to the current user.
|
||||||
|
|||||||
Reference in New Issue
Block a user