Type improvements
Summary: some type simplifications, that makes it easier to reuse data sources and helps type inference Reviewed By: passy Differential Revision: D28413380 fbshipit-source-id: 261a8b981bf18a00edc3075926bd668322e1c37d
This commit is contained in:
committed by
Facebook GitHub Bot
parent
9a2677fc24
commit
bc647972e1
@@ -20,12 +20,6 @@ const defaultLimit = 100 * 1000;
|
|||||||
// rather than search and remove the affected individual items
|
// rather than search and remove the affected individual items
|
||||||
const shiftRebuildTreshold = 0.05;
|
const shiftRebuildTreshold = 0.05;
|
||||||
|
|
||||||
export type ExtractKeyType<T, KEY extends keyof T> = T[KEY] extends string
|
|
||||||
? string
|
|
||||||
: T[KEY] extends number
|
|
||||||
? number
|
|
||||||
: never;
|
|
||||||
|
|
||||||
type AppendEvent<T> = {
|
type AppendEvent<T> = {
|
||||||
type: 'append';
|
type: 'append';
|
||||||
entry: Entry<T>;
|
entry: Entry<T>;
|
||||||
@@ -83,19 +77,45 @@ type OutputChange =
|
|||||||
newCount: number;
|
newCount: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class DataSource<
|
export type DataSourceOptions<T, K extends keyof T> = {
|
||||||
T = any,
|
/**
|
||||||
KEY extends keyof T = any,
|
* If a key is set, the given field of the records is assumed to be unique,
|
||||||
KEY_TYPE extends string | number | never = ExtractKeyType<T, KEY>,
|
* and it's value can be used to perform lookups and upserts.
|
||||||
> {
|
*/
|
||||||
|
key?: K;
|
||||||
|
/**
|
||||||
|
* The maximum amount of records that this DataSource will store.
|
||||||
|
* If the limit is exceeded, the oldest records will automatically be dropped to make place for the new ones
|
||||||
|
*/
|
||||||
|
limit?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createDataSource<T, KEY extends keyof T = any>(
|
||||||
|
initialSet: readonly T[],
|
||||||
|
options?: DataSourceOptions<T, KEY>,
|
||||||
|
): DataSource<T>;
|
||||||
|
export function createDataSource<T>(initialSet?: readonly T[]): DataSource<T>;
|
||||||
|
export function createDataSource<T, KEY extends keyof T>(
|
||||||
|
initialSet: readonly T[] = [],
|
||||||
|
options?: DataSourceOptions<T, KEY>,
|
||||||
|
): DataSource<T> {
|
||||||
|
const ds = new DataSource<T>(options?.key);
|
||||||
|
if (options?.limit !== undefined) {
|
||||||
|
ds.limit = options.limit;
|
||||||
|
}
|
||||||
|
initialSet.forEach((value) => ds.append(value));
|
||||||
|
return ds as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DataSource<T> {
|
||||||
private nextId = 0;
|
private nextId = 0;
|
||||||
private _records: Entry<T>[] = [];
|
private _records: Entry<T>[] = [];
|
||||||
private _recordsById: Map<KEY_TYPE, T> = new Map();
|
private _recordsById: Map<string, T> = new Map();
|
||||||
/**
|
/**
|
||||||
* @readonly
|
* @readonly
|
||||||
*/
|
*/
|
||||||
public keyAttribute: undefined | keyof T;
|
public keyAttribute: keyof T | undefined;
|
||||||
private idToIndex: Map<KEY_TYPE, number> = new Map();
|
private idToIndex: Map<string, number> = new Map();
|
||||||
|
|
||||||
// if we shift the window, we increase shiftOffset to correct idToIndex results, rather than remapping all values
|
// if we shift the window, we increase shiftOffset to correct idToIndex results, rather than remapping all values
|
||||||
private shiftOffset = 0;
|
private shiftOffset = 0;
|
||||||
@@ -113,7 +133,7 @@ export class DataSource<
|
|||||||
*/
|
*/
|
||||||
public readonly view: DataSourceView<T>;
|
public readonly view: DataSourceView<T>;
|
||||||
|
|
||||||
constructor(keyAttribute: KEY | undefined) {
|
constructor(keyAttribute: keyof T | undefined) {
|
||||||
this.keyAttribute = keyAttribute;
|
this.keyAttribute = keyAttribute;
|
||||||
this.view = new DataSourceView<T>(this);
|
this.view = new DataSourceView<T>(this);
|
||||||
}
|
}
|
||||||
@@ -134,22 +154,22 @@ export class DataSource<
|
|||||||
return unwrap(this._records[index]);
|
return unwrap(this._records[index]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public has(key: KEY_TYPE) {
|
public has(key: string) {
|
||||||
this.assertKeySet();
|
this.assertKeySet();
|
||||||
return this._recordsById.has(key);
|
return this._recordsById.has(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getById(key: KEY_TYPE) {
|
public getById(key: string): T | undefined {
|
||||||
this.assertKeySet();
|
this.assertKeySet();
|
||||||
return this._recordsById.get(key);
|
return this._recordsById.get(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
public keys(): IterableIterator<KEY_TYPE> {
|
public keys(): IterableIterator<string> {
|
||||||
this.assertKeySet();
|
this.assertKeySet();
|
||||||
return this._recordsById.keys();
|
return this._recordsById.keys();
|
||||||
}
|
}
|
||||||
|
|
||||||
public entries(): IterableIterator<[KEY_TYPE, T]> {
|
public entries(): IterableIterator<[string, T]> {
|
||||||
this.assertKeySet();
|
this.assertKeySet();
|
||||||
return this._recordsById.entries();
|
return this._recordsById.entries();
|
||||||
}
|
}
|
||||||
@@ -178,7 +198,7 @@ export class DataSource<
|
|||||||
* Returns the index of a specific key in the *records* set.
|
* Returns the index of a specific key in the *records* set.
|
||||||
* Returns -1 if the record wansn't found
|
* Returns -1 if the record wansn't found
|
||||||
*/
|
*/
|
||||||
public getIndexOfKey(key: KEY_TYPE): number {
|
public getIndexOfKey(key: string): number {
|
||||||
this.assertKeySet();
|
this.assertKeySet();
|
||||||
const stored = this.idToIndex.get(key);
|
const stored = this.idToIndex.get(key);
|
||||||
return stored === undefined ? -1 : stored + this.shiftOffset;
|
return stored === undefined ? -1 : stored + this.shiftOffset;
|
||||||
@@ -302,7 +322,7 @@ export class DataSource<
|
|||||||
*
|
*
|
||||||
* Warning: this operation can be O(n) if a key is set
|
* Warning: this operation can be O(n) if a key is set
|
||||||
*/
|
*/
|
||||||
public deleteByKey(keyValue: KEY_TYPE): boolean {
|
public deleteByKey(keyValue: string): boolean {
|
||||||
this.assertKeySet();
|
this.assertKeySet();
|
||||||
const index = this.getIndexOfKey(keyValue);
|
const index = this.getIndexOfKey(keyValue);
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
@@ -382,7 +402,7 @@ export class DataSource<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getKey(value: T): KEY_TYPE;
|
private getKey(value: T): string;
|
||||||
private getKey(value: any): any {
|
private getKey(value: any): any {
|
||||||
this.assertKeySet();
|
this.assertKeySet();
|
||||||
const key = value[this.keyAttribute!];
|
const key = value[this.keyAttribute!];
|
||||||
@@ -392,7 +412,7 @@ export class DataSource<
|
|||||||
throw new Error(`Invalid key value: '${key}'`);
|
throw new Error(`Invalid key value: '${key}'`);
|
||||||
}
|
}
|
||||||
|
|
||||||
private storeIndexOfKey(key: KEY_TYPE, index: number) {
|
private storeIndexOfKey(key: string, index: number) {
|
||||||
// de-normalize the index, so that on later look ups its corrected again
|
// de-normalize the index, so that on later look ups its corrected again
|
||||||
this.idToIndex.set(key, index - this.shiftOffset);
|
this.idToIndex.set(key, index - this.shiftOffset);
|
||||||
}
|
}
|
||||||
@@ -448,7 +468,7 @@ class DataSourceView<T> {
|
|||||||
*/
|
*/
|
||||||
private _output: Entry<T>[] = [];
|
private _output: Entry<T>[] = [];
|
||||||
|
|
||||||
constructor(datasource: DataSource<T, any, any>) {
|
constructor(datasource: DataSource<T>) {
|
||||||
this.datasource = datasource;
|
this.datasource = datasource;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ type DataSourceProps<T extends object, C> = {
|
|||||||
/**
|
/**
|
||||||
* The data source to render
|
* The data source to render
|
||||||
*/
|
*/
|
||||||
dataSource: DataSource<T, any, any>;
|
dataSource: DataSource<T>;
|
||||||
/**
|
/**
|
||||||
* additional context that will be passed verbatim to the itemRenderer, so that it can be easily memoized
|
* additional context that will be passed verbatim to the itemRenderer, so that it can be easily memoized
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ type DataSourceProps<T extends object, C> = {
|
|||||||
/**
|
/**
|
||||||
* The data source to render
|
* The data source to render
|
||||||
*/
|
*/
|
||||||
dataSource: DataSource<T, any, any>;
|
dataSource: DataSource<T>;
|
||||||
/**
|
/**
|
||||||
* Automatically scroll if the user is near the end?
|
* Automatically scroll if the user is near the end?
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -7,10 +7,7 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// ok for now, should be factored if this becomes a stand-alone lib
|
import {DataSource, createDataSource} from '../DataSource';
|
||||||
// eslint-disable-next-line
|
|
||||||
import {createDataSource} from '../../state/createDataSource';
|
|
||||||
import {DataSource} from '../DataSource';
|
|
||||||
|
|
||||||
type Todo = {
|
type Todo = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -487,7 +484,7 @@ test('clear', () => {
|
|||||||
|
|
||||||
function testEvents<T>(
|
function testEvents<T>(
|
||||||
initial: T[],
|
initial: T[],
|
||||||
op: (ds: DataSource<T, any, any>) => void,
|
op: (ds: DataSource<T>) => void,
|
||||||
key?: keyof T,
|
key?: keyof T,
|
||||||
): any[] {
|
): any[] {
|
||||||
const ds = createDataSource<T>(initial, {key});
|
const ds = createDataSource<T>(initial, {key});
|
||||||
|
|||||||
@@ -7,10 +7,7 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// ok for now, should be factored if this becomes a stand-alone lib
|
import {DataSource, createDataSource} from '../DataSource';
|
||||||
// eslint-disable-next-line
|
|
||||||
import {createDataSource} from '../../state/createDataSource';
|
|
||||||
import {DataSource} from '../DataSource';
|
|
||||||
|
|
||||||
type Todo = {
|
type Todo = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export {DataSource, ExtractKeyType} from './DataSource';
|
export {DataSource, createDataSource, DataSourceOptions} from './DataSource';
|
||||||
export {
|
export {
|
||||||
DataSourceRendererVirtual,
|
DataSourceRendererVirtual,
|
||||||
DataSourceVirtualizer,
|
DataSourceVirtualizer,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "flipper-data-source",
|
"name": "flipper-data-source",
|
||||||
"version": "0.0.1",
|
"version": "0.0.2",
|
||||||
"description": "Library to power streamig data visualisations",
|
"description": "Library to power streamig data visualisations",
|
||||||
"repository": "https://github.com/facebook/flipper",
|
"repository": "https://github.com/facebook/flipper",
|
||||||
"homepage": "https://github.com/facebook/flipper/blob/master/desktop/flipper-plugin/src/data-source/README.md",
|
"homepage": "https://github.com/facebook/flipper/blob/master/desktop/flipper-plugin/src/data-source/README.md",
|
||||||
|
|||||||
@@ -7,20 +7,17 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {DataSource, ExtractKeyType} from '../data-source/index';
|
import {
|
||||||
|
DataSource,
|
||||||
|
createDataSource as baseCreateDataSource,
|
||||||
|
DataSourceOptions as BaseDataSourceOptions,
|
||||||
|
} from '../data-source/index';
|
||||||
import {registerStorageAtom} from '../plugin/PluginBase';
|
import {registerStorageAtom} from '../plugin/PluginBase';
|
||||||
|
|
||||||
type CreateDataSourceOptions<T, K extends keyof T> = {
|
type CreateDataSourceOptions<T, K extends keyof T> = BaseDataSourceOptions<
|
||||||
/**
|
T,
|
||||||
* If a key is set, the given field of the records is assumed to be unique,
|
K
|
||||||
* and it's value can be used to perform lookups and upserts.
|
> & {
|
||||||
*/
|
|
||||||
key?: K;
|
|
||||||
/**
|
|
||||||
* The maximum amount of records that this DataSource will store.
|
|
||||||
* If the limit is exceeded, the oldest records will automatically be dropped to make place for the new ones
|
|
||||||
*/
|
|
||||||
limit?: number;
|
|
||||||
/**
|
/**
|
||||||
* Should this state persist when exporting a plugin?
|
* Should this state persist when exporting a plugin?
|
||||||
* If set, the dataSource will be saved / loaded under the key provided
|
* If set, the dataSource will be saved / loaded under the key provided
|
||||||
@@ -29,21 +26,15 @@ type CreateDataSourceOptions<T, K extends keyof T> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function createDataSource<T, KEY extends keyof T = any>(
|
export function createDataSource<T, KEY extends keyof T = any>(
|
||||||
initialSet: T[],
|
initialSet: readonly T[],
|
||||||
options: CreateDataSourceOptions<T, KEY>,
|
options: CreateDataSourceOptions<T, KEY>,
|
||||||
): DataSource<T, KEY, ExtractKeyType<T, KEY>>;
|
): DataSource<T>;
|
||||||
export function createDataSource<T>(
|
export function createDataSource<T>(initialSet?: readonly T[]): DataSource<T>;
|
||||||
initialSet?: T[],
|
|
||||||
): DataSource<T, never, never>;
|
|
||||||
export function createDataSource<T, KEY extends keyof T>(
|
export function createDataSource<T, KEY extends keyof T>(
|
||||||
initialSet: T[] = [],
|
initialSet: readonly T[] = [],
|
||||||
options?: CreateDataSourceOptions<T, KEY>,
|
options?: CreateDataSourceOptions<T, KEY>,
|
||||||
): DataSource<T, any, any> {
|
): DataSource<T> {
|
||||||
const ds = new DataSource<T, KEY>(options?.key);
|
const ds = baseCreateDataSource(initialSet, options);
|
||||||
if (options?.limit !== undefined) {
|
|
||||||
ds.limit = options.limit;
|
|
||||||
}
|
|
||||||
registerStorageAtom(options?.persist, ds);
|
registerStorageAtom(options?.persist, ds);
|
||||||
initialSet.forEach((value) => ds.append(value));
|
|
||||||
return ds;
|
return ds;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import styled from '@emotion/styled';
|
|||||||
import {DataTableManager} from './data-table/DataTableManager';
|
import {DataTableManager} from './data-table/DataTableManager';
|
||||||
import {Atom, createState} from '../state/atom';
|
import {Atom, createState} from '../state/atom';
|
||||||
import {useAssertStableRef} from '../utils/useAssertStableRef';
|
import {useAssertStableRef} from '../utils/useAssertStableRef';
|
||||||
|
import {DataSource} from '../data-source';
|
||||||
|
|
||||||
const {Text} = Typography;
|
const {Text} = Typography;
|
||||||
|
|
||||||
@@ -62,7 +63,7 @@ interface DataListBaseProps<T extends Item> {
|
|||||||
/**
|
/**
|
||||||
* Items to display. Per item at least a title and unique id should be provided
|
* Items to display. Per item at least a title and unique id should be provided
|
||||||
*/
|
*/
|
||||||
items: readonly Item[];
|
items: DataSource<Item> | readonly 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
|
||||||
*/
|
*/
|
||||||
@@ -152,7 +153,8 @@ export const DataList: React.FC<DataListProps<any>> = function DataList<
|
|||||||
<DataTable<any>
|
<DataTable<any>
|
||||||
{...tableProps}
|
{...tableProps}
|
||||||
tableManagerRef={tableManagerRef}
|
tableManagerRef={tableManagerRef}
|
||||||
records={items}
|
records={Array.isArray(items) ? items : undefined}
|
||||||
|
dataSource={Array.isArray(items) ? undefined : (items as any)}
|
||||||
recordsKey="id"
|
recordsKey="id"
|
||||||
columns={dataListColumns}
|
columns={dataListColumns}
|
||||||
onSelect={handleSelect}
|
onSelect={handleSelect}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ import {Formatter} from '../DataFormatter';
|
|||||||
import {usePluginInstance} from '../../plugin/PluginContext';
|
import {usePluginInstance} from '../../plugin/PluginContext';
|
||||||
import {debounce} from 'lodash';
|
import {debounce} from 'lodash';
|
||||||
import {useInUnitTest} from '../../utils/useInUnitTest';
|
import {useInUnitTest} from '../../utils/useInUnitTest';
|
||||||
|
import {createDataSource} from 'flipper-plugin/src/state/createDataSource';
|
||||||
|
|
||||||
interface DataTableBaseProps<T = any> {
|
interface DataTableBaseProps<T = any> {
|
||||||
columns: DataTableColumn<T>[];
|
columns: DataTableColumn<T>[];
|
||||||
@@ -66,9 +67,7 @@ interface DataTableBaseProps<T = any> {
|
|||||||
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;
|
||||||
onRenderEmpty?:
|
onRenderEmpty?: null | ((dataSource?: DataSource<T>) => React.ReactElement);
|
||||||
| null
|
|
||||||
| ((dataSource?: DataSource<T, any, any>) => React.ReactElement);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ItemRenderer<T> = (
|
export type ItemRenderer<T> = (
|
||||||
@@ -79,7 +78,7 @@ export type ItemRenderer<T> = (
|
|||||||
|
|
||||||
type DataTableInput<T = any> =
|
type DataTableInput<T = any> =
|
||||||
| {
|
| {
|
||||||
dataSource: DataSource<T, any, any>;
|
dataSource: DataSource<T>;
|
||||||
records?: undefined;
|
records?: undefined;
|
||||||
recordsKey?: undefined;
|
recordsKey?: undefined;
|
||||||
}
|
}
|
||||||
@@ -525,11 +524,9 @@ function normalizeDataSourceInput<T>(props: DataTableInput<T>): DataSource<T> {
|
|||||||
return props.dataSource;
|
return props.dataSource;
|
||||||
}
|
}
|
||||||
if (props.records) {
|
if (props.records) {
|
||||||
const [dataSource] = useState(() => {
|
const [dataSource] = useState(() =>
|
||||||
const ds = new DataSource<T>(props.recordsKey);
|
createDataSource(props.records, {key: props.recordsKey}),
|
||||||
syncRecordsToDataSource(ds, props.records);
|
);
|
||||||
return ds;
|
|
||||||
});
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
syncRecordsToDataSource(dataSource, props.records);
|
syncRecordsToDataSource(dataSource, props.records);
|
||||||
}, [dataSource, props.records]);
|
}, [dataSource, props.records]);
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ type DataManagerActions<T> =
|
|||||||
| Action<
|
| Action<
|
||||||
'selectItemById',
|
'selectItemById',
|
||||||
{
|
{
|
||||||
id: string | number;
|
id: string;
|
||||||
addToSelection?: boolean;
|
addToSelection?: boolean;
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -213,7 +213,7 @@ export const dataTableManagerReducer = produce<
|
|||||||
}
|
}
|
||||||
case 'setColumnFilterFromSelection': {
|
case 'setColumnFilterFromSelection': {
|
||||||
const items = getSelectedItems(
|
const items = getSelectedItems(
|
||||||
config.dataSource as DataSource,
|
config.dataSource as DataSource<any>,
|
||||||
draft.selection,
|
draft.selection,
|
||||||
);
|
);
|
||||||
items.forEach((item, index) => {
|
items.forEach((item, index) => {
|
||||||
@@ -258,7 +258,7 @@ export type DataTableManager<T> = {
|
|||||||
end: number,
|
end: number,
|
||||||
allowUnselect?: boolean,
|
allowUnselect?: boolean,
|
||||||
): void;
|
): void;
|
||||||
selectItemById(id: string | number, addToSelection?: boolean): void;
|
selectItemById(id: string, addToSelection?: boolean): void;
|
||||||
clearSelection(): void;
|
clearSelection(): void;
|
||||||
getSelectedItem(): T | undefined;
|
getSelectedItem(): T | undefined;
|
||||||
getSelectedItems(): readonly T[];
|
getSelectedItems(): readonly T[];
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export interface Request {
|
|||||||
insights?: Insights;
|
insights?: Insights;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Requests = DataSource<Request, 'id', string>;
|
export type Requests = DataSource<Request>;
|
||||||
|
|
||||||
export type SerializedRequest = Omit<
|
export type SerializedRequest = Omit<
|
||||||
Request,
|
Request,
|
||||||
|
|||||||
Reference in New Issue
Block a user