Skip to content
This repository has been archived by the owner on Nov 22, 2024. It is now read-only.

Commit

Permalink
Refactored DataView to be the primary data driver for DataTable instead
Browse files Browse the repository at this point in the history
Summary:
In order to accomplish multi-panel mode, we need to use multiple data views on the same data source so that the filters can be applied differently, etc.

This diff serves to refactor DataTable and some of its associated classes to use DataView as the primary driver for data management. Additionally, the diff refactored the state to allow multi-paneling to be on the DataPanel layer instead of the DataTable layer for ease of usage

This is the last diff of the larger stack which introduces the multi-panel mode feature. A possible next step could be allowing infinite(up to a certain limit) panels to be populated.

Changelog: Introduced side by side view feature for `DataTable`. There is now a new boolean for `DataTable` props called `enableMultiPanels`. If this is passed in, then the table will have an option to open a different "side panel" using a completely different dataview which allows different filters, searches, etc.

Reviewed By: mweststrate

Differential Revision: D37685390

fbshipit-source-id: 51e35f59da1ceba07ba8d379066970b57ab1734e
  • Loading branch information
Feiyu Wong authored and facebook-github-bot committed Jul 22, 2022
1 parent 96a2349 commit 3fbf121
Show file tree
Hide file tree
Showing 10 changed files with 526 additions and 129 deletions.
132 changes: 96 additions & 36 deletions desktop/flipper-plugin/src/data-source/DataSource.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ const defaultLimit = 100 * 1000;
// rather than search and remove the affected individual items
const shiftRebuildTreshold = 0.05;

const DEFAULT_VIEW_ID = '0';

type AppendEvent<T> = {
type: 'append';
entry: Entry<T>;
Expand All @@ -28,7 +30,9 @@ type UpdateEvent<T> = {
type: 'update';
entry: Entry<T>;
oldValue: T;
oldVisible: boolean;
oldVisible: {
[viewId: string]: boolean;
};
index: number;
};
type RemoveEvent<T> = {
Expand All @@ -51,8 +55,12 @@ type DataEvent<T> =
type Entry<T> = {
value: T;
id: number; // insertion based
visible: boolean; // matches current filter?
approxIndex: number; // we could possible live at this index in the output. No guarantees.
visible: {
[viewId: string]: boolean;
}; // matches current filter?
approxIndex: {
[viewId: string]: number;
}; // we could possible live at this index in the output. No guarantees.
};

type Primitive = number | string | boolean | null | undefined;
Expand Down Expand Up @@ -138,9 +146,14 @@ export class DataSource<T extends any, KeyType = never> {
*/
public readonly view: DataSourceView<T, KeyType>;

public readonly additionalViews: {
[viewId: string]: DataSourceView<T, KeyType>;
};

constructor(keyAttribute: keyof T | undefined) {
this.keyAttribute = keyAttribute;
this.view = new DataSourceView<T, KeyType>(this);
this.view = new DataSourceView<T, KeyType>(this, DEFAULT_VIEW_ID);
this.additionalViews = {};
}

public get size() {
Expand Down Expand Up @@ -228,12 +241,17 @@ export class DataSource<T extends any, KeyType = never> {
this._recordsById.set(key, value);
this.storeIndexOfKey(key, this._records.length);
}
const visibleMap: {[viewId: string]: boolean} = {[DEFAULT_VIEW_ID]: false};
const approxIndexMap: {[viewId: string]: number} = {[DEFAULT_VIEW_ID]: -1};
Object.keys(this.additionalViews).forEach((viewId) => {
visibleMap[viewId] = false;
approxIndexMap[viewId] = -1;
});
const entry = {
value,
id: ++this.nextId,
// once we have multiple views, the following fields should be stored per view
visible: true,
approxIndex: -1,
visible: visibleMap,
approxIndex: approxIndexMap,
};
this._records.push(entry);
this.emitDataEvent({
Expand Down Expand Up @@ -268,7 +286,7 @@ export class DataSource<T extends any, KeyType = never> {
if (value === oldValue) {
return;
}
const oldVisible = entry.visible;
const oldVisible = {...entry.visible};
entry.value = value;
if (this.keyAttribute) {
const key = this.getKey(value);
Expand Down Expand Up @@ -374,7 +392,7 @@ export class DataSource<T extends any, KeyType = never> {
// let's fallback to the async processing of all data instead
// MWE: there is a risk here that rebuilding is too blocking, as this might happen
// in background when new data arrives, and not explicitly on a user interaction
this.view.rebuild();
this.rebuild();
} else {
this.emitDataEvent({
type: 'shift',
Expand All @@ -392,17 +410,51 @@ export class DataSource<T extends any, KeyType = never> {
this._recordsById = new Map();
this.shiftOffset = 0;
this.idToIndex = new Map();
this.rebuild();
}

/**
* The rebuild function that would support rebuilding multiple views all at once
*/
public rebuild() {
this.view.rebuild();
Object.entries(this.additionalViews).forEach(([, dataView]) => {
dataView.rebuild();
});
}

/**
* Returns a fork of this dataSource, that shares the source data with this dataSource,
* but has it's own FSRW pipeline, to allow multiple views on the same data
*/
public fork(): DataSourceView<T, KeyType> {
throw new Error(
'Not implemented. Please contact oncall if this feature is needed',
);
private fork(viewId: string): DataSourceView<T, KeyType> {
this._records.forEach((entry) => {
entry.visible[viewId] = entry.visible[DEFAULT_VIEW_ID];
entry.approxIndex[viewId] = entry.approxIndex[DEFAULT_VIEW_ID];
});
const newView = new DataSourceView<T, KeyType>(this, viewId);
// Refresh the new view so that it has all the existing records.
newView.rebuild();
return newView;
}

public getAdditionalView(viewId: string): DataSourceView<T, KeyType> {
if (viewId in this.additionalViews) {
return this.additionalViews[viewId];
}
this.additionalViews[viewId] = this.fork(viewId);
return this.additionalViews[viewId];
}

public deleteView(viewId: string): void {
if (viewId in this.additionalViews) {
delete this.additionalViews[viewId];
// TODO: Ideally remove the viewId in the visible and approxIndex of DataView outputs
this._records.forEach((entry) => {
delete entry.visible[viewId];
delete entry.approxIndex[viewId];
});
}
}

private assertKeySet() {
Expand Down Expand Up @@ -433,6 +485,9 @@ export class DataSource<T extends any, KeyType = never> {
// using a queue,
// or only if there is an active view (although that could leak memory)
this.view.processEvent(event);
Object.entries(this.additionalViews).forEach(([, dataView]) => {
dataView.processEvent(event);
});
}

/**
Expand All @@ -457,7 +512,7 @@ function unwrap<T>(entry: Entry<T>): T {
return entry?.value;
}

class DataSourceView<T, KeyType> {
export class DataSourceView<T, KeyType> {
public readonly datasource: DataSource<T, KeyType>;
private sortBy: undefined | ((a: T) => Primitive) = undefined;
private reverse: boolean = false;
Expand All @@ -471,6 +526,7 @@ class DataSourceView<T, KeyType> {
* @readonly
*/
public windowEnd = 0;
private viewId;

private outputChangeListeners = new Set<(change: OutputChange) => void>();

Expand All @@ -479,8 +535,9 @@ class DataSourceView<T, KeyType> {
*/
private _output: Entry<T>[] = [];

constructor(datasource: DataSource<T, KeyType>) {
constructor(datasource: DataSource<T, KeyType>, viewId: string) {
this.datasource = datasource;
this.viewId = viewId;
}

public get size() {
Expand Down Expand Up @@ -591,8 +648,11 @@ class DataSourceView<T, KeyType> {
// so any changes in the entry being moved around etc will be reflected in the original `entry` object,
// and we just want to verify that this entry is indeed still the same element, visible, and still present in
// the output data set.
if (entry.visible && entry.id === this._output[entry.approxIndex]?.id) {
return this.normalizeIndex(entry.approxIndex);
if (
entry.visible[this.viewId] &&
entry.id === this._output[entry.approxIndex[this.viewId]]?.id
) {
return this.normalizeIndex(entry.approxIndex[this.viewId]);
}
return -1;
}
Expand Down Expand Up @@ -674,30 +734,30 @@ class DataSourceView<T, KeyType> {
switch (event.type) {
case 'append': {
const {entry} = event;
entry.visible = filter ? filter(entry.value) : true;
if (!entry.visible) {
entry.visible[this.viewId] = filter ? filter(entry.value) : true;
if (!entry.visible[this.viewId]) {
// not in filter? skip this entry
return;
}
if (!sortBy) {
// no sorting? insert at the end, or beginning
entry.approxIndex = output.length;
entry.approxIndex[this.viewId] = output.length;
output.push(entry);
this.notifyItemShift(entry.approxIndex, 1);
this.notifyItemShift(entry.approxIndex[this.viewId], 1);
} else {
this.insertSorted(entry);
}
break;
}
case 'update': {
const {entry} = event;
entry.visible = filter ? filter(entry.value) : true;
entry.visible[this.viewId] = filter ? filter(entry.value) : true;
// short circuit; no view active so update straight away
if (!filter && !sortBy) {
output[event.index].approxIndex = event.index;
output[event.index].approxIndex[this.viewId] = event.index;
this.notifyItemUpdated(event.index);
} else if (!event.oldVisible) {
if (!entry.visible) {
} else if (!event.oldVisible[this.viewId]) {
if (!entry.visible[this.viewId]) {
// Done!
} else {
// insertion, not visible before
Expand All @@ -706,7 +766,7 @@ class DataSourceView<T, KeyType> {
} else {
// Entry was visible previously
const existingIndex = this.getSortedIndex(entry, event.oldValue);
if (!entry.visible) {
if (!entry.visible[this.viewId]) {
// Remove from output
output.splice(existingIndex, 1);
this.notifyItemShift(existingIndex, -1);
Expand Down Expand Up @@ -744,7 +804,7 @@ class DataSourceView<T, KeyType> {
} else {
// if there is a filter, count the visibles and shift those
for (let i = 0; i < event.entries.length; i++)
if (event.entries[i].visible) amount++;
if (event.entries[i].visible[this.viewId]) amount++;
}
output.splice(0, amount);
this.notifyItemShift(0, -amount);
Expand All @@ -766,7 +826,7 @@ class DataSourceView<T, KeyType> {
const {_output: output, sortBy, filter} = this;

// filter active, and not visible? short circuilt
if (!entry.visible) {
if (!entry.visible[this.viewId]) {
return;
}
// no sorting, no filter?
Expand Down Expand Up @@ -798,8 +858,8 @@ class DataSourceView<T, KeyType> {
const records: Entry<T>[] = this.datasource._records;
let output = filter
? records.filter((entry) => {
entry.visible = filter(entry.value);
return entry.visible;
entry.visible[this.viewId] = filter(entry.value);
return entry.visible[this.viewId];
})
: records.slice();
if (sortBy) {
Expand All @@ -818,7 +878,7 @@ class DataSourceView<T, KeyType> {

// write approx indexes for faster lookup of entries in visible output
for (let i = 0; i < output.length; i++) {
output[i].approxIndex = i;
output[i].approxIndex[this.viewId] = i;
}
this._output = output;
this.notifyReset(output.length);
Expand All @@ -829,17 +889,17 @@ class DataSourceView<T, KeyType> {

private getSortedIndex(entry: Entry<T>, oldValue: T) {
const {_output: output} = this;
if (output[entry.approxIndex] === entry) {
if (output[entry.approxIndex[this.viewId]] === entry) {
// yay!
return entry.approxIndex;
return entry.approxIndex[this.viewId];
}
let index = sortedIndexBy(
output,
{
value: oldValue,
id: -1,
visible: true,
approxIndex: -1,
visible: entry.visible,
approxIndex: entry.approxIndex,
},
this.sortHelper,
);
Expand All @@ -862,7 +922,7 @@ class DataSourceView<T, KeyType> {
entry,
this.sortHelper,
);
entry.approxIndex = insertionIndex;
entry.approxIndex[this.viewId] = insertionIndex;
this._output.splice(insertionIndex, 0, entry);
this.notifyItemShift(insertionIndex, 1);
}
Expand Down
22 changes: 12 additions & 10 deletions desktop/flipper-plugin/src/data-source/DataSourceRendererStatic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@
* @format
*/

import {DataSource} from './DataSource';
import {DataSourceView} from './DataSource';
import React, {memo, useCallback, useEffect, useState} from 'react';

import {RedrawContext} from './DataSourceRendererVirtual';

type DataSourceProps<T extends object, C> = {
/**
* The data source to render
* The data view to render
*/
dataSource: DataSource<T, T[keyof T]>;
dataView: DataSourceView<T, T[keyof T]>;
/**
* additional context that will be passed verbatim to the itemRenderer, so that it can be easily memoized
*/
Expand All @@ -30,11 +30,12 @@ type DataSourceProps<T extends object, C> = {
itemRenderer(item: T, index: number, context: C): React.ReactElement;
useFixedRowHeight: boolean;
defaultRowHeight: number;
maxRecords: number;
onKeyDown?: React.KeyboardEventHandler<HTMLDivElement>;
onUpdateAutoScroll?(autoScroll: boolean): void;
emptyRenderer?:
| null
| ((dataSource: DataSource<T, T[keyof T]>) => React.ReactElement);
| ((dataView: DataSourceView<T, T[keyof T]>) => React.ReactElement);
};

/**
Expand All @@ -44,7 +45,8 @@ type DataSourceProps<T extends object, C> = {
export const DataSourceRendererStatic: <T extends object, C>(
props: DataSourceProps<T, C>,
) => React.ReactElement = memo(function DataSourceRendererStatic({
dataSource,
dataView,
maxRecords,
useFixedRowHeight,
context,
itemRenderer,
Expand All @@ -65,8 +67,8 @@ export const DataSourceRendererStatic: <T extends object, C>(
function subscribeToDataSource() {
let unmounted = false;

dataSource.view.setWindow(0, dataSource.limit);
const unsubscribe = dataSource.view.addListener((_event) => {
dataView.setWindow(0, maxRecords);
const unsubscribe = dataView.addListener((_event) => {
if (unmounted) {
return;
}
Expand All @@ -78,7 +80,7 @@ export const DataSourceRendererStatic: <T extends object, C>(
unsubscribe();
};
},
[dataSource, setForceUpdate, useFixedRowHeight],
[dataView, maxRecords, setForceUpdate, useFixedRowHeight],
);

useEffect(() => {
Expand All @@ -89,7 +91,7 @@ export const DataSourceRendererStatic: <T extends object, C>(
/**
* Rendering
*/
const records = dataSource.view.output();
const records = dataView.output();
if (records.length > 500) {
console.warn(
"StaticDataSourceRenderer should only be used on small datasets. For large datasets the 'scrollable' flag should enabled on DataTable",
Expand All @@ -100,7 +102,7 @@ export const DataSourceRendererStatic: <T extends object, C>(
<RedrawContext.Provider value={redraw}>
<div onKeyDown={onKeyDown} tabIndex={0}>
{records.length === 0
? emptyRenderer?.(dataSource)
? emptyRenderer?.(dataView)
: records.map((item, index) => (
<div key={index}>{itemRenderer(item, index, context)}</div>
))}
Expand Down
Loading

0 comments on commit 3fbf121

Please sign in to comment.