Summary: React is printing errors saying not to use "Span", and booleans in html properties Reviewed By: danielbuechele Differential Revision: D15535593 fbshipit-source-id: e074137c89abfa69625b370087c9c18b579ff279
763 lines
19 KiB
JavaScript
763 lines
19 KiB
JavaScript
/**
|
|
* Copyright 2018-present Facebook.
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
* @format
|
|
*/
|
|
|
|
import {
|
|
styled,
|
|
Toolbar,
|
|
Select,
|
|
FlexColumn,
|
|
FlexRow,
|
|
ManagedTable,
|
|
Text,
|
|
Button,
|
|
ButtonGroup,
|
|
Input,
|
|
} from 'flipper';
|
|
import {Component} from 'react';
|
|
import type {
|
|
TableBodyRow,
|
|
TableRowSortOrder,
|
|
} from '../../ui/components/table/types';
|
|
import {FlipperPlugin} from 'flipper';
|
|
import {DatabaseClient} from './ClientProtocol';
|
|
import {renderValue} from 'flipper';
|
|
import type {Value} from 'flipper';
|
|
import ButtonNavigation from './ButtonNavigation';
|
|
import _ from 'lodash';
|
|
|
|
const PAGE_SIZE = 50;
|
|
|
|
const BoldSpan = styled('span')({
|
|
fontSize: 12,
|
|
color: '#90949c',
|
|
fontWeight: 'bold',
|
|
textTransform: 'uppercase',
|
|
});
|
|
|
|
type DatabasesPluginState = {|
|
|
selectedDatabase: number,
|
|
selectedDatabaseTable: ?string,
|
|
pageRowNumber: number,
|
|
databases: Array<DatabaseEntry>,
|
|
outdatedDatabaseList: boolean,
|
|
viewMode: 'data' | 'structure',
|
|
error: ?null,
|
|
currentPage: ?Page,
|
|
currentStructure: ?Structure,
|
|
currentSort: ?TableRowSortOrder,
|
|
|};
|
|
|
|
type Page = {
|
|
databaseId: number,
|
|
table: string,
|
|
columns: Array<string>,
|
|
rows: Array<TableBodyRow>,
|
|
start: number,
|
|
count: number,
|
|
total: number,
|
|
};
|
|
|
|
type Structure = {|
|
|
databaseId: number,
|
|
table: string,
|
|
columns: Array<string>,
|
|
rows: Array<TableBodyRow>,
|
|
indexesColumns: Array<string>,
|
|
indexesValues: Array<TableBodyRow>,
|
|
|};
|
|
|
|
type Actions =
|
|
| SelectDatabaseEvent
|
|
| SelectDatabaseTableEvent
|
|
| UpdateDatabasesEvent
|
|
| UpdateViewModeEvent
|
|
| UpdatePageEvent
|
|
| UpdateStructureEvent
|
|
| NextPageEvent
|
|
| PreviousPageEvent
|
|
| RefreshEvent
|
|
| SortByChangedEvent
|
|
| GoToRowEvent;
|
|
|
|
type DatabaseEntry = {
|
|
id: number,
|
|
name: string,
|
|
tables: Array<string>,
|
|
};
|
|
|
|
type UpdateDatabasesEvent = {|
|
|
databases: Array<{name: string, id: number, tables: Array<string>}>,
|
|
type: 'UpdateDatabases',
|
|
|};
|
|
|
|
type SelectDatabaseEvent = {|
|
|
type: 'UpdateSelectedDatabase',
|
|
database: number,
|
|
|};
|
|
|
|
type SelectDatabaseTableEvent = {|
|
|
type: 'UpdateSelectedDatabaseTable',
|
|
table: string,
|
|
|};
|
|
|
|
type UpdateViewModeEvent = {|
|
|
type: 'UpdateViewMode',
|
|
viewMode: 'data' | 'structure',
|
|
|};
|
|
|
|
type UpdatePageEvent = {|
|
|
type: 'UpdatePage',
|
|
databaseId: number,
|
|
table: string,
|
|
columns: Array<string>,
|
|
values: Array<Array<any>>,
|
|
start: number,
|
|
count: number,
|
|
total: number,
|
|
|};
|
|
|
|
type UpdateStructureEvent = {|
|
|
type: 'UpdateStructure',
|
|
databaseId: number,
|
|
table: string,
|
|
columns: Array<string>,
|
|
rows: Array<Array<any>>,
|
|
indexesColumns: Array<string>,
|
|
indexesValues: Array<Array<any>>,
|
|
|};
|
|
|
|
type NextPageEvent = {
|
|
type: 'NextPage',
|
|
};
|
|
|
|
type PreviousPageEvent = {
|
|
type: 'PreviousPage',
|
|
};
|
|
|
|
type RefreshEvent = {
|
|
type: 'Refresh',
|
|
};
|
|
|
|
type SortByChangedEvent = {
|
|
type: 'SortByChanged',
|
|
sortOrder: TableRowSortOrder,
|
|
};
|
|
|
|
type GoToRowEvent = {
|
|
type: 'GoToRow',
|
|
row: number,
|
|
};
|
|
|
|
function transformRow(
|
|
columns: Array<string>,
|
|
row: Array<Value>,
|
|
index: number,
|
|
): TableBodyRow {
|
|
const transformedColumns = {};
|
|
for (var i = 0; i < columns.length; i++) {
|
|
transformedColumns[columns[i]] = {value: renderValue(row[i])};
|
|
}
|
|
return {key: String(index), columns: transformedColumns};
|
|
}
|
|
|
|
function renderTable(page: ?Page, component: DatabasesPlugin) {
|
|
if (!page) {
|
|
return null;
|
|
}
|
|
return (
|
|
<ManagedTable
|
|
tableKey={`databases-${page.databaseId}-${page.table}`}
|
|
floating={false}
|
|
columnOrder={page.columns.map(name => ({
|
|
key: name,
|
|
visible: true,
|
|
}))}
|
|
columns={page.columns.reduce((acc, val) => {
|
|
acc[val] = {value: val, resizable: true, sortable: true};
|
|
return acc;
|
|
}, {})}
|
|
zebra={true}
|
|
rows={page.rows}
|
|
horizontallyScrollable={true}
|
|
onSort={(sortOrder: TableRowSortOrder) => {
|
|
component.dispatchAction({
|
|
type: 'SortByChanged',
|
|
sortOrder,
|
|
});
|
|
}}
|
|
initialSortOrder={component.state.currentSort}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function renderDatabaseColumns(structure: ?Structure) {
|
|
if (!structure) {
|
|
return null;
|
|
}
|
|
return (
|
|
<FlexRow grow={true}>
|
|
<ManagedTable
|
|
floating={false}
|
|
columnOrder={structure.columns.map(name => ({
|
|
key: name,
|
|
visible: true,
|
|
}))}
|
|
columns={structure.columns.reduce((acc, val) => {
|
|
acc[val] = {value: val, resizable: true};
|
|
return acc;
|
|
}, {})}
|
|
zebra={true}
|
|
rows={structure.rows || []}
|
|
horizontallyScrollable={true}
|
|
/>
|
|
</FlexRow>
|
|
);
|
|
}
|
|
|
|
function renderDatabaseIndexes(structure: ?Structure) {
|
|
if (!structure) {
|
|
return null;
|
|
}
|
|
return (
|
|
<FlexRow grow={true}>
|
|
<ManagedTable
|
|
floating={false}
|
|
columnOrder={structure.indexesColumns.map(name => ({
|
|
key: name,
|
|
visible: true,
|
|
}))}
|
|
columns={structure.indexesColumns.reduce((acc, val) => {
|
|
acc[val] = {value: val, resizable: true};
|
|
return acc;
|
|
}, {})}
|
|
zebra={true}
|
|
rows={structure.indexesValues || []}
|
|
horizontallyScrollable={true}
|
|
/>
|
|
</FlexRow>
|
|
);
|
|
}
|
|
|
|
type PageInfoProps = {
|
|
currentRow: number,
|
|
count: number,
|
|
totalRows: number,
|
|
onChange: (currentRow: number, count: number) => void,
|
|
};
|
|
|
|
class PageInfo extends Component<
|
|
PageInfoProps,
|
|
{isOpen: boolean, inputValue: string},
|
|
> {
|
|
constructor(props: PageInfoProps) {
|
|
super(props);
|
|
this.state = {isOpen: false, inputValue: String(props.currentRow)};
|
|
}
|
|
|
|
onOpen() {
|
|
this.setState({isOpen: true});
|
|
}
|
|
|
|
onInputChanged(e) {
|
|
this.setState({inputValue: e.target.value});
|
|
}
|
|
|
|
onSubmit(e: SyntheticKeyboardEvent<>) {
|
|
if (e.key === 'Enter') {
|
|
const rowNumber = parseInt(this.state.inputValue);
|
|
console.log(rowNumber);
|
|
this.props.onChange(rowNumber - 1, this.props.count);
|
|
this.setState({isOpen: false});
|
|
}
|
|
}
|
|
|
|
render() {
|
|
return (
|
|
<FlexRow grow={true} alignItems={'center'}>
|
|
<div style={{flex: 1}} />
|
|
<Text>
|
|
{this.props.count === this.props.totalRows
|
|
? `${this.props.count} `
|
|
: `${this.props.currentRow + 1}-${this.props.currentRow +
|
|
this.props.count} `}
|
|
of {this.props.totalRows} rows
|
|
</Text>
|
|
<div style={{flex: 1}} />
|
|
{this.state.isOpen ? (
|
|
<Input
|
|
tabIndex={1}
|
|
placeholder={this.props.currentRow + 1}
|
|
onChange={this.onInputChanged.bind(this)}
|
|
onKeyDown={this.onSubmit.bind(this)}
|
|
/>
|
|
) : (
|
|
<Button
|
|
style={{textAlign: 'center'}}
|
|
onClick={this.onOpen.bind(this)}>
|
|
Go To Row
|
|
</Button>
|
|
)}
|
|
</FlexRow>
|
|
);
|
|
}
|
|
}
|
|
|
|
export default class DatabasesPlugin extends FlipperPlugin<
|
|
DatabasesPluginState,
|
|
Actions,
|
|
> {
|
|
databaseClient: DatabaseClient;
|
|
|
|
state: DatabasesPluginState = {
|
|
selectedDatabase: 0,
|
|
selectedDatabaseTable: null,
|
|
pageRowNumber: 0,
|
|
databases: [],
|
|
outdatedDatabaseList: true,
|
|
viewMode: 'data',
|
|
error: null,
|
|
currentPage: null,
|
|
currentStructure: null,
|
|
currentSort: null,
|
|
};
|
|
|
|
reducers = [
|
|
[
|
|
'UpdateDatabases',
|
|
(
|
|
state: DatabasesPluginState,
|
|
results: UpdateDatabasesEvent,
|
|
): DatabasesPluginState => {
|
|
const updates = results.databases;
|
|
const databases = updates;
|
|
const selectedDatabase =
|
|
state.selectedDatabase ||
|
|
(Object.values(databases)[0]
|
|
? // $FlowFixMe
|
|
Object.values(databases)[0].id
|
|
: 0);
|
|
const selectedTable =
|
|
state.selectedDatabaseTable &&
|
|
databases[selectedDatabase - 1].tables.includes(
|
|
state.selectedDatabaseTable,
|
|
)
|
|
? state.selectedDatabaseTable
|
|
: databases[selectedDatabase - 1].tables[0];
|
|
const sameTableSelected =
|
|
selectedDatabase === state.selectedDatabase &&
|
|
selectedTable === state.selectedDatabaseTable;
|
|
return {
|
|
...state,
|
|
databases,
|
|
outdatedDatabaseList: false,
|
|
selectedDatabase: selectedDatabase,
|
|
selectedDatabaseTable: selectedTable,
|
|
pageRowNumber: 0,
|
|
currentPage: sameTableSelected ? state.currentPage : null,
|
|
currentStructure: null,
|
|
currentSort: sameTableSelected ? state.currentSort : null,
|
|
};
|
|
},
|
|
],
|
|
[
|
|
'UpdateSelectedDatabase',
|
|
(
|
|
state: DatabasesPluginState,
|
|
event: SelectDatabaseEvent,
|
|
): DatabasesPluginState => {
|
|
return {
|
|
...state,
|
|
selectedDatabase: event.database,
|
|
selectedDatabaseTable:
|
|
state.databases[event.database - 1].tables[0] || null,
|
|
pageRowNumber: 0,
|
|
currentPage: null,
|
|
currentStructure: null,
|
|
currentSort: null,
|
|
};
|
|
},
|
|
],
|
|
[
|
|
'UpdateSelectedDatabaseTable',
|
|
(
|
|
state: DatabasesPluginState,
|
|
event: SelectDatabaseTableEvent,
|
|
): DatabasesPluginState => {
|
|
return {
|
|
...state,
|
|
selectedDatabaseTable: event.table,
|
|
pageRowNumber: 0,
|
|
currentPage: null,
|
|
currentStructure: null,
|
|
currentSort: null,
|
|
};
|
|
},
|
|
],
|
|
[
|
|
'UpdateViewMode',
|
|
(
|
|
state: DatabasesPluginState,
|
|
event: UpdateViewModeEvent,
|
|
): DatabasesPluginState => {
|
|
return {
|
|
...state,
|
|
viewMode: event.viewMode,
|
|
};
|
|
},
|
|
],
|
|
[
|
|
'UpdatePage',
|
|
(
|
|
state: DatabasesPluginState,
|
|
event: UpdatePageEvent,
|
|
): DatabasesPluginState => {
|
|
return {
|
|
...state,
|
|
currentPage: {
|
|
rows: event.values.map((row: Array<Value>, index: number) =>
|
|
transformRow(event.columns, row, index),
|
|
),
|
|
...event,
|
|
},
|
|
};
|
|
},
|
|
],
|
|
[
|
|
'UpdateStructure',
|
|
(
|
|
state: DatabasesPluginState,
|
|
event: UpdateStructureEvent,
|
|
): DatabasesPluginState => {
|
|
return {
|
|
...state,
|
|
currentStructure: {
|
|
databaseId: event.databaseId,
|
|
table: event.table,
|
|
columns: event.columns,
|
|
rows: event.rows.map((row: Array<Value>, index: number) =>
|
|
transformRow(event.columns, row, index),
|
|
),
|
|
indexesColumns: event.indexesColumns,
|
|
indexesValues: event.indexesValues.map(
|
|
(row: Array<Value>, index: number) =>
|
|
transformRow(event.columns, row, index),
|
|
),
|
|
},
|
|
};
|
|
},
|
|
],
|
|
[
|
|
'NextPage',
|
|
(
|
|
state: DatabasesPluginState,
|
|
event: UpdatePageEvent,
|
|
): DatabasesPluginState => {
|
|
return {
|
|
...state,
|
|
pageRowNumber: state.pageRowNumber + PAGE_SIZE,
|
|
currentPage: null,
|
|
};
|
|
},
|
|
],
|
|
[
|
|
'PreviousPage',
|
|
(
|
|
state: DatabasesPluginState,
|
|
event: UpdatePageEvent,
|
|
): DatabasesPluginState => {
|
|
return {
|
|
...state,
|
|
pageRowNumber: Math.max(state.pageRowNumber - PAGE_SIZE, 0),
|
|
currentPage: null,
|
|
};
|
|
},
|
|
],
|
|
[
|
|
'GoToRow',
|
|
(
|
|
state: DatabasesPluginState,
|
|
event: GoToRowEvent,
|
|
): DatabasesPluginState => {
|
|
if (!state.currentPage) {
|
|
return state;
|
|
}
|
|
const destinationRow =
|
|
event.row < 0
|
|
? 0
|
|
: event.row >= state.currentPage.total - PAGE_SIZE
|
|
? Math.max(state.currentPage.total - PAGE_SIZE, 0)
|
|
: event.row;
|
|
return {
|
|
...state,
|
|
pageRowNumber: destinationRow,
|
|
currentPage: null,
|
|
};
|
|
},
|
|
],
|
|
[
|
|
'Refresh',
|
|
(
|
|
state: DatabasesPluginState,
|
|
event: RefreshEvent,
|
|
): DatabasesPluginState => {
|
|
return {
|
|
...state,
|
|
outdatedDatabaseList: true,
|
|
currentPage: null,
|
|
};
|
|
},
|
|
],
|
|
[
|
|
'UpdateViewMode',
|
|
(
|
|
state: DatabasesPluginState,
|
|
event: UpdateViewModeEvent,
|
|
): DatabasesPluginState => {
|
|
return {
|
|
...state,
|
|
viewMode: event.viewMode,
|
|
};
|
|
},
|
|
],
|
|
[
|
|
'SortByChanged',
|
|
(state: DatabasesPluginState, event: SortByChangedEvent) => {
|
|
return {
|
|
...state,
|
|
currentSort: event.sortOrder,
|
|
pageRowNumber: 0,
|
|
currentPage: null,
|
|
};
|
|
},
|
|
],
|
|
].reduce((acc, val) => {
|
|
const name = val[0];
|
|
const f = val[1];
|
|
|
|
acc[name] = (previousState, event) => {
|
|
const newState = f(previousState, event);
|
|
this.onStateChanged(previousState, newState);
|
|
return newState;
|
|
};
|
|
return acc;
|
|
}, {});
|
|
|
|
onStateChanged(
|
|
previousState: DatabasesPluginState,
|
|
newState: DatabasesPluginState,
|
|
) {
|
|
const databaseId = newState.selectedDatabase;
|
|
const table = newState.selectedDatabaseTable;
|
|
if (
|
|
newState.viewMode === 'data' &&
|
|
newState.currentPage === null &&
|
|
databaseId &&
|
|
table
|
|
) {
|
|
this.databaseClient
|
|
.getTableData({
|
|
count: PAGE_SIZE,
|
|
databaseId: newState.selectedDatabase,
|
|
order: newState.currentSort?.key,
|
|
reverse: (newState.currentSort?.direction || 'up') === 'down',
|
|
table: table,
|
|
start: newState.pageRowNumber,
|
|
})
|
|
.then(data => {
|
|
console.log(data);
|
|
this.dispatchAction({
|
|
type: 'UpdatePage',
|
|
databaseId: databaseId,
|
|
table: table,
|
|
columns: data.columns,
|
|
values: data.values,
|
|
start: data.start,
|
|
count: data.count,
|
|
total: data.total,
|
|
});
|
|
})
|
|
.catch(e => {
|
|
this.setState({error: e});
|
|
});
|
|
}
|
|
if (
|
|
newState.viewMode === 'structure' &&
|
|
newState.currentStructure === null &&
|
|
databaseId &&
|
|
table
|
|
) {
|
|
this.databaseClient
|
|
.getTableStructure({
|
|
databaseId: databaseId,
|
|
table: table,
|
|
})
|
|
.then(data => {
|
|
console.log(data);
|
|
this.dispatchAction({
|
|
type: 'UpdateStructure',
|
|
databaseId: databaseId,
|
|
table: table,
|
|
columns: data.structureColumns,
|
|
rows: data.structureValues,
|
|
indexesColumns: data.indexesColumns,
|
|
indexesValues: data.indexesValues,
|
|
});
|
|
})
|
|
.catch(e => {
|
|
this.setState({error: e});
|
|
});
|
|
}
|
|
if (!previousState.outdatedDatabaseList && newState.outdatedDatabaseList) {
|
|
this.databaseClient.getDatabases({}).then(databases => {
|
|
this.dispatchAction({
|
|
type: 'UpdateDatabases',
|
|
databases,
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
init() {
|
|
this.databaseClient = new DatabaseClient(this.client);
|
|
this.databaseClient.getDatabases({}).then(databases => {
|
|
this.dispatchAction({
|
|
type: 'UpdateDatabases',
|
|
databases,
|
|
});
|
|
});
|
|
}
|
|
|
|
onDataClicked = () => {
|
|
this.dispatchAction({type: 'UpdateViewMode', viewMode: 'data'});
|
|
};
|
|
|
|
onStructureClicked = () => {
|
|
this.dispatchAction({type: 'UpdateViewMode', viewMode: 'structure'});
|
|
};
|
|
|
|
onRefreshClicked = () => {
|
|
this.dispatchAction({type: 'Refresh'});
|
|
};
|
|
|
|
onDatabaseSelected = (selected: string) => {
|
|
const dbId = this.state.databases.find(x => x.name === selected)?.id || 0;
|
|
this.dispatchAction({
|
|
database: dbId,
|
|
type: 'UpdateSelectedDatabase',
|
|
});
|
|
};
|
|
|
|
onDatabaseTableSelected = (selected: string) => {
|
|
this.dispatchAction({
|
|
table: selected,
|
|
type: 'UpdateSelectedDatabaseTable',
|
|
});
|
|
};
|
|
|
|
onNextPageClicked = () => {
|
|
this.dispatchAction({type: 'NextPage'});
|
|
};
|
|
|
|
onPreviousPageClicked = () => {
|
|
this.dispatchAction({type: 'PreviousPage'});
|
|
};
|
|
|
|
onGoToRow = (row: number, count: number) => {
|
|
this.dispatchAction({type: 'GoToRow', row: row});
|
|
};
|
|
|
|
renderStructure() {
|
|
return [
|
|
renderDatabaseColumns(this.state.currentStructure),
|
|
renderDatabaseIndexes(this.state.currentStructure),
|
|
];
|
|
}
|
|
|
|
render() {
|
|
const tableOptions =
|
|
(this.state.selectedDatabase &&
|
|
this.state.databases[this.state.selectedDatabase - 1] &&
|
|
this.state.databases[this.state.selectedDatabase - 1].tables.reduce(
|
|
(options, tableName) => ({...options, [tableName]: tableName}),
|
|
{},
|
|
)) ||
|
|
{};
|
|
|
|
return (
|
|
<FlexColumn style={{flex: 1}}>
|
|
<Toolbar position="top" style={{paddingLeft: 8}}>
|
|
<BoldSpan style={{marginRight: 16}}>Database</BoldSpan>
|
|
<Select
|
|
options={this.state.databases
|
|
.map(x => x.name)
|
|
.reduce((obj, item) => {
|
|
obj[item] = item;
|
|
return obj;
|
|
}, {})}
|
|
selected={String(this.state.selectedDatabase)}
|
|
onChange={this.onDatabaseSelected}
|
|
/>
|
|
<BoldSpan style={{marginLeft: 16, marginRight: 16}}>Table</BoldSpan>
|
|
<Select
|
|
options={tableOptions}
|
|
selected={this.state.selectedDatabaseTable}
|
|
onChange={this.onDatabaseTableSelected}
|
|
/>
|
|
<div />
|
|
<Button onClick={this.onRefreshClicked}>Refresh</Button>
|
|
<Button style={{marginLeft: 'auto', display: 'none'}}>
|
|
Execute SQL
|
|
</Button>
|
|
</Toolbar>
|
|
<FlexColumn grow={true}>
|
|
{this.state.viewMode === 'data'
|
|
? renderTable(this.state.currentPage, this)
|
|
: this.renderStructure()}
|
|
</FlexColumn>
|
|
<Toolbar position="bottom" style={{paddingLeft: 8}}>
|
|
<FlexRow grow={true}>
|
|
<ButtonGroup>
|
|
<Button
|
|
icon={'data-table'}
|
|
onClick={this.onDataClicked}
|
|
selected={this.state.viewMode === 'data'}>
|
|
Data
|
|
</Button>
|
|
<Button
|
|
icon={'gears-two'}
|
|
onClick={this.onStructureClicked}
|
|
selected={this.state.viewMode === 'structure'}>
|
|
Structure
|
|
</Button>
|
|
</ButtonGroup>
|
|
{this.state.viewMode === 'data' && this.state.currentPage ? (
|
|
<PageInfo
|
|
currentRow={this.state.currentPage.start}
|
|
count={this.state.currentPage.count}
|
|
totalRows={this.state.currentPage.total}
|
|
onChange={this.onGoToRow}
|
|
/>
|
|
) : null}
|
|
{this.state.viewMode === 'data' && this.state.currentPage ? (
|
|
<ButtonNavigation
|
|
canGoBack={this.state.currentPage.start > 0}
|
|
canGoForward={
|
|
this.state.currentPage.start + this.state.currentPage.count <
|
|
this.state.currentPage.total
|
|
}
|
|
onBack={this.onPreviousPageClicked}
|
|
onForward={this.onNextPageClicked}
|
|
/>
|
|
) : null}
|
|
</FlexRow>
|
|
</Toolbar>
|
|
{this.state.error && JSON.stringify(this.state.error)}
|
|
</FlexColumn>
|
|
);
|
|
}
|
|
}
|