/** * 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, outdatedDatabaseList: boolean, viewMode: 'data' | 'structure', error: ?null, currentPage: ?Page, currentStructure: ?Structure, currentSort: ?TableRowSortOrder, |}; type Page = { databaseId: number, table: string, columns: Array, rows: Array, start: number, count: number, total: number, }; type Structure = {| databaseId: number, table: string, columns: Array, rows: Array, indexesColumns: Array, indexesValues: Array, |}; type Actions = | SelectDatabaseEvent | SelectDatabaseTableEvent | UpdateDatabasesEvent | UpdateViewModeEvent | UpdatePageEvent | UpdateStructureEvent | NextPageEvent | PreviousPageEvent | RefreshEvent | SortByChangedEvent | GoToRowEvent; type DatabaseEntry = { id: number, name: string, tables: Array, }; type UpdateDatabasesEvent = {| databases: Array<{name: string, id: number, tables: Array}>, 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, values: Array>, start: number, count: number, total: number, |}; type UpdateStructureEvent = {| type: 'UpdateStructure', databaseId: number, table: string, columns: Array, rows: Array>, indexesColumns: Array, indexesValues: Array>, |}; 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, row: Array, 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 ( ({ 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 ( ({ 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} /> ); } function renderDatabaseIndexes(structure: ?Structure) { if (!structure) { return null; } return ( ({ 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} /> ); } 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 (
{this.props.count === this.props.totalRows ? `${this.props.count} ` : `${this.props.currentRow + 1}-${this.props.currentRow + this.props.count} `} of {this.props.totalRows} rows
{this.state.isOpen ? ( ) : ( )} ); } } export default class DatabasesPlugin extends FlipperPlugin< DatabasesPluginState, Actions, > { databaseClient: DatabaseClient; state: DatabasesPluginState = { selectedDatabase: 0, selectedDatabaseTable: null, pageRowNumber: 0, databases: [], outdatedDatabaseList: true, viewMode: 'data', error: null, currentPage: null, currentStructure: null, currentSort: null, }; reducers = [ [ 'UpdateDatabases', ( state: DatabasesPluginState, results: UpdateDatabasesEvent, ): DatabasesPluginState => { const updates = results.databases; const databases = updates; const selectedDatabase = state.selectedDatabase || (Object.values(databases)[0] ? // $FlowFixMe Object.values(databases)[0].id : 0); const selectedTable = state.selectedDatabaseTable && databases[selectedDatabase - 1].tables.includes( state.selectedDatabaseTable, ) ? state.selectedDatabaseTable : databases[selectedDatabase - 1].tables[0]; const sameTableSelected = selectedDatabase === state.selectedDatabase && selectedTable === state.selectedDatabaseTable; return { ...state, databases, outdatedDatabaseList: false, selectedDatabase: selectedDatabase, selectedDatabaseTable: selectedTable, pageRowNumber: 0, currentPage: sameTableSelected ? state.currentPage : null, currentStructure: null, currentSort: sameTableSelected ? state.currentSort : null, }; }, ], [ 'UpdateSelectedDatabase', ( state: DatabasesPluginState, event: SelectDatabaseEvent, ): DatabasesPluginState => { return { ...state, selectedDatabase: event.database, selectedDatabaseTable: state.databases[event.database - 1].tables[0] || null, pageRowNumber: 0, currentPage: null, currentStructure: null, currentSort: null, }; }, ], [ 'UpdateSelectedDatabaseTable', ( state: DatabasesPluginState, event: SelectDatabaseTableEvent, ): DatabasesPluginState => { return { ...state, selectedDatabaseTable: event.table, pageRowNumber: 0, currentPage: null, currentStructure: null, currentSort: null, }; }, ], [ 'UpdateViewMode', ( state: DatabasesPluginState, event: UpdateViewModeEvent, ): DatabasesPluginState => { return { ...state, viewMode: event.viewMode, }; }, ], [ 'UpdatePage', ( state: DatabasesPluginState, event: UpdatePageEvent, ): DatabasesPluginState => { return { ...state, currentPage: { rows: event.values.map((row: Array, 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, index: number) => transformRow(event.columns, row, index), ), indexesColumns: event.indexesColumns, indexesValues: event.indexesValues.map( (row: Array, index: number) => transformRow(event.columns, row, index), ), }, }; }, ], [ 'NextPage', ( state: DatabasesPluginState, event: UpdatePageEvent, ): DatabasesPluginState => { return { ...state, pageRowNumber: state.pageRowNumber + PAGE_SIZE, currentPage: null, }; }, ], [ 'PreviousPage', ( state: DatabasesPluginState, event: UpdatePageEvent, ): DatabasesPluginState => { return { ...state, pageRowNumber: Math.max(state.pageRowNumber - PAGE_SIZE, 0), currentPage: null, }; }, ], [ 'GoToRow', ( state: DatabasesPluginState, event: GoToRowEvent, ): DatabasesPluginState => { if (!state.currentPage) { return state; } const destinationRow = event.row < 0 ? 0 : event.row >= state.currentPage.total - PAGE_SIZE ? Math.max(state.currentPage.total - PAGE_SIZE, 0) : event.row; return { ...state, pageRowNumber: destinationRow, currentPage: null, }; }, ], [ 'Refresh', ( state: DatabasesPluginState, event: RefreshEvent, ): DatabasesPluginState => { return { ...state, outdatedDatabaseList: true, currentPage: null, }; }, ], [ 'UpdateViewMode', ( state: DatabasesPluginState, event: UpdateViewModeEvent, ): DatabasesPluginState => { return { ...state, viewMode: event.viewMode, }; }, ], [ 'SortByChanged', (state: DatabasesPluginState, event: SortByChangedEvent) => { return { ...state, currentSort: event.sortOrder, pageRowNumber: 0, currentPage: null, }; }, ], ].reduce((acc, val) => { const name = val[0]; const f = val[1]; acc[name] = (previousState, event) => { const newState = f(previousState, event); this.onStateChanged(previousState, newState); return newState; }; return acc; }, {}); onStateChanged( previousState: DatabasesPluginState, newState: DatabasesPluginState, ) { const databaseId = newState.selectedDatabase; const table = newState.selectedDatabaseTable; if ( newState.viewMode === 'data' && newState.currentPage === null && databaseId && table ) { this.databaseClient .getTableData({ count: PAGE_SIZE, databaseId: newState.selectedDatabase, order: newState.currentSort?.key, reverse: (newState.currentSort?.direction || 'up') === 'down', table: table, start: newState.pageRowNumber, }) .then(data => { console.log(data); this.dispatchAction({ type: 'UpdatePage', databaseId: databaseId, table: table, columns: data.columns, values: data.values, start: data.start, count: data.count, total: data.total, }); }) .catch(e => { this.setState({error: e}); }); } if ( newState.viewMode === 'structure' && newState.currentStructure === null && databaseId && table ) { this.databaseClient .getTableStructure({ databaseId: databaseId, table: table, }) .then(data => { console.log(data); this.dispatchAction({ type: 'UpdateStructure', databaseId: databaseId, table: table, columns: data.structureColumns, rows: data.structureValues, indexesColumns: data.indexesColumns, indexesValues: data.indexesValues, }); }) .catch(e => { this.setState({error: e}); }); } if (!previousState.outdatedDatabaseList && newState.outdatedDatabaseList) { this.databaseClient.getDatabases({}).then(databases => { this.dispatchAction({ type: 'UpdateDatabases', databases, }); }); } } init() { this.databaseClient = new DatabaseClient(this.client); this.databaseClient.getDatabases({}).then(databases => { this.dispatchAction({ type: 'UpdateDatabases', databases, }); }); } onDataClicked = () => { this.dispatchAction({type: 'UpdateViewMode', viewMode: 'data'}); }; onStructureClicked = () => { this.dispatchAction({type: 'UpdateViewMode', viewMode: 'structure'}); }; onRefreshClicked = () => { this.dispatchAction({type: 'Refresh'}); }; onDatabaseSelected = (selected: string) => { const dbId = this.state.databases.find(x => x.name === selected)?.id || 0; this.dispatchAction({ database: dbId, type: 'UpdateSelectedDatabase', }); }; onDatabaseTableSelected = (selected: string) => { this.dispatchAction({ table: selected, type: 'UpdateSelectedDatabaseTable', }); }; onNextPageClicked = () => { this.dispatchAction({type: 'NextPage'}); }; onPreviousPageClicked = () => { this.dispatchAction({type: 'PreviousPage'}); }; onGoToRow = (row: number, count: number) => { this.dispatchAction({type: 'GoToRow', row: row}); }; renderStructure() { return [ renderDatabaseColumns(this.state.currentStructure), renderDatabaseIndexes(this.state.currentStructure), ]; } render() { const tableOptions = (this.state.selectedDatabase && this.state.databases[this.state.selectedDatabase - 1] && this.state.databases[this.state.selectedDatabase - 1].tables.reduce( (options, tableName) => ({...options, [tableName]: tableName}), {}, )) || {}; return ( Database
{this.state.viewMode === 'data' ? renderTable(this.state.currentPage, this) : this.renderStructure()} {this.state.viewMode === 'data' && this.state.currentPage ? ( ) : null} {this.state.viewMode === 'data' && this.state.currentPage ? ( 0} canGoForward={ this.state.currentPage.start + this.state.currentPage.count < this.state.currentPage.total } onBack={this.onPreviousPageClicked} onForward={this.onNextPageClicked} /> ) : null} {this.state.error && JSON.stringify(this.state.error)} ); } }