Frontend improvements for Databases plugin

Summary:
Specs here: https://fb.quip.com/aPPOAWMraRMT

Screenshots:
https://pxl.cl/Gngf
https://pxl.cl/Gngg
https://pxl.cl/Gngd
https://pxl.cl/GxQr
https://pxl.cl/Gngh
https://pxl.cl/Gngk
https://pxl.cl/Gngl
https://pxl.cl/Gngm

Reviewed By: quanturium

Differential Revision: D16266093

fbshipit-source-id: a5408b974875dcabcbd6055ccbb2818d0c1b25f6
This commit is contained in:
Chun-Ho Ng
2019-08-07 21:06:32 -07:00
committed by Facebook Github Bot
parent dbf0e3db43
commit fe56c8471c
7 changed files with 908 additions and 54 deletions

View File

@@ -1,7 +1,6 @@
[ignore] [ignore]
.*/scripts/.* .*/scripts/.*
.*/coverage/.* .*/coverage/.*
.*/node_modules/.*
.*/build/.* .*/build/.*
.*/dist/.* .*/dist/.*
.*/static/.* .*/static/.*
@@ -9,6 +8,7 @@
.*/website/.* .*/website/.*
<PROJECT_ROOT>/src/plugins/sections/d3/d3.js$ <PROJECT_ROOT>/src/plugins/sections/d3/d3.js$
.*\.tsx .*\.tsx
.*flow-typed/.*
[libs] [libs]
flow-typed flow-typed

View File

@@ -173,11 +173,14 @@ public class ObjectMapper {
DatabaseExecuteSqlResponse databaseExecuteSqlResponse) { DatabaseExecuteSqlResponse databaseExecuteSqlResponse) {
FlipperArray.Builder columnBuilder = new FlipperArray.Builder(); FlipperArray.Builder columnBuilder = new FlipperArray.Builder();
if (databaseExecuteSqlResponse.columns != null) {
for (String columnName : databaseExecuteSqlResponse.columns) { for (String columnName : databaseExecuteSqlResponse.columns) {
columnBuilder.put(columnName); columnBuilder.put(columnName);
} }
}
FlipperArray.Builder rowBuilder = new FlipperArray.Builder(); FlipperArray.Builder rowBuilder = new FlipperArray.Builder();
if (databaseExecuteSqlResponse.values != null) {
for (List<Object> row : databaseExecuteSqlResponse.values) { for (List<Object> row : databaseExecuteSqlResponse.values) {
FlipperArray.Builder valueBuilder = new FlipperArray.Builder(); FlipperArray.Builder valueBuilder = new FlipperArray.Builder();
for (Object item : row) { for (Object item : row) {
@@ -185,6 +188,7 @@ public class ObjectMapper {
} }
rowBuilder.put(valueBuilder.build()); rowBuilder.put(valueBuilder.build());
} }
}
return new FlipperObject.Builder() return new FlipperObject.Builder()
.put("type", databaseExecuteSqlResponse.type) .put("type", databaseExecuteSqlResponse.type)

193
flow-typed/npm/sql-formatter_vx.x.x.js vendored Normal file
View File

@@ -0,0 +1,193 @@
// flow-typed signature: 21bf70a7f73a77579f4cc048adda0919
// flow-typed version: <<STUB>>/sql-formatter_v2.3.3/flow_v0.102.0
/**
* This is an autogenerated libdef stub for:
*
* 'sql-formatter'
*
* Fill this stub out by replacing all the `any` types.
*
* Once filled out, we encourage you to share your work with the
* community by sending a pull request to:
* https://github.com/flowtype/flow-typed
*/
declare module 'sql-formatter' {
declare module.exports: any;
}
/**
* We include stubs for each file inside this npm package in case you need to
* require those files directly. Feel free to delete any files that aren't
* needed.
*/
declare module 'sql-formatter/dist/sql-formatter' {
declare module.exports: any;
}
declare module 'sql-formatter/dist/sql-formatter.min' {
declare module.exports: any;
}
declare module 'sql-formatter/lib/core/Formatter' {
declare module.exports: any;
}
declare module 'sql-formatter/lib/core/Indentation' {
declare module.exports: any;
}
declare module 'sql-formatter/lib/core/InlineBlock' {
declare module.exports: any;
}
declare module 'sql-formatter/lib/core/Params' {
declare module.exports: any;
}
declare module 'sql-formatter/lib/core/Tokenizer' {
declare module.exports: any;
}
declare module 'sql-formatter/lib/core/tokenTypes' {
declare module.exports: any;
}
declare module 'sql-formatter/lib/languages/Db2Formatter' {
declare module.exports: any;
}
declare module 'sql-formatter/lib/languages/N1qlFormatter' {
declare module.exports: any;
}
declare module 'sql-formatter/lib/languages/PlSqlFormatter' {
declare module.exports: any;
}
declare module 'sql-formatter/lib/languages/StandardSqlFormatter' {
declare module.exports: any;
}
declare module 'sql-formatter/lib/sqlFormatter' {
declare module.exports: any;
}
declare module 'sql-formatter/src/core/Formatter' {
declare module.exports: any;
}
declare module 'sql-formatter/src/core/Indentation' {
declare module.exports: any;
}
declare module 'sql-formatter/src/core/InlineBlock' {
declare module.exports: any;
}
declare module 'sql-formatter/src/core/Params' {
declare module.exports: any;
}
declare module 'sql-formatter/src/core/Tokenizer' {
declare module.exports: any;
}
declare module 'sql-formatter/src/core/tokenTypes' {
declare module.exports: any;
}
declare module 'sql-formatter/src/languages/Db2Formatter' {
declare module.exports: any;
}
declare module 'sql-formatter/src/languages/N1qlFormatter' {
declare module.exports: any;
}
declare module 'sql-formatter/src/languages/PlSqlFormatter' {
declare module.exports: any;
}
declare module 'sql-formatter/src/languages/StandardSqlFormatter' {
declare module.exports: any;
}
declare module 'sql-formatter/src/sqlFormatter' {
declare module.exports: any;
}
// Filename aliases
declare module 'sql-formatter/dist/sql-formatter.js' {
declare module.exports: $Exports<'sql-formatter/dist/sql-formatter'>;
}
declare module 'sql-formatter/dist/sql-formatter.min.js' {
declare module.exports: $Exports<'sql-formatter/dist/sql-formatter.min'>;
}
declare module 'sql-formatter/lib/core/Formatter.js' {
declare module.exports: $Exports<'sql-formatter/lib/core/Formatter'>;
}
declare module 'sql-formatter/lib/core/Indentation.js' {
declare module.exports: $Exports<'sql-formatter/lib/core/Indentation'>;
}
declare module 'sql-formatter/lib/core/InlineBlock.js' {
declare module.exports: $Exports<'sql-formatter/lib/core/InlineBlock'>;
}
declare module 'sql-formatter/lib/core/Params.js' {
declare module.exports: $Exports<'sql-formatter/lib/core/Params'>;
}
declare module 'sql-formatter/lib/core/Tokenizer.js' {
declare module.exports: $Exports<'sql-formatter/lib/core/Tokenizer'>;
}
declare module 'sql-formatter/lib/core/tokenTypes.js' {
declare module.exports: $Exports<'sql-formatter/lib/core/tokenTypes'>;
}
declare module 'sql-formatter/lib/languages/Db2Formatter.js' {
declare module.exports: $Exports<'sql-formatter/lib/languages/Db2Formatter'>;
}
declare module 'sql-formatter/lib/languages/N1qlFormatter.js' {
declare module.exports: $Exports<'sql-formatter/lib/languages/N1qlFormatter'>;
}
declare module 'sql-formatter/lib/languages/PlSqlFormatter.js' {
declare module.exports: $Exports<'sql-formatter/lib/languages/PlSqlFormatter'>;
}
declare module 'sql-formatter/lib/languages/StandardSqlFormatter.js' {
declare module.exports: $Exports<'sql-formatter/lib/languages/StandardSqlFormatter'>;
}
declare module 'sql-formatter/lib/sqlFormatter.js' {
declare module.exports: $Exports<'sql-formatter/lib/sqlFormatter'>;
}
declare module 'sql-formatter/src/core/Formatter.js' {
declare module.exports: $Exports<'sql-formatter/src/core/Formatter'>;
}
declare module 'sql-formatter/src/core/Indentation.js' {
declare module.exports: $Exports<'sql-formatter/src/core/Indentation'>;
}
declare module 'sql-formatter/src/core/InlineBlock.js' {
declare module.exports: $Exports<'sql-formatter/src/core/InlineBlock'>;
}
declare module 'sql-formatter/src/core/Params.js' {
declare module.exports: $Exports<'sql-formatter/src/core/Params'>;
}
declare module 'sql-formatter/src/core/Tokenizer.js' {
declare module.exports: $Exports<'sql-formatter/src/core/Tokenizer'>;
}
declare module 'sql-formatter/src/core/tokenTypes.js' {
declare module.exports: $Exports<'sql-formatter/src/core/tokenTypes'>;
}
declare module 'sql-formatter/src/languages/Db2Formatter.js' {
declare module.exports: $Exports<'sql-formatter/src/languages/Db2Formatter'>;
}
declare module 'sql-formatter/src/languages/N1qlFormatter.js' {
declare module.exports: $Exports<'sql-formatter/src/languages/N1qlFormatter'>;
}
declare module 'sql-formatter/src/languages/PlSqlFormatter.js' {
declare module.exports: $Exports<'sql-formatter/src/languages/PlSqlFormatter'>;
}
declare module 'sql-formatter/src/languages/StandardSqlFormatter.js' {
declare module.exports: $Exports<'sql-formatter/src/languages/StandardSqlFormatter'>;
}
declare module 'sql-formatter/src/sqlFormatter.js' {
declare module.exports: $Exports<'sql-formatter/src/sqlFormatter'>;
}

View File

@@ -48,6 +48,28 @@ type GetTableStructureResponse = {
definition: string, definition: string,
}; };
type ExecuteSqlRequest = {
databaseId: number,
value: string,
};
type ExecuteSqlResponse = {
type: string,
columns: Array<string>,
values: Array<Array<Value>>,
insertedId: number,
affectedCount: number,
};
type GetTableInfoRequest = {
databaseId: number,
table: string,
};
type GetTableInfoResponse = {
definition: string,
};
export class DatabaseClient { export class DatabaseClient {
client: PluginClient; client: PluginClient;
@@ -67,4 +89,12 @@ export class DatabaseClient {
GetTableStructureRequest, GetTableStructureRequest,
GetTableStructureResponse, GetTableStructureResponse,
> = params => this.client.call('getTableStructure', params); > = params => this.client.call('getTableStructure', params);
getExecution: ClientCall<ExecuteSqlRequest, ExecuteSqlResponse> = params =>
this.client.call('execute', params);
getTableInfo: ClientCall<
GetTableInfoRequest,
GetTableInfoResponse,
> = params => this.client.call('getTableInfo', params);
} }

View File

@@ -18,6 +18,11 @@ import {
Input, Input,
colors, colors,
getStringFromErrorLike, getStringFromErrorLike,
Spacer,
Textarea,
DetailSidebar,
Panel,
ManagedDataInspector,
} from 'flipper'; } from 'flipper';
import {Component} from 'react'; import {Component} from 'react';
import type { import type {
@@ -29,6 +34,8 @@ import {DatabaseClient} from './ClientProtocol';
import {renderValue} from 'flipper'; import {renderValue} from 'flipper';
import type {Value} from 'flipper'; import type {Value} from 'flipper';
import ButtonNavigation from './ButtonNavigation'; import ButtonNavigation from './ButtonNavigation';
import sqlFormatter from 'sql-formatter';
import dateFormat from 'dateformat';
const PAGE_SIZE = 50; const PAGE_SIZE = 50;
@@ -51,11 +58,17 @@ type DatabasesPluginState = {|
pageRowNumber: number, pageRowNumber: number,
databases: Array<DatabaseEntry>, databases: Array<DatabaseEntry>,
outdatedDatabaseList: boolean, outdatedDatabaseList: boolean,
viewMode: 'data' | 'structure', viewMode: 'data' | 'structure' | 'SQL' | 'tableInfo' | 'queryHistory',
error: ?null, error: ?null,
currentPage: ?Page, currentPage: ?Page,
currentStructure: ?Structure, currentStructure: ?Structure,
currentSort: ?TableRowSortOrder, currentSort: ?TableRowSortOrder,
query: ?Query,
queryResult: ?QueryResult,
favorites: Array<string>,
executionTime: number,
tableInfo: string,
queryHistory: Array<Query>,
|}; |};
type Page = { type Page = {
@@ -77,6 +90,18 @@ type Structure = {|
indexesValues: Array<TableBodyRow>, indexesValues: Array<TableBodyRow>,
|}; |};
type QueryResult = {
table: ?QueriedTable,
id: ?number,
count: ?number,
};
type QueriedTable = {
columns: Array<string>,
rows: Array<TableBodyRow>,
highlightedRows: Array<number>,
};
type Actions = type Actions =
| SelectDatabaseEvent | SelectDatabaseEvent
| SelectDatabaseTableEvent | SelectDatabaseTableEvent
@@ -84,11 +109,18 @@ type Actions =
| UpdateViewModeEvent | UpdateViewModeEvent
| UpdatePageEvent | UpdatePageEvent
| UpdateStructureEvent | UpdateStructureEvent
| DisplaySelectEvent
| DisplayInsertEvent
| DisplayUpdateDeleteEvent
| UpdateTableInfoEvent
| NextPageEvent | NextPageEvent
| PreviousPageEvent | PreviousPageEvent
| ExecuteEvent
| RefreshEvent | RefreshEvent
| UpdateFavoritesEvent
| SortByChangedEvent | SortByChangedEvent
| GoToRowEvent; | GoToRowEvent
| UpdateQueryEvent;
type DatabaseEntry = { type DatabaseEntry = {
id: number, id: number,
@@ -96,6 +128,11 @@ type DatabaseEntry = {
tables: Array<string>, tables: Array<string>,
}; };
type Query = {|
value: string,
time: string,
|};
type UpdateDatabasesEvent = {| type UpdateDatabasesEvent = {|
databases: Array<{name: string, id: number, tables: Array<string>}>, databases: Array<{name: string, id: number, tables: Array<string>}>,
type: 'UpdateDatabases', type: 'UpdateDatabases',
@@ -113,7 +150,7 @@ type SelectDatabaseTableEvent = {|
type UpdateViewModeEvent = {| type UpdateViewModeEvent = {|
type: 'UpdateViewMode', type: 'UpdateViewMode',
viewMode: 'data' | 'structure', viewMode: 'data' | 'structure' | 'SQL' | 'tableInfo' | 'queryHistory',
|}; |};
type UpdatePageEvent = {| type UpdatePageEvent = {|
@@ -137,6 +174,27 @@ type UpdateStructureEvent = {|
indexesValues: Array<Array<any>>, indexesValues: Array<Array<any>>,
|}; |};
type DisplaySelectEvent = {|
type: 'DisplaySelect',
columns: Array<string>,
values: Array<Array<any>>,
|};
type DisplayInsertEvent = {|
type: 'DisplayInsert',
id: number,
|};
type DisplayUpdateDeleteEvent = {|
type: 'DisplayUpdateDelete',
count: number,
|};
type UpdateTableInfoEvent = {|
type: 'UpdateTableInfo',
tableInfo: string,
|};
type NextPageEvent = { type NextPageEvent = {
type: 'NextPage', type: 'NextPage',
}; };
@@ -145,10 +203,18 @@ type PreviousPageEvent = {
type: 'PreviousPage', type: 'PreviousPage',
}; };
type ExecuteEvent = {
type: 'Execute',
};
type RefreshEvent = { type RefreshEvent = {
type: 'Refresh', type: 'Refresh',
}; };
type UpdateFavoritesEvent = {
type: 'UpdateFavorites',
};
type SortByChangedEvent = { type SortByChangedEvent = {
type: 'SortByChanged', type: 'SortByChanged',
sortOrder: TableRowSortOrder, sortOrder: TableRowSortOrder,
@@ -159,6 +225,11 @@ type GoToRowEvent = {
row: number, row: number,
}; };
type UpdateQueryEvent = {
type: 'UpdateQuery',
value: string,
};
function transformRow( function transformRow(
columns: Array<string>, columns: Array<string>,
row: Array<Value>, row: Array<Value>,
@@ -250,6 +321,47 @@ function renderDatabaseIndexes(structure: ?Structure) {
); );
} }
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 = [];
if (history.length > 0) {
for (const query of history) {
const time = query.time;
const value = query.value;
rows.push({
key: query,
columns: {time: {value: time}, query: {value: value}},
});
}
}
return (
<FlexRow grow={true}>
<ManagedTable
style={{paddingLeft: 16}}
floating={false}
columns={columns}
columnSizes={{time: 75}}
zebra={true}
rows={rows}
horizontallyScrollable={true}
/>
</FlexRow>
);
}
type PageInfoProps = { type PageInfoProps = {
currentRow: number, currentRow: number,
count: number, count: number,
@@ -277,7 +389,6 @@ class PageInfo extends Component<
onSubmit(e: SyntheticKeyboardEvent<>) { onSubmit(e: SyntheticKeyboardEvent<>) {
if (e.key === 'Enter') { if (e.key === 'Enter') {
const rowNumber = parseInt(this.state.inputValue, 10); const rowNumber = parseInt(this.state.inputValue, 10);
console.log(rowNumber);
this.props.onChange(rowNumber - 1, this.props.count); this.props.onChange(rowNumber - 1, this.props.count);
this.setState({isOpen: false}); this.setState({isOpen: false});
} }
@@ -331,6 +442,12 @@ export default class DatabasesPlugin extends FlipperPlugin<
currentPage: null, currentPage: null,
currentStructure: null, currentStructure: null,
currentSort: null, currentSort: null,
query: null,
queryResult: null,
favorites: [],
executionTime: 0,
tableInfo: '',
queryHistory: [],
}; };
reducers = [ reducers = [
@@ -414,6 +531,7 @@ export default class DatabasesPlugin extends FlipperPlugin<
return { return {
...state, ...state,
viewMode: event.viewMode, viewMode: event.viewMode,
error: null,
}; };
}, },
], ],
@@ -458,6 +576,73 @@ export default class DatabasesPlugin extends FlipperPlugin<
}; };
}, },
], ],
[
'DisplaySelect',
(
state: DatabasesPluginState,
event: DisplaySelectEvent,
): DatabasesPluginState => {
return {
...state,
queryResult: {
table: {
columns: event.columns,
rows: event.values.map((row: Array<Value>, index: number) =>
transformRow(event.columns, row, index),
),
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', 'NextPage',
( (
@@ -484,6 +669,66 @@ export default class DatabasesPlugin extends FlipperPlugin<
}; };
}, },
], ],
[
'Execute',
(
state: DatabasesPluginState,
results: ExecuteEvent,
): DatabasesPluginState => {
const timeBefore = Date.now();
if (
this.state.query !== null &&
typeof this.state.query !== 'undefined'
) {
this.databaseClient
.getExecution({
databaseId: state.selectedDatabase,
value: this.state.query.value,
})
.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', 'GoToRow',
( (
@@ -519,6 +764,32 @@ export default class DatabasesPlugin extends FlipperPlugin<
}; };
}, },
], ],
[
'UpdateFavorites',
(
state: DatabasesPluginState,
event: UpdateFavoritesEvent,
): DatabasesPluginState => {
let newFavorites = 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);
}
}
return {
...state,
favorites: newFavorites,
};
},
],
[ [
'UpdateViewMode', 'UpdateViewMode',
( (
@@ -528,6 +799,7 @@ export default class DatabasesPlugin extends FlipperPlugin<
return { return {
...state, ...state,
viewMode: event.viewMode, viewMode: event.viewMode,
error: null,
}; };
}, },
], ],
@@ -542,6 +814,18 @@ export default class DatabasesPlugin extends FlipperPlugin<
}; };
}, },
], ],
[
'UpdateQuery',
(state: DatabasesPluginState, event: UpdateQueryEvent) => {
return {
...state,
query: {
value: event.value,
time: dateFormat(new Date(), 'hh:MM:ss'),
},
};
},
],
].reduce((acc, val) => { ].reduce((acc, val) => {
const name = val[0]; const name = val[0];
const f = val[1]; const f = val[1];
@@ -576,7 +860,6 @@ export default class DatabasesPlugin extends FlipperPlugin<
start: newState.pageRowNumber, start: newState.pageRowNumber,
}) })
.then(data => { .then(data => {
console.log(data);
this.dispatchAction({ this.dispatchAction({
type: 'UpdatePage', type: 'UpdatePage',
databaseId: databaseId, databaseId: databaseId,
@@ -604,7 +887,6 @@ export default class DatabasesPlugin extends FlipperPlugin<
table: table, table: table,
}) })
.then(data => { .then(data => {
console.log(data);
this.dispatchAction({ this.dispatchAction({
type: 'UpdateStructure', type: 'UpdateStructure',
databaseId: databaseId, databaseId: databaseId,
@@ -619,6 +901,28 @@ export default class DatabasesPlugin extends FlipperPlugin<
this.setState({error: 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) { if (!previousState.outdatedDatabaseList && newState.outdatedDatabaseList) {
this.databaseClient.getDatabases({}).then(databases => { this.databaseClient.getDatabases({}).then(databases => {
this.dispatchAction({ this.dispatchAction({
@@ -647,10 +951,29 @@ export default class DatabasesPlugin extends FlipperPlugin<
this.dispatchAction({type: 'UpdateViewMode', viewMode: 'structure'}); 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 = () => { onRefreshClicked = () => {
this.setState({error: null});
this.dispatchAction({type: 'Refresh'}); this.dispatchAction({type: 'Refresh'});
}; };
onFavoritesClicked = () => {
this.dispatchAction({
type: 'UpdateFavorites',
});
};
onDatabaseSelected = (selected: string) => { onDatabaseSelected = (selected: string) => {
const dbId = this.state.databases.find(x => x.name === selected)?.id || 0; const dbId = this.state.databases.find(x => x.name === selected)?.id || 0;
this.dispatchAction({ this.dispatchAction({
@@ -674,10 +997,25 @@ export default class DatabasesPlugin extends FlipperPlugin<
this.dispatchAction({type: 'PreviousPage'}); this.dispatchAction({type: 'PreviousPage'});
}; };
onExecuteClicked = () => {
this.dispatchAction({type: 'Execute'});
};
onFavoriteClicked = (selected: any) => {
this.setState({query: selected.target.value});
};
onGoToRow = (row: number, count: number) => { onGoToRow = (row: number, count: number) => {
this.dispatchAction({type: 'GoToRow', row: row}); this.dispatchAction({type: 'GoToRow', row: row});
}; };
onQueryChanged = (selected: any) => {
this.dispatchAction({
type: 'UpdateQuery',
value: selected.target.value,
});
};
renderStructure() { renderStructure() {
return [ return [
renderDatabaseColumns(this.state.currentStructure), renderDatabaseColumns(this.state.currentStructure),
@@ -685,6 +1023,153 @@ export default class DatabasesPlugin extends FlipperPlugin<
]; ];
} }
renderSidebar = (table: QueriedTable) => {
if (
table.highlightedRows === null ||
typeof table.highlightedRows === 'undefined' ||
table.highlightedRows.length !== 1
) {
return null;
}
const id = table.highlightedRows[0];
const cols = {
col: {
value: 'Column',
resizable: true,
},
val: {
value: 'Value',
resizable: true,
},
};
const colSizes = {
col: '35%',
val: 'flex',
};
return (
<DetailSidebar width={500}>
<Panel
padded={true}
heading="Row details"
floating={false}
collapsable={true}
grow={true}>
<ManagedTable
highlightableRows={false}
columnSizes={colSizes}
multiline={true}
columns={cols}
autoHeight={true}
floating={false}
zebra={true}
rows={this.sidebarRows(id, table)}
/>
</Panel>
</DetailSidebar>
);
};
sidebarRows = (id: number, table: QueriedTable) => {
const columns = table.columns;
const row = table.rows[id];
const sidebarArray = [];
for (let i = 0; i < columns.length; i++) {
sidebarArray.push(
this.buildSidebarRow(columns[i], row.columns[columns[i]].value),
);
}
return sidebarArray;
};
buildSidebarRow = (key: string, val: any) => {
let output = '';
try {
output = (
<ManagedDataInspector
data={JSON.parse(val.props.children)}
expandRoot={true}
/>
);
} catch (error) {
output = val;
}
return {
columns: {
col: {value: <Text>{key}</Text>},
val: {
value: output,
},
},
key: key,
};
};
renderQuery(query: ?QueryResult) {
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) => {
acc[val] = {value: val, resizable: true};
return acc;
}, {})}
zebra={true}
rows={rows}
horizontallyScrollable={true}
onRowHighlighted={highlightedRows => {
this.setState({
queryResult: {
table: {
columns: columns,
rows: rows,
highlightedRows: highlightedRows,
},
id: null,
count: null,
},
});
}}
/>
{this.renderSidebar(table)}
</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() { render() {
const tableOptions = const tableOptions =
(this.state.selectedDatabase && (this.state.selectedDatabase &&
@@ -697,7 +1182,45 @@ export default class DatabasesPlugin extends FlipperPlugin<
return ( return (
<FlexColumn style={{flex: 1}}> <FlexColumn style={{flex: 1}}>
<Toolbar position="top" style={{paddingLeft: 8}}> <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> <BoldSpan style={{marginRight: 16}}>Database</BoldSpan>
<Select <Select
options={this.state.databases options={this.state.databases
@@ -719,31 +1242,117 @@ export default class DatabasesPlugin extends FlipperPlugin<
/> />
<div /> <div />
<Button onClick={this.onRefreshClicked}>Refresh</Button> <Button onClick={this.onRefreshClicked}>Refresh</Button>
<Button style={{marginLeft: 'auto', display: 'none'}}>
Execute SQL
</Button>
</Toolbar> </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) => {
obj[item] = item;
return obj;
}, {})}
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%',
}}
onChange={this.onQueryChanged.bind(this)}
placeholder="Type query here.."
value={
this.state.query !== null &&
typeof this.state.query !== 'undefined'
? this.state.query.value
: null
}
/>
}
<Toolbar
position="top"
style={{paddingLeft: 16, paddingTop: 24, paddingBottom: 24}}>
<ButtonGroup>
<Button
icon={'star'}
iconSize={14}
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}>Execute</Button>
</ButtonGroup>
</Toolbar>
</div>
) : null}
<FlexColumn grow={true}> <FlexColumn grow={true}>
{this.state.viewMode === 'data' {this.state.viewMode === 'data'
? renderTable(this.state.currentPage, this) ? renderTable(this.state.currentPage, this)
: this.renderStructure()} : null}
{this.state.viewMode === 'structure' ? this.renderStructure() : null}
{this.state.viewMode === 'SQL'
? this.renderQuery(this.state.queryResult)
: null}
{this.state.viewMode === 'tableInfo' ? (
<Textarea
style={{
width: '98%',
height: '100%',
marginLeft: '1%',
marginTop: '1%',
marginBottom: '1%',
readOnly: true,
}}
value={sqlFormatter.format(this.state.tableInfo)}
/>
) : null}
{this.state.viewMode === 'queryHistory'
? renderQueryHistory(this.state.queryHistory)
: null}
</FlexColumn> </FlexColumn>
<Toolbar position="bottom" style={{paddingLeft: 8}}> <Toolbar position="bottom" style={{paddingLeft: 8}}>
<FlexRow grow={true}> <FlexRow grow={true}>
<ButtonGroup> {this.state.viewMode === 'SQL' && this.state.executionTime !== 0 ? (
<Button <Text> {this.state.executionTime} ms </Text>
icon={'data-table'} ) : null}
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 ? ( {this.state.viewMode === 'data' && this.state.currentPage ? (
<PageInfo <PageInfo
currentRow={this.state.currentPage.start} currentRow={this.state.currentPage.start}

View File

@@ -6,6 +6,8 @@
"main": "index.js", "main": "index.js",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"sql-formatter": "^2.3.3",
"dateformat": "^3.0.3"
}, },
"bugs": { "bugs": {
"email": "oncall+flipper@xmail.facebook.com", "email": "oncall+flipper@xmail.facebook.com",

View File

@@ -2,3 +2,19 @@
# yarn lockfile v1 # yarn lockfile v1
dateformat@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==
lodash@^4.16.0:
version "4.17.14"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.14.tgz#9ce487ae66c96254fe20b599f21b6816028078ba"
integrity sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==
sql-formatter@^2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/sql-formatter/-/sql-formatter-2.3.3.tgz#910ef484fbb988a5e510bea4161157e3b80b2f62"
integrity sha512-m6pqVXwsm9GkCHC/+gdPvNowI7PNoVTT6OZMWKwXJoP2MvfntfhcfyliIf4/QX6t+DirSJ6XDSiSS70YvZ87Lw==
dependencies:
lodash "^4.16.0"