Inline createTablePlugin for native plugins
Summary: Before expanding what a table plugin can do, I'm extracting the createTablePlugin functionality into the native plugin component, so it can evolve independently without lots of forks in the code. Reviewed By: passy Differential Revision: D14800313 fbshipit-source-id: dbb34fe974c507663151daeb6631d1911b46f6df
This commit is contained in:
committed by
Facebook Github Bot
parent
ad48b0c9a1
commit
287cad2f4b
@@ -5,14 +5,65 @@
|
||||
* @format
|
||||
*/
|
||||
|
||||
import {ManagedDataInspector, Panel} from 'flipper';
|
||||
import {createTablePlugin} from '../createTablePlugin';
|
||||
import {colors, styled, Text, Toolbar, Spacer, Button} from 'flipper';
|
||||
import {
|
||||
ManagedDataInspector,
|
||||
Panel,
|
||||
colors,
|
||||
styled,
|
||||
Text,
|
||||
Toolbar,
|
||||
Spacer,
|
||||
Button,
|
||||
} from 'flipper';
|
||||
import ErrorBlock from '../ui/components/ErrorBlock';
|
||||
import FlexColumn from '../ui/components/FlexColumn';
|
||||
import DetailSidebar from '../chrome/DetailSidebar';
|
||||
import {FlipperPlugin} from '../plugin';
|
||||
import SearchableTable from '../ui/components/searchable/SearchableTable';
|
||||
import textContent from '../utils/textContent.js';
|
||||
import createPaste from '../fb-stubs/createPaste.js';
|
||||
|
||||
import type {Node} from 'react';
|
||||
import type {
|
||||
TableHighlightedRows,
|
||||
TableRows,
|
||||
TableColumnSizes,
|
||||
TableColumns,
|
||||
TableColumnOrderVal,
|
||||
TableBodyRow,
|
||||
} from 'flipper';
|
||||
|
||||
type ID = string;
|
||||
|
||||
type TableMetadata = {
|
||||
columns: TableColumns,
|
||||
columnSizes?: TableColumnSizes,
|
||||
columnOrder?: Array<TableColumnOrderVal>,
|
||||
filterableColumns?: Set<string>,
|
||||
};
|
||||
|
||||
type PersistedState = {|
|
||||
rows: TableRows,
|
||||
datas: {[key: ID]: NumberedRowData},
|
||||
tableMetadata: ?TableMetadata,
|
||||
|};
|
||||
|
||||
type State = {|
|
||||
selectedIds: Array<ID>,
|
||||
error: ?string,
|
||||
|};
|
||||
|
||||
type RowData = {
|
||||
id: string,
|
||||
columns: {},
|
||||
sidebar: Array<SidebarSection>,
|
||||
columns: {[key: string]: any},
|
||||
sidebar?: Array<SidebarSection>,
|
||||
};
|
||||
|
||||
type NumberedRowData = {
|
||||
id: string,
|
||||
columns: {[key: string]: any},
|
||||
sidebar?: Array<SidebarSection>,
|
||||
rowNumber: number,
|
||||
};
|
||||
|
||||
type SidebarSection = JsonSection | ToolbarSection;
|
||||
@@ -59,7 +110,7 @@ function renderValue({type, value}: {type: string, value: any}) {
|
||||
}
|
||||
}
|
||||
|
||||
function buildRow(rowData: RowData, previousRowData: ?RowData) {
|
||||
function buildRow(rowData: RowData, previousRowData: ?RowData): TableBodyRow {
|
||||
if (!rowData.columns) {
|
||||
throw new Error('defaultBuildRow used with incorrect data format.');
|
||||
}
|
||||
@@ -112,7 +163,7 @@ function renderToolbar(section: ToolbarSection) {
|
||||
);
|
||||
}
|
||||
|
||||
function renderSidebar(rowData: RowData) {
|
||||
function renderSidebarForRow(rowData: RowData): Node {
|
||||
if (!rowData.sidebar) {
|
||||
throw new Error('renderSidebar used with missing rowData.sidebar');
|
||||
}
|
||||
@@ -122,7 +173,7 @@ function renderSidebar(rowData: RowData) {
|
||||
return rowData.sidebar.map(renderSidebarSection);
|
||||
}
|
||||
|
||||
function renderSidebarSection(section: SidebarSection, index: number) {
|
||||
function renderSidebarSection(section: SidebarSection, index: number): Node {
|
||||
switch (section.type) {
|
||||
case 'json':
|
||||
return (
|
||||
@@ -142,11 +193,220 @@ function renderSidebarSection(section: SidebarSection, index: number) {
|
||||
}
|
||||
|
||||
export default function createTableNativePlugin(id: string, title: string) {
|
||||
return createTablePlugin({
|
||||
method: 'updateRows',
|
||||
title,
|
||||
id,
|
||||
renderSidebar: renderSidebar,
|
||||
buildRow: buildRow,
|
||||
});
|
||||
return class extends FlipperPlugin<State, *, PersistedState> {
|
||||
static keyboardActions = ['clear', 'createPaste'];
|
||||
static id = id || '';
|
||||
static title = title || '';
|
||||
|
||||
static defaultPersistedState: PersistedState = {
|
||||
rows: [],
|
||||
datas: {},
|
||||
tableMetadata: null,
|
||||
};
|
||||
|
||||
static persistedStateReducer = (
|
||||
persistedState: PersistedState,
|
||||
method: string,
|
||||
payload: RowData | Array<RowData>,
|
||||
): $Shape<PersistedState> => {
|
||||
if (method === 'updateRows') {
|
||||
const newRows = [];
|
||||
const newData = {};
|
||||
if (!Array.isArray(payload)) {
|
||||
throw new Error('updateRows called with non array type');
|
||||
}
|
||||
|
||||
for (const rowData of payload.reverse()) {
|
||||
if (rowData.id == null) {
|
||||
throw new Error(
|
||||
`updateRows: row is missing id: ${JSON.stringify(rowData)}`,
|
||||
);
|
||||
}
|
||||
const previousRowData: ?NumberedRowData =
|
||||
persistedState.datas[rowData.id];
|
||||
const newRow: TableBodyRow = buildRow(rowData, previousRowData);
|
||||
if (persistedState.datas[rowData.id] == null) {
|
||||
newData[rowData.id] = {
|
||||
...rowData,
|
||||
rowNumber: persistedState.rows.length + newRows.length,
|
||||
};
|
||||
newRows.push(newRow);
|
||||
} else {
|
||||
persistedState.rows = persistedState.rows
|
||||
.slice(0, persistedState.datas[rowData.id].rowNumber)
|
||||
.concat(
|
||||
[newRow],
|
||||
persistedState.rows.slice(
|
||||
persistedState.datas[rowData.id].rowNumber + 1,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return {
|
||||
...persistedState,
|
||||
datas: {...persistedState.datas, ...newData},
|
||||
rows: [...persistedState.rows, ...newRows],
|
||||
};
|
||||
} else if (method === 'clearTable') {
|
||||
return {
|
||||
...persistedState,
|
||||
rows: [],
|
||||
datas: {},
|
||||
};
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
state = {
|
||||
selectedIds: [],
|
||||
error: null,
|
||||
};
|
||||
|
||||
init() {
|
||||
this.getTableMetadata();
|
||||
}
|
||||
|
||||
getTableMetadata = () => {
|
||||
if (!this.props.persistedState.tableMetadata) {
|
||||
this.client
|
||||
.call('getMetadata')
|
||||
.then(metadata => {
|
||||
this.props.setPersistedState({
|
||||
tableMetadata: {
|
||||
...metadata,
|
||||
filterableColumns: new Set(metadata.filterableColumns),
|
||||
},
|
||||
});
|
||||
})
|
||||
.catch(e => this.setState({error: e}));
|
||||
}
|
||||
};
|
||||
|
||||
onKeyboardAction = (action: string) => {
|
||||
if (action === 'clear') {
|
||||
this.clear();
|
||||
} else if (action === 'createPaste') {
|
||||
this.createPaste();
|
||||
}
|
||||
};
|
||||
|
||||
clear = () => {
|
||||
this.props.setPersistedState({
|
||||
rows: [],
|
||||
datas: {},
|
||||
});
|
||||
this.setState({
|
||||
selectedIds: [],
|
||||
});
|
||||
};
|
||||
|
||||
createPaste = () => {
|
||||
if (!this.props.persistedState.tableMetadata) {
|
||||
return;
|
||||
}
|
||||
let paste = '';
|
||||
const mapFn = row =>
|
||||
(
|
||||
(this.props.persistedState.tableMetadata &&
|
||||
Object.keys(this.props.persistedState.tableMetadata.columns)) ||
|
||||
[]
|
||||
)
|
||||
.map(key => textContent(row.columns[key].value))
|
||||
.join('\t');
|
||||
|
||||
if (this.state.selectedIds.length > 0) {
|
||||
// create paste from selection
|
||||
paste = this.props.persistedState.rows
|
||||
.filter(row => this.state.selectedIds.indexOf(row.key) > -1)
|
||||
.map(mapFn)
|
||||
.join('\n');
|
||||
} else {
|
||||
// create paste with all rows
|
||||
paste = this.props.persistedState.rows.map(mapFn).join('\n');
|
||||
}
|
||||
createPaste(paste);
|
||||
};
|
||||
|
||||
onRowHighlighted = (keys: TableHighlightedRows) => {
|
||||
this.setState({
|
||||
selectedIds: keys,
|
||||
});
|
||||
};
|
||||
|
||||
// We don't necessarily have the table metadata at the time when buildRow
|
||||
// is being used. This includes presentation layer info like which
|
||||
// columns should be filterable. This does a pass over the built rows and
|
||||
// applies that presentation layer information.
|
||||
applyMetadataToRows(rows: TableRows): TableRows {
|
||||
if (!this.props.persistedState.tableMetadata) {
|
||||
console.error(
|
||||
'applyMetadataToRows called without tableMetadata present',
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
return rows.map(r => {
|
||||
return {
|
||||
...r,
|
||||
columns: Object.keys(r.columns).reduce((map, columnName) => {
|
||||
map[columnName].isFilterable =
|
||||
this.props.persistedState.tableMetadata &&
|
||||
this.props.persistedState.tableMetadata.filterableColumns
|
||||
? this.props.persistedState.tableMetadata.filterableColumns.has(
|
||||
columnName,
|
||||
)
|
||||
: false;
|
||||
return map;
|
||||
}, r.columns),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
renderSidebar = () => {
|
||||
const {selectedIds} = this.state;
|
||||
const {datas} = this.props.persistedState;
|
||||
const selectedId = selectedIds.length !== 1 ? null : selectedIds[0];
|
||||
|
||||
if (selectedId != null) {
|
||||
return renderSidebarForRow(datas[selectedId]);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.error) {
|
||||
return <ErrorBlock error={this.state.error} />;
|
||||
}
|
||||
if (!this.props.persistedState.tableMetadata) {
|
||||
return 'Loading...';
|
||||
}
|
||||
const {
|
||||
columns,
|
||||
columnSizes,
|
||||
columnOrder,
|
||||
} = this.props.persistedState.tableMetadata;
|
||||
const {rows} = this.props.persistedState;
|
||||
|
||||
return (
|
||||
<FlexColumn grow={true}>
|
||||
<SearchableTable
|
||||
key={this.constructor.id}
|
||||
rowLineHeight={28}
|
||||
floating={false}
|
||||
multiline={true}
|
||||
columnSizes={columnSizes}
|
||||
columnOrder={columnOrder}
|
||||
columns={columns}
|
||||
onRowHighlighted={this.onRowHighlighted}
|
||||
multiHighlight={true}
|
||||
rows={this.applyMetadataToRows(rows)}
|
||||
stickyBottom={true}
|
||||
actions={<Button onClick={this.clear}>Clear Table</Button>}
|
||||
/>
|
||||
<DetailSidebar>{this.renderSidebar()}</DetailSidebar>
|
||||
</FlexColumn>
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user