Summary: The different is small after the change from storing pre-rendered data to raw data. Hence, the diff changes it to match `currentPage` to ease the assignment (used in next diffs) Reviewed By: jknoxville Differential Revision: D21788245 fbshipit-source-id: 87bdbce33f0615bf31878797a74fce5d5969db7b
1322 lines
34 KiB
TypeScript
1322 lines
34 KiB
TypeScript
/**
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* 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,
|
|
produce,
|
|
Toolbar,
|
|
Select,
|
|
FlexColumn,
|
|
FlexRow,
|
|
ManagedTable,
|
|
Text,
|
|
Button,
|
|
ButtonGroup,
|
|
Input,
|
|
colors,
|
|
getStringFromErrorLike,
|
|
Spacer,
|
|
Textarea,
|
|
TableBodyColumn,
|
|
TableRows,
|
|
Props as FlipperPluginProps,
|
|
TableBodyRow,
|
|
TableRowSortOrder,
|
|
FlipperPlugin,
|
|
Value,
|
|
renderValue,
|
|
} from 'flipper';
|
|
import React, {Component, KeyboardEvent, ChangeEvent} from 'react';
|
|
import {DatabaseClient} from './ClientProtocol';
|
|
import ButtonNavigation from './ButtonNavigation';
|
|
import DatabaseDetailSidebar from './DatabaseDetailSidebar';
|
|
import DatabaseStructure from './DatabaseStructure';
|
|
import sqlFormatter from 'sql-formatter';
|
|
import dateFormat from 'dateformat';
|
|
|
|
const PAGE_SIZE = 50;
|
|
|
|
const BoldSpan = styled.span({
|
|
fontSize: 12,
|
|
color: '#90949c',
|
|
fontWeight: 'bold',
|
|
textTransform: 'uppercase',
|
|
});
|
|
const ErrorBar = styled.div({
|
|
backgroundColor: colors.cherry,
|
|
color: colors.white,
|
|
lineHeight: '26px',
|
|
textAlign: 'center',
|
|
});
|
|
const QueryHistoryManagedTable = styled(ManagedTable)({paddingLeft: 16});
|
|
const PageInfoContainer = styled(FlexRow)({alignItems: 'center'});
|
|
const TableInfoTextArea = styled(Textarea)({
|
|
width: '98%',
|
|
height: '100%',
|
|
marginLeft: '1%',
|
|
marginTop: '1%',
|
|
marginBottom: '1%',
|
|
readOnly: true,
|
|
});
|
|
|
|
type DatabasesPluginState = {
|
|
selectedDatabase: number;
|
|
selectedDatabaseTable: string | null;
|
|
pageRowNumber: number;
|
|
databases: Array<DatabaseEntry>;
|
|
outdatedDatabaseList: boolean;
|
|
viewMode: 'data' | 'structure' | 'SQL' | 'tableInfo' | 'queryHistory';
|
|
error: null;
|
|
currentPage: Page | null;
|
|
currentStructure: Structure | null;
|
|
currentSort: TableRowSortOrder | null;
|
|
query: Query | null;
|
|
queryResult: QueryResult | null;
|
|
favorites: Array<string>;
|
|
executionTime: number;
|
|
tableInfo: string;
|
|
queryHistory: Array<Query>;
|
|
};
|
|
|
|
type Page = {
|
|
databaseId: number;
|
|
table: string;
|
|
columns: Array<string>;
|
|
rows: Array<Array<Value>>;
|
|
start: number;
|
|
count: number;
|
|
total: number;
|
|
highlightedRows: Array<number>;
|
|
};
|
|
|
|
export type Structure = {
|
|
databaseId: number;
|
|
table: string;
|
|
columns: Array<string>;
|
|
rows: Array<Array<Value>>;
|
|
indexesColumns: Array<string>;
|
|
indexesValues: Array<Array<Value>>;
|
|
};
|
|
|
|
type QueryResult = {
|
|
table: QueriedTable | null;
|
|
id: number | null;
|
|
count: number | null;
|
|
};
|
|
|
|
export type QueriedTable = {
|
|
columns: Array<string>;
|
|
rows: Array<Array<Value>>;
|
|
highlightedRows: Array<number>;
|
|
};
|
|
|
|
type Actions =
|
|
| SelectDatabaseEvent
|
|
| SelectDatabaseTableEvent
|
|
| UpdateDatabasesEvent
|
|
| UpdateViewModeEvent
|
|
| UpdatePageEvent
|
|
| UpdateStructureEvent
|
|
| DisplaySelectEvent
|
|
| DisplayInsertEvent
|
|
| DisplayUpdateDeleteEvent
|
|
| UpdateTableInfoEvent
|
|
| NextPageEvent
|
|
| PreviousPageEvent
|
|
| ExecuteEvent
|
|
| RefreshEvent
|
|
| UpdateFavoritesEvent
|
|
| SortByChangedEvent
|
|
| GoToRowEvent
|
|
| UpdateQueryEvent;
|
|
|
|
type DatabaseEntry = {
|
|
id: number;
|
|
name: string;
|
|
tables: Array<string>;
|
|
};
|
|
|
|
type Query = {
|
|
value: string;
|
|
time: 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' | 'SQL' | 'tableInfo' | 'queryHistory';
|
|
};
|
|
|
|
type UpdatePageEvent = {
|
|
type: 'UpdatePage';
|
|
} & Page;
|
|
|
|
type UpdateStructureEvent = {
|
|
type: 'UpdateStructure';
|
|
databaseId: number;
|
|
table: string;
|
|
columns: Array<string>;
|
|
rows: Array<Array<Value>>;
|
|
indexesColumns: Array<string>;
|
|
indexesValues: Array<Array<Value>>;
|
|
};
|
|
|
|
type DisplaySelectEvent = {
|
|
type: 'DisplaySelect';
|
|
columns: Array<string>;
|
|
values: Array<Array<Value>>;
|
|
};
|
|
|
|
type DisplayInsertEvent = {
|
|
type: 'DisplayInsert';
|
|
id: number;
|
|
};
|
|
|
|
type DisplayUpdateDeleteEvent = {
|
|
type: 'DisplayUpdateDelete';
|
|
count: number;
|
|
};
|
|
|
|
type UpdateTableInfoEvent = {
|
|
type: 'UpdateTableInfo';
|
|
tableInfo: string;
|
|
};
|
|
|
|
type NextPageEvent = {
|
|
type: 'NextPage';
|
|
};
|
|
|
|
type PreviousPageEvent = {
|
|
type: 'PreviousPage';
|
|
};
|
|
|
|
type ExecuteEvent = {
|
|
type: 'Execute';
|
|
query: string;
|
|
};
|
|
|
|
type RefreshEvent = {
|
|
type: 'Refresh';
|
|
};
|
|
|
|
type UpdateFavoritesEvent = {
|
|
type: 'UpdateFavorites';
|
|
favorites: Array<string> | undefined;
|
|
};
|
|
|
|
type SortByChangedEvent = {
|
|
type: 'SortByChanged';
|
|
sortOrder: TableRowSortOrder;
|
|
};
|
|
|
|
type GoToRowEvent = {
|
|
type: 'GoToRow';
|
|
row: number;
|
|
};
|
|
|
|
type UpdateQueryEvent = {
|
|
type: 'UpdateQuery';
|
|
value: string;
|
|
};
|
|
|
|
function transformRow(
|
|
columns: Array<string>,
|
|
row: Array<Value>,
|
|
index: number,
|
|
): TableBodyRow {
|
|
const transformedColumns: {[key: string]: TableBodyColumn} = {};
|
|
for (let i = 0; i < columns.length; i++) {
|
|
transformedColumns[columns[i]] = {value: renderValue(row[i], true)};
|
|
}
|
|
return {key: String(index), columns: transformedColumns};
|
|
}
|
|
|
|
function renderQueryHistory(history: Array<Query>) {
|
|
if (!history || typeof history === 'undefined') {
|
|
return null;
|
|
}
|
|
const columns = {
|
|
time: {
|
|
value: 'Time',
|
|
resizable: true,
|
|
},
|
|
query: {
|
|
value: 'Query',
|
|
resizable: true,
|
|
},
|
|
};
|
|
const rows: TableRows = [];
|
|
if (history.length > 0) {
|
|
for (const query of history) {
|
|
const time = query.time;
|
|
const value = query.value;
|
|
rows.push({
|
|
key: value,
|
|
columns: {time: {value: time}, query: {value: value}},
|
|
});
|
|
}
|
|
}
|
|
|
|
return (
|
|
<FlexRow grow={true}>
|
|
<QueryHistoryManagedTable
|
|
floating={false}
|
|
columns={columns}
|
|
columnSizes={{time: 75}}
|
|
zebra={true}
|
|
rows={rows}
|
|
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: ChangeEvent<any>) {
|
|
this.setState({inputValue: e.target.value});
|
|
}
|
|
|
|
onSubmit(e: KeyboardEvent) {
|
|
if (e.key === 'Enter') {
|
|
const rowNumber = parseInt(this.state.inputValue, 10);
|
|
this.props.onChange(rowNumber - 1, this.props.count);
|
|
this.setState({isOpen: false});
|
|
}
|
|
}
|
|
|
|
render() {
|
|
return (
|
|
<PageInfoContainer grow={true}>
|
|
<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).toString()}
|
|
onChange={this.onInputChanged.bind(this)}
|
|
onKeyDown={this.onSubmit.bind(this)}
|
|
/>
|
|
) : (
|
|
<Button
|
|
style={{textAlign: 'center'}}
|
|
onClick={this.onOpen.bind(this)}>
|
|
Go To Row
|
|
</Button>
|
|
)}
|
|
</PageInfoContainer>
|
|
);
|
|
}
|
|
}
|
|
|
|
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,
|
|
query: null,
|
|
queryResult: null,
|
|
favorites: [],
|
|
executionTime: 0,
|
|
tableInfo: '',
|
|
queryHistory: [],
|
|
};
|
|
|
|
reducers = ([
|
|
[
|
|
'UpdateDatabases',
|
|
(
|
|
state: DatabasesPluginState,
|
|
results: UpdateDatabasesEvent,
|
|
): DatabasesPluginState => {
|
|
const updates = results.databases;
|
|
const databases = updates.sort((db1, db2) => db1.id - db2.id);
|
|
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,
|
|
error: null,
|
|
};
|
|
},
|
|
],
|
|
[
|
|
'UpdatePage',
|
|
(
|
|
state: DatabasesPluginState,
|
|
event: UpdatePageEvent,
|
|
): DatabasesPluginState => {
|
|
return {
|
|
...state,
|
|
currentPage: event,
|
|
};
|
|
},
|
|
],
|
|
[
|
|
'UpdateStructure',
|
|
(
|
|
state: DatabasesPluginState,
|
|
event: UpdateStructureEvent,
|
|
): DatabasesPluginState => {
|
|
return {
|
|
...state,
|
|
currentStructure: {
|
|
databaseId: event.databaseId,
|
|
table: event.table,
|
|
columns: event.columns,
|
|
rows: event.rows,
|
|
indexesColumns: event.indexesColumns,
|
|
indexesValues: event.indexesValues,
|
|
},
|
|
};
|
|
},
|
|
],
|
|
[
|
|
'DisplaySelect',
|
|
(
|
|
state: DatabasesPluginState,
|
|
event: DisplaySelectEvent,
|
|
): DatabasesPluginState => {
|
|
return {
|
|
...state,
|
|
queryResult: {
|
|
table: {
|
|
columns: event.columns,
|
|
rows: event.values,
|
|
highlightedRows: [],
|
|
},
|
|
id: null,
|
|
count: null,
|
|
},
|
|
};
|
|
},
|
|
],
|
|
[
|
|
'DisplayInsert',
|
|
(
|
|
state: DatabasesPluginState,
|
|
event: DisplayInsertEvent,
|
|
): DatabasesPluginState => {
|
|
return {
|
|
...state,
|
|
queryResult: {
|
|
table: null,
|
|
id: event.id,
|
|
count: null,
|
|
},
|
|
};
|
|
},
|
|
],
|
|
|
|
[
|
|
'DisplayUpdateDelete',
|
|
(
|
|
state: DatabasesPluginState,
|
|
event: DisplayUpdateDeleteEvent,
|
|
): DatabasesPluginState => {
|
|
return {
|
|
...state,
|
|
queryResult: {
|
|
table: null,
|
|
id: null,
|
|
count: event.count,
|
|
},
|
|
};
|
|
},
|
|
],
|
|
[
|
|
'UpdateTableInfo',
|
|
(
|
|
state: DatabasesPluginState,
|
|
event: UpdateTableInfoEvent,
|
|
): DatabasesPluginState => {
|
|
return {
|
|
...state,
|
|
tableInfo: event.tableInfo,
|
|
};
|
|
},
|
|
],
|
|
[
|
|
'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,
|
|
};
|
|
},
|
|
],
|
|
[
|
|
'Execute',
|
|
(
|
|
state: DatabasesPluginState,
|
|
event: ExecuteEvent,
|
|
): DatabasesPluginState => {
|
|
const timeBefore = Date.now();
|
|
const {query} = event;
|
|
this.databaseClient
|
|
.getExecution({databaseId: state.selectedDatabase, value: query})
|
|
.then((data) => {
|
|
this.setState({
|
|
error: null,
|
|
executionTime: Date.now() - timeBefore,
|
|
});
|
|
if (data.type === 'select') {
|
|
this.dispatchAction({
|
|
type: 'DisplaySelect',
|
|
columns: data.columns,
|
|
values: data.values,
|
|
});
|
|
} else if (data.type === 'insert') {
|
|
this.dispatchAction({
|
|
type: 'DisplayInsert',
|
|
id: data.insertedId,
|
|
});
|
|
} else if (data.type === 'update_delete') {
|
|
this.dispatchAction({
|
|
type: 'DisplayUpdateDelete',
|
|
count: data.affectedCount,
|
|
});
|
|
}
|
|
})
|
|
.catch((e) => {
|
|
this.setState({error: e});
|
|
});
|
|
let newHistory = this.state.queryHistory;
|
|
const newQuery = this.state.query;
|
|
if (
|
|
newQuery !== null &&
|
|
typeof newQuery !== 'undefined' &&
|
|
newHistory !== null &&
|
|
typeof newHistory !== 'undefined'
|
|
) {
|
|
newQuery.time = dateFormat(new Date(), 'hh:MM:ss');
|
|
newHistory = newHistory.concat(newQuery);
|
|
}
|
|
return {
|
|
...state,
|
|
queryHistory: newHistory,
|
|
};
|
|
},
|
|
],
|
|
[
|
|
'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,
|
|
};
|
|
},
|
|
],
|
|
[
|
|
'UpdateFavorites',
|
|
(
|
|
state: DatabasesPluginState,
|
|
event: UpdateFavoritesEvent,
|
|
): DatabasesPluginState => {
|
|
let newFavorites = event.favorites || state.favorites;
|
|
if (
|
|
state.query &&
|
|
state.query !== null &&
|
|
typeof state.query !== 'undefined'
|
|
) {
|
|
const value = state.query.value;
|
|
if (newFavorites.includes(value)) {
|
|
const index = newFavorites.indexOf(value);
|
|
newFavorites.splice(index, 1);
|
|
} else {
|
|
newFavorites = state.favorites.concat(value);
|
|
}
|
|
}
|
|
window.localStorage.setItem(
|
|
'plugin-database-favorites-sql-queries',
|
|
JSON.stringify(newFavorites),
|
|
);
|
|
return {
|
|
...state,
|
|
favorites: newFavorites,
|
|
};
|
|
},
|
|
],
|
|
[
|
|
'UpdateViewMode',
|
|
(
|
|
state: DatabasesPluginState,
|
|
event: UpdateViewModeEvent,
|
|
): DatabasesPluginState => {
|
|
return {
|
|
...state,
|
|
viewMode: event.viewMode,
|
|
error: null,
|
|
};
|
|
},
|
|
],
|
|
[
|
|
'SortByChanged',
|
|
(state: DatabasesPluginState, event: SortByChangedEvent) => {
|
|
return {
|
|
...state,
|
|
currentSort: event.sortOrder,
|
|
pageRowNumber: 0,
|
|
currentPage: null,
|
|
};
|
|
},
|
|
],
|
|
[
|
|
'UpdateQuery',
|
|
(state: DatabasesPluginState, event: UpdateQueryEvent) => {
|
|
return {
|
|
...state,
|
|
query: {
|
|
value: event.value,
|
|
time: dateFormat(new Date(), 'hh:MM:ss'),
|
|
},
|
|
};
|
|
},
|
|
],
|
|
] as Array<
|
|
[
|
|
string,
|
|
(
|
|
state: DatabasesPluginState,
|
|
event: UpdateQueryEvent,
|
|
) => DatabasesPluginState,
|
|
]
|
|
>).reduce(
|
|
(
|
|
acc: {
|
|
[name: string]: (
|
|
state: DatabasesPluginState,
|
|
event: UpdateQueryEvent,
|
|
) => DatabasesPluginState;
|
|
},
|
|
val: [
|
|
string,
|
|
(
|
|
state: DatabasesPluginState,
|
|
event: UpdateQueryEvent,
|
|
) => DatabasesPluginState,
|
|
],
|
|
) => {
|
|
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) => {
|
|
this.dispatchAction({
|
|
type: 'UpdatePage',
|
|
databaseId: databaseId,
|
|
table: table,
|
|
columns: data.columns,
|
|
rows: data.values,
|
|
start: data.start,
|
|
count: data.count,
|
|
total: data.total,
|
|
highlightedRows: [],
|
|
});
|
|
})
|
|
.catch((e) => {
|
|
this.setState({error: e});
|
|
});
|
|
}
|
|
if (newState.currentStructure === null && databaseId && table) {
|
|
this.databaseClient
|
|
.getTableStructure({
|
|
databaseId: databaseId,
|
|
table: table,
|
|
})
|
|
.then((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 (
|
|
newState.viewMode === 'tableInfo' &&
|
|
newState.currentStructure === null &&
|
|
databaseId &&
|
|
table
|
|
) {
|
|
this.databaseClient
|
|
.getTableInfo({
|
|
databaseId: databaseId,
|
|
table: table,
|
|
})
|
|
.then((data) => {
|
|
this.dispatchAction({
|
|
type: 'UpdateTableInfo',
|
|
tableInfo: data.definition,
|
|
});
|
|
})
|
|
.catch((e) => {
|
|
this.setState({error: e});
|
|
});
|
|
}
|
|
|
|
if (!previousState.outdatedDatabaseList && newState.outdatedDatabaseList) {
|
|
this.databaseClient.getDatabases({}).then((databases) => {
|
|
this.dispatchAction({
|
|
type: 'UpdateDatabases',
|
|
databases,
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
// to keep eslint happy
|
|
constructor(props: FlipperPluginProps<{}>) {
|
|
super(props);
|
|
this.databaseClient = new DatabaseClient(this.client);
|
|
}
|
|
|
|
init() {
|
|
this.databaseClient = new DatabaseClient(this.client);
|
|
this.databaseClient.getDatabases({}).then((databases) => {
|
|
this.dispatchAction({
|
|
type: 'UpdateDatabases',
|
|
databases,
|
|
});
|
|
});
|
|
this.dispatchAction({
|
|
type: 'UpdateFavorites',
|
|
favorites: JSON.parse(
|
|
localStorage.getItem('plugin-database-favorites-sql-queries') || '[]',
|
|
),
|
|
});
|
|
}
|
|
|
|
onDataClicked = () => {
|
|
this.dispatchAction({type: 'UpdateViewMode', viewMode: 'data'});
|
|
};
|
|
|
|
onStructureClicked = () => {
|
|
this.dispatchAction({type: 'UpdateViewMode', viewMode: 'structure'});
|
|
};
|
|
|
|
onSQLClicked = () => {
|
|
this.dispatchAction({type: 'UpdateViewMode', viewMode: 'SQL'});
|
|
};
|
|
|
|
onTableInfoClicked = () => {
|
|
this.dispatchAction({type: 'UpdateViewMode', viewMode: 'tableInfo'});
|
|
};
|
|
|
|
onQueryHistoryClicked = () => {
|
|
this.dispatchAction({type: 'UpdateViewMode', viewMode: 'queryHistory'});
|
|
};
|
|
|
|
onRefreshClicked = () => {
|
|
this.setState({error: null});
|
|
this.dispatchAction({type: 'Refresh'});
|
|
};
|
|
|
|
onFavoritesClicked = () => {
|
|
this.dispatchAction({
|
|
type: 'UpdateFavorites',
|
|
favorites: this.state.favorites,
|
|
});
|
|
};
|
|
|
|
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'});
|
|
};
|
|
|
|
onExecuteClicked = () => {
|
|
if (this.state.query !== null) {
|
|
this.dispatchAction({type: 'Execute', query: this.state.query.value});
|
|
}
|
|
};
|
|
|
|
onQueryTextareaKeyPress = (event: KeyboardEvent) => {
|
|
// Implement ctrl+enter as a shortcut for clicking 'Execute'.
|
|
if (event.key === '\n' && event.ctrlKey) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
this.onExecuteClicked();
|
|
}
|
|
};
|
|
|
|
onFavoriteClicked = (selected: any) => {
|
|
this.setState({query: selected.target.value});
|
|
};
|
|
|
|
onGoToRow = (row: number, _count: number) => {
|
|
this.dispatchAction({type: 'GoToRow', row: row});
|
|
};
|
|
|
|
onQueryChanged = (selected: any) => {
|
|
this.dispatchAction({
|
|
type: 'UpdateQuery',
|
|
value: selected.target.value,
|
|
});
|
|
};
|
|
|
|
renderTable(page: Page | null) {
|
|
if (!page) {
|
|
return null;
|
|
}
|
|
return (
|
|
<FlexRow grow={true}>
|
|
<ManagedTable
|
|
tableKey={`databases-${page.databaseId}-${page.table}`}
|
|
floating={false}
|
|
columnOrder={page.columns.map((name) => ({
|
|
key: name,
|
|
visible: true,
|
|
}))}
|
|
columns={page.columns.reduce(
|
|
(acc, val) =>
|
|
Object.assign({}, acc, {
|
|
[val]: {value: val, resizable: true, sortable: true},
|
|
}),
|
|
{},
|
|
)}
|
|
zebra={true}
|
|
rows={page.rows.map((row: Array<Value>, index: number) =>
|
|
transformRow(page.columns, row, index),
|
|
)}
|
|
horizontallyScrollable={true}
|
|
multiHighlight={true}
|
|
onRowHighlighted={(highlightedRows) =>
|
|
this.setState(
|
|
produce((draftState: DatabasesPluginState) => {
|
|
if (draftState.currentPage !== null) {
|
|
draftState.currentPage.highlightedRows = highlightedRows.map(
|
|
parseInt,
|
|
);
|
|
}
|
|
}),
|
|
)
|
|
}
|
|
onSort={(sortOrder: TableRowSortOrder) => {
|
|
this.dispatchAction({
|
|
type: 'SortByChanged',
|
|
sortOrder,
|
|
});
|
|
}}
|
|
initialSortOrder={this.state.currentSort ?? undefined}
|
|
/>
|
|
{page.highlightedRows.length === 1 && (
|
|
<DatabaseDetailSidebar
|
|
columnLabels={page.columns}
|
|
columnValues={page.rows[page.highlightedRows[0]]}
|
|
/>
|
|
)}
|
|
</FlexRow>
|
|
);
|
|
}
|
|
|
|
renderQuery(query: QueryResult | null) {
|
|
if (!query || query === null) {
|
|
return null;
|
|
}
|
|
if (
|
|
query.table &&
|
|
typeof query.table !== 'undefined' &&
|
|
query.table !== null
|
|
) {
|
|
const table = query.table;
|
|
const columns = table.columns;
|
|
const rows = table.rows;
|
|
return (
|
|
<FlexRow grow={true} style={{paddingTop: 18}}>
|
|
<ManagedTable
|
|
floating={false}
|
|
multiline={true}
|
|
columnOrder={columns.map((name) => ({
|
|
key: name,
|
|
visible: true,
|
|
}))}
|
|
columns={columns.reduce(
|
|
(acc, val) =>
|
|
Object.assign({}, acc, {[val]: {value: val, resizable: true}}),
|
|
{},
|
|
)}
|
|
zebra={true}
|
|
rows={rows.map((row: Array<Value>, index: number) =>
|
|
transformRow(columns, row, index),
|
|
)}
|
|
horizontallyScrollable={true}
|
|
onRowHighlighted={(highlightedRows) => {
|
|
this.setState({
|
|
queryResult: {
|
|
table: {
|
|
columns: columns,
|
|
rows: rows,
|
|
highlightedRows: highlightedRows.map(parseInt),
|
|
},
|
|
id: null,
|
|
count: null,
|
|
},
|
|
});
|
|
}}
|
|
/>
|
|
{table.highlightedRows.length === 1 && (
|
|
<DatabaseDetailSidebar
|
|
columnLabels={table.columns}
|
|
columnValues={table.rows[table.highlightedRows[0]]}
|
|
/>
|
|
)}
|
|
</FlexRow>
|
|
);
|
|
} else if (query.id && query.id !== null) {
|
|
return (
|
|
<FlexRow grow={true} style={{paddingTop: 18}}>
|
|
<Text style={{paddingTop: 8, paddingLeft: 8}}>
|
|
Row id: {query.id}
|
|
</Text>
|
|
</FlexRow>
|
|
);
|
|
} else if (query.count && query.count !== null) {
|
|
return (
|
|
<FlexRow grow={true} style={{paddingTop: 18}}>
|
|
<Text style={{paddingTop: 8, paddingLeft: 8}}>
|
|
Rows affected: {query.count}
|
|
</Text>
|
|
</FlexRow>
|
|
);
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
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: 16}}>
|
|
<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>
|
|
<Button
|
|
icon={'magnifying-glass'}
|
|
onClick={this.onSQLClicked}
|
|
selected={this.state.viewMode === 'SQL'}>
|
|
SQL
|
|
</Button>
|
|
<Button
|
|
icon={'info-cursive'}
|
|
onClick={this.onTableInfoClicked}
|
|
selected={this.state.viewMode === 'tableInfo'}>
|
|
Table Info
|
|
</Button>
|
|
<Button
|
|
icon={'on-this-day'}
|
|
iconSize={12}
|
|
onClick={this.onQueryHistoryClicked}
|
|
selected={this.state.viewMode === 'queryHistory'}>
|
|
Query History
|
|
</Button>
|
|
</ButtonGroup>
|
|
</Toolbar>
|
|
{this.state.viewMode === 'data' ||
|
|
this.state.viewMode === 'structure' ||
|
|
this.state.viewMode === 'tableInfo' ? (
|
|
<Toolbar position="top" style={{paddingLeft: 16}}>
|
|
<BoldSpan style={{marginRight: 16}}>Database</BoldSpan>
|
|
<Select
|
|
options={this.state.databases
|
|
.map((x) => x.name)
|
|
.reduce(
|
|
(obj, item) => Object.assign({}, obj, {[item]: item}),
|
|
{},
|
|
)}
|
|
selected={
|
|
this.state.databases[this.state.selectedDatabase - 1]?.name
|
|
}
|
|
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>
|
|
</Toolbar>
|
|
) : null}
|
|
{this.state.viewMode === 'SQL' ? (
|
|
<div>
|
|
<Toolbar position="top" style={{paddingLeft: 16}}>
|
|
<BoldSpan style={{marginRight: 16}}>Database</BoldSpan>
|
|
<Select
|
|
options={this.state.databases
|
|
.map((x) => x.name)
|
|
.reduce(
|
|
(obj, item) => Object.assign({}, obj, {[item]: item}),
|
|
{},
|
|
)}
|
|
selected={
|
|
this.state.databases[this.state.selectedDatabase - 1]?.name
|
|
}
|
|
onChange={this.onDatabaseSelected}
|
|
/>
|
|
</Toolbar>
|
|
{
|
|
<Textarea
|
|
style={{
|
|
width: '98%',
|
|
height: '40%',
|
|
marginLeft: 16,
|
|
marginTop: '1%',
|
|
marginBottom: '1%',
|
|
resize: 'vertical',
|
|
}}
|
|
onChange={this.onQueryChanged.bind(this)}
|
|
onKeyPress={this.onQueryTextareaKeyPress}
|
|
placeholder="Type query here.."
|
|
value={
|
|
this.state.query !== null &&
|
|
typeof this.state.query !== 'undefined'
|
|
? this.state.query.value
|
|
: undefined
|
|
}
|
|
/>
|
|
}
|
|
<Toolbar
|
|
position="top"
|
|
style={{paddingLeft: 16, paddingTop: 24, paddingBottom: 24}}>
|
|
<ButtonGroup>
|
|
<Button
|
|
icon={'star'}
|
|
iconSize={12}
|
|
iconVariant={
|
|
this.state.query !== null &&
|
|
typeof this.state.query !== 'undefined' &&
|
|
this.state.favorites.includes(this.state.query.value)
|
|
? 'filled'
|
|
: 'outline'
|
|
}
|
|
onClick={this.onFavoritesClicked}
|
|
/>
|
|
{this.state.favorites !== null ? (
|
|
<Button
|
|
dropdown={this.state.favorites.map((option) => {
|
|
return {
|
|
click: () => {
|
|
this.setState({
|
|
query: {
|
|
value: option,
|
|
time: dateFormat(new Date(), 'hh:MM:ss'),
|
|
},
|
|
});
|
|
this.onQueryChanged.bind(this);
|
|
},
|
|
label: option,
|
|
};
|
|
})}>
|
|
Choose from previous queries
|
|
</Button>
|
|
) : null}
|
|
</ButtonGroup>
|
|
<Spacer />
|
|
<ButtonGroup>
|
|
<Button
|
|
onClick={this.onExecuteClicked}
|
|
title={'Execute SQL [Ctrl+Return]'}>
|
|
Execute
|
|
</Button>
|
|
</ButtonGroup>
|
|
</Toolbar>
|
|
</div>
|
|
) : null}
|
|
<FlexRow grow={true}>
|
|
<FlexColumn grow={true}>
|
|
{this.state.viewMode === 'data'
|
|
? this.renderTable(this.state.currentPage)
|
|
: null}
|
|
{this.state.viewMode === 'structure' ? (
|
|
<DatabaseStructure structure={this.state.currentStructure} />
|
|
) : null}
|
|
{this.state.viewMode === 'SQL'
|
|
? this.renderQuery(this.state.queryResult)
|
|
: null}
|
|
{this.state.viewMode === 'tableInfo' ? (
|
|
<TableInfoTextArea
|
|
value={sqlFormatter.format(this.state.tableInfo)}
|
|
/>
|
|
) : null}
|
|
{this.state.viewMode === 'queryHistory'
|
|
? renderQueryHistory(this.state.queryHistory)
|
|
: null}
|
|
</FlexColumn>
|
|
</FlexRow>
|
|
<Toolbar position="bottom" style={{paddingLeft: 8}}>
|
|
<FlexRow grow={true}>
|
|
{this.state.viewMode === 'SQL' && this.state.executionTime !== 0 ? (
|
|
<Text> {this.state.executionTime} ms </Text>
|
|
) : null}
|
|
{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 && (
|
|
<ErrorBar>{getStringFromErrorLike(this.state.error)}</ErrorBar>
|
|
)}
|
|
</FlexColumn>
|
|
);
|
|
}
|
|
}
|