Add databases plugin v0 (android) (#441)
Summary: Adds a plugin for listing the databases, tables and contents of those tables in an android app. Right now, works with sqlite, but it should be generic enough to work with other db types. ## Changelog Add initial version of android databases plugin Creating a PR, I may need to do some cleaning up, but this is to kick off that process. Pull Request resolved: https://github.com/facebook/flipper/pull/441 Reviewed By: danielbuechele Differential Revision: D15288831 Pulled By: jknoxville fbshipit-source-id: 6379ad60d640ea6b0a9473acc03dd6ea81a3a8d4
This commit is contained in:
committed by
Facebook Github Bot
parent
f20a781bca
commit
a630b50a8f
@@ -25,7 +25,7 @@ exports[`Empty app state matches snapshot 1`] = `
|
||||
className="css-1ayt83l"
|
||||
>
|
||||
<div
|
||||
className="css-rb77sv"
|
||||
className="css-z34oxs"
|
||||
disabled={true}
|
||||
onClick={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
@@ -40,7 +40,7 @@ exports[`Empty app state matches snapshot 1`] = `
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="css-rb77sv"
|
||||
className="css-z34oxs"
|
||||
disabled={true}
|
||||
onClick={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
@@ -96,7 +96,7 @@ exports[`Empty app state matches snapshot 1`] = `
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="css-rb77sv"
|
||||
className="css-z34oxs"
|
||||
disabled={true}
|
||||
onClick={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
|
||||
@@ -22,7 +22,7 @@ exports[`TitleBar is rendered 1`] = `
|
||||
className="css-1ayt83l"
|
||||
>
|
||||
<div
|
||||
className="css-rb77sv"
|
||||
className="css-z34oxs"
|
||||
disabled={true}
|
||||
onClick={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
@@ -37,7 +37,7 @@ exports[`TitleBar is rendered 1`] = `
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="css-rb77sv"
|
||||
className="css-z34oxs"
|
||||
disabled={true}
|
||||
onClick={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
@@ -93,7 +93,7 @@ exports[`TitleBar is rendered 1`] = `
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="css-rb77sv"
|
||||
className="css-z34oxs"
|
||||
disabled={true}
|
||||
onClick={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
|
||||
45
src/plugins/databases/ButtonNavigation.js
Normal file
45
src/plugins/databases/ButtonNavigation.js
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* 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 {Button, ButtonGroup, Glyph, colors} from 'flipper';
|
||||
|
||||
export default function ButtonNavigation(props: {|
|
||||
/** Back button is enabled */
|
||||
canGoBack: boolean,
|
||||
/** Forwards button is enabled */
|
||||
canGoForward: boolean,
|
||||
/** Callback when back button is clicked */
|
||||
onBack: () => void,
|
||||
/** Callback when forwards button is clicked */
|
||||
onForward: () => void,
|
||||
|}) {
|
||||
return (
|
||||
<ButtonGroup>
|
||||
<Button disabled={!props.canGoBack} onClick={props.onBack}>
|
||||
<Glyph
|
||||
name="chevron-left"
|
||||
size={16}
|
||||
color={
|
||||
props.canGoBack
|
||||
? colors.macOSTitleBarIconActive
|
||||
: colors.macOSTitleBarIconBlur
|
||||
}
|
||||
/>
|
||||
</Button>
|
||||
<Button disabled={!props.canGoForward} onClick={props.onForward}>
|
||||
<Glyph
|
||||
name="chevron-right"
|
||||
size={16}
|
||||
color={
|
||||
props.canGoForward
|
||||
? colors.macOSTitleBarIconActive
|
||||
: colors.macOSTitleBarIconBlur
|
||||
}
|
||||
/>
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
);
|
||||
}
|
||||
70
src/plugins/databases/ClientProtocol.js
Normal file
70
src/plugins/databases/ClientProtocol.js
Normal file
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* 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 type {PluginClient} from '../../plugin';
|
||||
import type {Value} from '../../ui/components/table/TypeBasedValueRenderer';
|
||||
|
||||
type ClientCall<Params, Response> = Params => Promise<Response>;
|
||||
|
||||
type DatabaseListRequest = {};
|
||||
|
||||
type DatabaseListResponse = Array<{
|
||||
id: number,
|
||||
name: string,
|
||||
tables: Array<string>,
|
||||
}>;
|
||||
|
||||
type QueryTableRequest = {
|
||||
databaseId: number,
|
||||
table: string,
|
||||
order?: string,
|
||||
reverse: boolean,
|
||||
start: number,
|
||||
count: number,
|
||||
};
|
||||
|
||||
type QueryTableResponse = {
|
||||
columns: Array<string>,
|
||||
values: Array<Array<Value>>,
|
||||
start: number,
|
||||
count: number,
|
||||
total: number,
|
||||
};
|
||||
|
||||
type GetTableStructureRequest = {
|
||||
databaseId: number,
|
||||
table: string,
|
||||
};
|
||||
|
||||
type GetTableStructureResponse = {
|
||||
structureColumns: Array<string>,
|
||||
structureValues: Array<Array<Value>>,
|
||||
indexesColumns: Array<string>,
|
||||
indexesValues: Array<Array<Value>>,
|
||||
definition: string,
|
||||
};
|
||||
|
||||
export class DatabaseClient {
|
||||
client: PluginClient;
|
||||
|
||||
constructor(pluginClient: PluginClient) {
|
||||
this.client = pluginClient;
|
||||
}
|
||||
|
||||
getDatabases: ClientCall<
|
||||
DatabaseListRequest,
|
||||
DatabaseListResponse,
|
||||
> = params => this.client.call('databaseList', {});
|
||||
|
||||
getTableData: ClientCall<QueryTableRequest, QueryTableResponse> = params =>
|
||||
this.client.call('getTableData', params);
|
||||
|
||||
getTableStructure: ClientCall<
|
||||
GetTableStructureRequest,
|
||||
GetTableStructureResponse,
|
||||
> = params => this.client.call('getTableStructure', params);
|
||||
}
|
||||
744
src/plugins/databases/index.js
Normal file
744
src/plugins/databases/index.js
Normal file
@@ -0,0 +1,744 @@
|
||||
/**
|
||||
* 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>,
|
||||
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: [],
|
||||
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 = databases[selectedDatabase - 1].tables[0];
|
||||
const sameTableSelected =
|
||||
selectedDatabase === state.selectedDatabase &&
|
||||
selectedTable === state.selectedDatabaseTable;
|
||||
return {
|
||||
...state,
|
||||
databases,
|
||||
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,
|
||||
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});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
this.databaseClient = new DatabaseClient(this.client);
|
||||
this.databaseClient.getDatabases({}).then(databases => {
|
||||
console.log(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 grow={true} />
|
||||
<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>
|
||||
);
|
||||
}
|
||||
}
|
||||
15
src/plugins/databases/package.json
Normal file
15
src/plugins/databases/package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "Databases",
|
||||
"version": "1.0.0",
|
||||
"title": "Databases",
|
||||
"icon": "internet",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.11"
|
||||
},
|
||||
"bugs": {
|
||||
"email": "oncall+flipper@xmail.facebook.com",
|
||||
"url": "https://fb.workplace.com/groups/230455004101832/"
|
||||
}
|
||||
}
|
||||
8
src/plugins/databases/yarn.lock
Normal file
8
src/plugins/databases/yarn.lock
Normal file
@@ -0,0 +1,8 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
lodash@^4.17.11:
|
||||
version "4.17.11"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
|
||||
integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==
|
||||
@@ -127,15 +127,17 @@ const StyledButton = styled('div')(props => ({
|
||||
marginLeft: 0,
|
||||
},
|
||||
|
||||
'&:active': {
|
||||
borderColor: colors.macOSTitleBarButtonBorder,
|
||||
borderBottomColor: colors.macOSTitleBarButtonBorderBottom,
|
||||
background: `linear-gradient(to bottom, ${
|
||||
colors.macOSTitleBarButtonBackgroundActiveHighlight
|
||||
} 1px, ${colors.macOSTitleBarButtonBackgroundActive} 0%, ${
|
||||
colors.macOSTitleBarButtonBorderBlur
|
||||
} 100%)`,
|
||||
},
|
||||
'&:active': props.disabled
|
||||
? null
|
||||
: {
|
||||
borderColor: colors.macOSTitleBarButtonBorder,
|
||||
borderBottomColor: colors.macOSTitleBarButtonBorderBottom,
|
||||
background: `linear-gradient(to bottom, ${
|
||||
colors.macOSTitleBarButtonBackgroundActiveHighlight
|
||||
} 1px, ${colors.macOSTitleBarButtonBackgroundActive} 0%, ${
|
||||
colors.macOSTitleBarButtonBorderBlur
|
||||
} 100%)`,
|
||||
},
|
||||
|
||||
'&:disabled': {
|
||||
borderColor: borderColor(props),
|
||||
|
||||
@@ -116,6 +116,7 @@ export type ManagedTableProps = {|
|
||||
* Allows to create context menu items for rows.
|
||||
*/
|
||||
buildContextMenuItems?: () => MenuTemplate,
|
||||
initialSortOrder?: ?TableRowSortOrder,
|
||||
/**
|
||||
* Callback when sorting changes.
|
||||
*/
|
||||
|
||||
@@ -171,7 +171,8 @@ class TableHeadColumn extends PureComponent<{
|
||||
<TableHeaderColumnInteractive
|
||||
grow={true}
|
||||
resizable={RIGHT_RESIZABLE}
|
||||
onResize={this.onResize}>
|
||||
onResize={this.onResize}
|
||||
minWidth={20}>
|
||||
{children}
|
||||
</TableHeaderColumnInteractive>
|
||||
);
|
||||
|
||||
68
src/ui/components/table/TypeBasedValueRenderer.js
Normal file
68
src/ui/components/table/TypeBasedValueRenderer.js
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* 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 {default as styled} from 'react-emotion';
|
||||
import {colors} from '../colors';
|
||||
import {default as Text} from '../Text';
|
||||
|
||||
export type Value =
|
||||
| {
|
||||
type: 'string',
|
||||
value: string,
|
||||
}
|
||||
| {
|
||||
type: 'boolean',
|
||||
value: boolean,
|
||||
}
|
||||
| {
|
||||
type: 'integer' | 'float' | 'double' | 'number',
|
||||
value: number,
|
||||
}
|
||||
| {
|
||||
type: 'null',
|
||||
};
|
||||
|
||||
const NonWrappingText = styled(Text)({
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
userSelect: 'none',
|
||||
});
|
||||
|
||||
const BooleanValue = styled(NonWrappingText)(props => ({
|
||||
'&::before': {
|
||||
content: '""',
|
||||
display: 'inline-block',
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
backgroundColor: props.active ? colors.green : colors.red,
|
||||
marginRight: 5,
|
||||
marginTop: 1,
|
||||
},
|
||||
}));
|
||||
|
||||
export function renderValue(val: Value) {
|
||||
switch (val.type) {
|
||||
case 'boolean':
|
||||
return (
|
||||
<BooleanValue code={true} active={val.value}>
|
||||
{val.value.toString()}
|
||||
</BooleanValue>
|
||||
);
|
||||
case 'string':
|
||||
return <NonWrappingText>{val.value}</NonWrappingText>;
|
||||
case 'integer':
|
||||
case 'float':
|
||||
case 'double':
|
||||
case 'number':
|
||||
return <NonWrappingText>{val.value}</NonWrappingText>;
|
||||
case 'null':
|
||||
return <NonWrappingText>NULL</NonWrappingText>;
|
||||
default:
|
||||
return <NonWrappingText>{val.value}</NonWrappingText>;
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ export {default as LoadingIndicator} from './components/LoadingIndicator.js';
|
||||
//
|
||||
export {default as Popover} from './components/Popover.js';
|
||||
|
||||
//
|
||||
// tables
|
||||
export type {
|
||||
TableColumns,
|
||||
TableRows,
|
||||
@@ -39,6 +39,8 @@ export type {
|
||||
} from './components/table/types.js';
|
||||
export {default as ManagedTable} from './components/table/ManagedTable.js';
|
||||
export type {ManagedTableProps} from './components/table/ManagedTable.js';
|
||||
export type {Value} from './components/table/TypeBasedValueRenderer.js';
|
||||
export {renderValue} from './components/table/TypeBasedValueRenderer.js';
|
||||
|
||||
//
|
||||
export type {
|
||||
|
||||
Reference in New Issue
Block a user