From c2a07e7638baa4078fcde2b69be5e7a2c9378b65 Mon Sep 17 00:00:00 2001 From: Anton Nikolaev Date: Tue, 27 Apr 2021 09:30:11 -0700 Subject: [PATCH] Databases plugin migrated to Sandy functional API Summary: Converted Databases plugin to Sandy functional API Reviewed By: mweststrate Differential Revision: D27999205 fbshipit-source-id: e9d2c7aa5858b9da3c1672efbb558fb6b1077b6b --- .../public/databases/ClientProtocol.tsx | 38 +- desktop/plugins/public/databases/index.tsx | 1903 ++++++++--------- desktop/plugins/public/databases/package.json | 3 + 3 files changed, 852 insertions(+), 1092 deletions(-) diff --git a/desktop/plugins/public/databases/ClientProtocol.tsx b/desktop/plugins/public/databases/ClientProtocol.tsx index c6cd16a63..6865b46dc 100644 --- a/desktop/plugins/public/databases/ClientProtocol.tsx +++ b/desktop/plugins/public/databases/ClientProtocol.tsx @@ -7,9 +7,7 @@ * @format */ -import {PluginClient, Value} from 'flipper'; - -type ClientCall = (arg: Params) => Promise; +import {Value} from 'flipper'; type DatabaseListRequest = {}; @@ -71,28 +69,14 @@ type GetTableInfoResponse = { definition: string; }; -export class DatabaseClient { - client: PluginClient; +export type Methods = { + databaseList(params: DatabaseListRequest): Promise; + getTableData(params: QueryTableRequest): Promise; + getTableStructure( + params: GetTableStructureRequest, + ): Promise; + execute(params: ExecuteSqlRequest): Promise; + getTableInfo(params: GetTableInfoRequest): Promise; +}; - constructor(pluginClient: PluginClient) { - this.client = pluginClient; - } - - getDatabases: ClientCall = () => - this.client.call('databaseList', {}); - - getTableData: ClientCall = (params) => - this.client.call('getTableData', params); - - getTableStructure: ClientCall< - GetTableStructureRequest, - GetTableStructureResponse - > = (params) => this.client.call('getTableStructure', params); - - getExecution: ClientCall = (params) => - this.client.call('execute', params); - - getTableInfo: ClientCall = ( - params, - ) => this.client.call('getTableInfo', params); -} +export type Events = {}; diff --git a/desktop/plugins/public/databases/index.tsx b/desktop/plugins/public/databases/index.tsx index 2241e5002..8d528b704 100644 --- a/desktop/plugins/public/databases/index.tsx +++ b/desktop/plugins/public/databases/index.tsx @@ -25,15 +25,13 @@ import { 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 React, {KeyboardEvent, ChangeEvent, useState, useCallback} from 'react'; +import {Methods, Events} from './ClientProtocol'; import ButtonNavigation from './ButtonNavigation'; import DatabaseDetailSidebar from './DatabaseDetailSidebar'; import DatabaseStructure from './DatabaseStructure'; @@ -44,6 +42,7 @@ import { } from './UpdateQueryUtil'; import sqlFormatter from 'sql-formatter'; import dateFormat from 'dateformat'; +import {createState, PluginClient, usePlugin, useValue} from 'flipper-plugin'; const PAGE_SIZE = 50; @@ -121,26 +120,6 @@ export type QueriedTable = { highlightedRows: Array; }; -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; @@ -152,98 +131,6 @@ type Query = { time: string; }; -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' | 'SQL' | 'tableInfo' | 'queryHistory'; -}; - -type UpdatePageEvent = { - type: 'UpdatePage'; -} & Page; - -type UpdateStructureEvent = { - type: 'UpdateStructure'; - databaseId: number; - table: string; - columns: Array; - rows: Array>; - indexesColumns: Array; - indexesValues: Array>; -}; - -type DisplaySelectEvent = { - type: 'DisplaySelect'; - columns: Array; - values: Array>; -}; - -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 | undefined; -}; - -type SortByChangedEvent = { - type: 'SortByChanged'; - sortOrder: TableRowSortOrder; -}; - -type GoToRowEvent = { - type: 'GoToRow'; - row: number; -}; - -type UpdateQueryEvent = { - type: 'UpdateQuery'; - value: string; -}; - function transformRow( columns: Array, row: Array, @@ -303,71 +190,56 @@ type PageInfoProps = { 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)}; - } +function PageInfo(props: PageInfoProps) { + const [state, setState] = useState({ + isOpen: false, + inputValue: String(props.currentRow), + }); - onOpen() { - this.setState({isOpen: true}); - } + const onOpen = () => { + setState({...state, isOpen: true}); + }; - onInputChanged(e: ChangeEvent) { - this.setState({inputValue: e.target.value}); - } + const onInputChanged = (e: ChangeEvent) => { + setState({...state, inputValue: e.target.value}); + }; - onSubmit(e: KeyboardEvent) { + const 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}); + const rowNumber = parseInt(state.inputValue, 10); + props.onChange(rowNumber - 1, props.count); + setState({...state, 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 ? ( - - ) : ( - - )} - - ); - } + return ( + +
+ + {props.count === props.totalRows + ? `${props.count} ` + : `${props.currentRow + 1}-${props.currentRow + props.count} `} + of {props.totalRows} rows + +
+ {state.isOpen ? ( + + ) : ( + + )} + + ); } -export default class DatabasesPlugin extends FlipperPlugin< - DatabasesPluginState, - Actions, - {} -> { - databaseClient: DatabaseClient; - - state: DatabasesPluginState = { +export function plugin(client: PluginClient) { + const pluginState = createState({ selectedDatabase: 0, selectedDatabaseTable: null, pageRowNumber: 0, @@ -384,704 +256,616 @@ export default class DatabasesPlugin extends FlipperPlugin< executionTime: 0, tableInfo: '', queryHistory: [], + }); + + const updateDatabases = (event: { + databases: Array<{name: string; id: number; tables: Array}>; + }) => { + const updates = event.databases; + const state = pluginState.get(); + const databases = updates.sort((db1, db2) => db1.id - db2.id); + const selectedDatabase = + state.selectedDatabase || + (Object.values(databases)[0] ? 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; + pluginState.set({ + ...state, + databases, + outdatedDatabaseList: false, + selectedDatabase: selectedDatabase, + selectedDatabaseTable: selectedTable, + pageRowNumber: 0, + currentPage: sameTableSelected ? state.currentPage : null, + currentStructure: null, + currentSort: sameTableSelected ? state.currentSort : null, + }); }; - 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] ? 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, - }, - }; - }, - ], + const updateSelectedDatabase = (event: {database: number}) => { + const state = pluginState.get(); + pluginState.set({ + ...state, + selectedDatabase: event.database, + selectedDatabaseTable: + state.databases[event.database - 1].tables[0] || null, + pageRowNumber: 0, + currentPage: null, + currentStructure: null, + currentSort: null, + }); + }; - [ - 'DisplayUpdateDelete', - ( - state: DatabasesPluginState, - event: DisplayUpdateDeleteEvent, - ): DatabasesPluginState => { - return { - ...state, - queryResult: { - table: null, - id: null, - count: event.count, - }, - }; + const updateSelectedDatabaseTable = (event: {table: string}) => { + const state = pluginState.get(); + pluginState.set({ + ...state, + selectedDatabaseTable: event.table, + pageRowNumber: 0, + currentPage: null, + currentStructure: null, + currentSort: null, + }); + }; + + const updateViewMode = (event: { + viewMode: 'data' | 'structure' | 'SQL' | 'tableInfo' | 'queryHistory'; + }) => { + pluginState.update((state) => { + state.viewMode = event.viewMode; + state.error = null; + }); + }; + + const updatePage = (event: Page) => { + pluginState.update((state) => { + state.currentPage = event; + }); + }; + + const updateStructure = (event: { + databaseId: number; + table: string; + columns: Array; + rows: Array>; + indexesColumns: Array; + indexesValues: Array>; + }) => { + pluginState.update((state) => { + state.currentStructure = { + databaseId: event.databaseId, + table: event.table, + columns: event.columns, + rows: event.rows, + indexesColumns: event.indexesColumns, + indexesValues: event.indexesValues, + }; + }); + }; + + const displaySelect = (event: { + columns: Array; + values: Array>; + }) => { + pluginState.update((state) => { + state.queryResult = { + table: { + columns: event.columns, + rows: event.values, + highlightedRows: [], + }, + id: null, + count: null, + }; + }); + }; + + const displayInsert = (event: {id: number}) => { + const state = pluginState.get(); + pluginState.set({ + ...state, + queryResult: { + table: null, + id: event.id, + count: null, }, - ], - [ - 'UpdateTableInfo', - ( - state: DatabasesPluginState, - event: UpdateTableInfoEvent, - ): DatabasesPluginState => { - return { - ...state, - tableInfo: event.tableInfo, - }; + }); + }; + + const displayUpdateDelete = (event: {count: number}) => { + pluginState.update((state) => { + state.queryResult = { + table: null, + id: null, + count: event.count, + }; + }); + }; + + const updateTableInfo = (event: {tableInfo: string}) => { + pluginState.update((state) => { + state.tableInfo = event.tableInfo; + }); + }; + + const nextPage = () => { + pluginState.update((state) => { + state.pageRowNumber += PAGE_SIZE; + state.currentPage = null; + }); + }; + + const previousPage = () => { + pluginState.update((state) => { + state.pageRowNumber = Math.max(state.pageRowNumber - PAGE_SIZE, 0); + state.currentPage = null; + }); + }; + + const execute = (event: {query: string}) => { + const timeBefore = Date.now(); + const {query} = event; + client + .send('execute', { + databaseId: pluginState.get().selectedDatabase, + value: query, + }) + .then((data) => { + pluginState.update((state) => { + state.error = null; + state.executionTime = Date.now() - timeBefore; + }); + if (data.type === 'select') { + displaySelect({ + columns: data.columns, + values: data.values, + }); + } else if (data.type === 'insert') { + displayInsert({ + id: data.insertedId, + }); + } else if (data.type === 'update_delete') { + displayUpdateDelete({ + count: data.affectedCount, + }); + } + }) + .catch((e) => { + pluginState.update((state) => { + state.error = e; + }); + }); + let newHistory = pluginState.get().queryHistory; + const newQuery = pluginState.get().query; + if ( + newQuery !== null && + typeof newQuery !== 'undefined' && + newHistory !== null && + typeof newHistory !== 'undefined' + ) { + newQuery.time = dateFormat(new Date(), 'hh:MM:ss'); + newHistory = newHistory.concat(newQuery); + } + pluginState.update((state) => { + state.queryHistory = newHistory; + }); + }; + + const goToRow = (event: {row: number}) => { + const state = pluginState.get(); + if (!state.currentPage) { + return; + } + const destinationRow = + event.row < 0 + ? 0 + : event.row >= state.currentPage.total - PAGE_SIZE + ? Math.max(state.currentPage.total - PAGE_SIZE, 0) + : event.row; + pluginState.update((state) => { + state.pageRowNumber = destinationRow; + state.currentPage = null; + }); + }; + + const refresh = () => { + pluginState.update((state) => { + state.outdatedDatabaseList = true; + state.currentPage = null; + }); + }; + + const updateFavorites = (event: {favorites: Array | undefined}) => { + const state = pluginState.get(); + 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, + }; + }; + + const sortByChanged = (event: {sortOrder: TableRowSortOrder}) => { + const state = pluginState.get(); + pluginState.set({ + ...state, + currentSort: event.sortOrder, + pageRowNumber: 0, + currentPage: null, + }); + }; + + const updateQuery = (event: {value: string}) => { + const state = pluginState.get(); + pluginState.set({ + ...state, + query: { + value: event.value, + time: dateFormat(new Date(), 'hh:MM:ss'), }, - ], - [ - '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}) + }); + }; + + pluginState.subscribe( + (newState: DatabasesPluginState, previousState: DatabasesPluginState) => { + const databaseId = newState.selectedDatabase; + const table = newState.selectedDatabaseTable; + if ( + newState.viewMode === 'data' && + newState.currentPage === null && + databaseId && + table + ) { + client + .send('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.setState({ - error: null, - executionTime: Date.now() - timeBefore, + updatePage({ + databaseId: databaseId, + table: table, + columns: data.columns, + rows: data.values, + start: data.start, + count: data.count, + total: data.total, + highlightedRows: [], }); - 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}); + pluginState.update((state) => { + state.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]; + } + if (newState.currentStructure === null && databaseId && table) { + client + .send('getTableStructure', { + databaseId: databaseId, + table: table, + }) + .then((data) => { + updateStructure({ + databaseId: databaseId, + table: table, + columns: data.structureColumns, + rows: data.structureValues, + indexesColumns: data.indexesColumns, + indexesValues: data.indexesValues, + }); + }) + .catch((e) => { + pluginState.update((state) => { + state.error = e; + }); + }); + } + if ( + newState.viewMode === 'tableInfo' && + newState.currentStructure === null && + databaseId && + table + ) { + client + .send('getTableInfo', { + databaseId: databaseId, + table: table, + }) + .then((data) => { + updateTableInfo({ + tableInfo: data.definition, + }); + }) + .catch((e) => { + pluginState.update((state) => { + state.error = e; + }); + }); + } - acc[name] = (previousState, event) => { - const newState = f(previousState, event); - this.onStateChanged(previousState, newState); - return newState; - }; - return acc; + if ( + !previousState.outdatedDatabaseList && + newState.outdatedDatabaseList + ) { + client.send('databaseList', {}).then((databases) => { + updateDatabases({ + databases, + }); + }); + } }, - {}, ); - 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, - }); + client.onConnect(() => { + client.send('databaseList', {}).then((databases) => { + updateDatabases({ + databases, }); - } - } + }); + updateFavorites({ + favorites: JSON.parse( + localStorage.getItem('plugin-database-favorites-sql-queries') || '[]', + ), + }); + }); - // to keep eslint happy - constructor(props: FlipperPluginProps<{}>) { - super(props); - this.databaseClient = new DatabaseClient(this.client); - } + return { + state: pluginState, + updateDatabases, + updateSelectedDatabase, + updateSelectedDatabaseTable, + updateViewMode, + updatePage, + updateStructure, + displaySelect, + displayInsert, + displayUpdateDelete, + updateTableInfo, + nextPage, + previousPage, + execute, + goToRow, + refresh, + updateFavorites, + sortByChanged, + updateQuery, + }; +} - init() { - if (this.client.isConnected) { - this.databaseClient = new DatabaseClient(this.client); - this.databaseClient.getDatabases({}).then((databases) => { - this.dispatchAction({ - type: 'UpdateDatabases', - databases, - }); +export function Component() { + const instance = usePlugin(plugin); + const state = useValue(instance.state); + + const onDataClicked = useCallback(() => { + instance.updateViewMode({viewMode: 'data'}); + }, [instance]); + + const onStructureClicked = useCallback(() => { + instance.updateViewMode({viewMode: 'structure'}); + }, [instance]); + + const onSQLClicked = useCallback(() => { + instance.updateViewMode({viewMode: 'SQL'}); + }, [instance]); + + const onTableInfoClicked = useCallback(() => { + instance.updateViewMode({viewMode: 'tableInfo'}); + }, [instance]); + + const onQueryHistoryClicked = useCallback(() => { + instance.updateViewMode({viewMode: 'queryHistory'}); + }, [instance]); + + const onRefreshClicked = useCallback(() => { + instance.state.update((state) => { + state.error = null; + }); + instance.refresh(); + }, [instance]); + + const onFavoritesClicked = useCallback(() => { + instance.updateFavorites({ + favorites: instance.state.get().favorites, + }); + }, [instance]); + + const onDatabaseSelected = useCallback( + (selected: string) => { + const dbId = + instance.state.get().databases.find((x) => x.name === selected)?.id || + 0; + instance.updateSelectedDatabase({ + database: dbId, }); - this.dispatchAction({ - type: 'UpdateFavorites', - favorites: JSON.parse( - localStorage.getItem('plugin-database-favorites-sql-queries') || '[]', + }, + [instance], + ); + + const onDatabaseTableSelected = useCallback( + (selected: string) => { + instance.updateSelectedDatabaseTable({ + table: selected, + }); + }, + [instance], + ); + + const onNextPageClicked = useCallback(() => { + instance.nextPage(); + }, [instance]); + + const onPreviousPageClicked = useCallback(() => { + instance.previousPage(); + }, [instance]); + + const onExecuteClicked = useCallback(() => { + const query = instance.state.get().query; + if (query) { + instance.execute({query: query.value}); + } + }, [instance]); + + const onQueryTextareaKeyPress = useCallback( + (event: KeyboardEvent) => { + // Implement ctrl+enter as a shortcut for clicking 'Execute'. + if (event.key === '\n' && event.ctrlKey) { + event.preventDefault(); + event.stopPropagation(); + onExecuteClicked(); + } + }, + [onExecuteClicked], + ); + + const onGoToRow = useCallback( + (row: number, _count: number) => { + instance.goToRow({row: row}); + }, + [instance], + ); + + const onQueryChanged = useCallback( + (selected: any) => { + instance.updateQuery({ + value: selected.target.value, + }); + }, + [instance], + ); + + const onRowEdited = useCallback( + (change: {[key: string]: string | null}) => { + const { + selectedDatabaseTable, + currentStructure, + viewMode, + currentPage, + } = instance.state.get(); + const highlightedRowIdx = currentPage?.highlightedRows[0] ?? -1; + const row = + highlightedRowIdx >= 0 + ? currentPage?.rows[currentPage?.highlightedRows[0]] + : undefined; + const columns = currentPage?.columns; + // currently only allow to edit data shown in Data tab + if ( + viewMode !== 'data' || + selectedDatabaseTable === null || + currentStructure === null || + currentPage === null || + row === undefined || + columns === undefined || + // only trigger when there is change + Object.keys(change).length <= 0 + ) { + return; + } + // check if the table has primary key to use for query + // This is assumed data are in the same format as in SqliteDatabaseDriver.java + const primaryKeyIdx = currentStructure.columns.indexOf('primary_key'); + const nameKeyIdx = currentStructure.columns.indexOf('column_name'); + const typeIdx = currentStructure.columns.indexOf('data_type'); + const nullableIdx = currentStructure.columns.indexOf('nullable'); + if (primaryKeyIdx < 0 && nameKeyIdx < 0 && typeIdx < 0) { + console.error( + 'primary_key, column_name, and/or data_type cannot be empty', + ); + return; + } + const primaryColumnIndexes = currentStructure.rows + .reduce((acc, row) => { + const primary = row[primaryKeyIdx]; + if (primary.type === 'boolean' && primary.value) { + const name = row[nameKeyIdx]; + return name.type === 'string' ? acc.concat(name.value) : acc; + } else { + return acc; + } + }, [] as Array) + .map((name) => columns.indexOf(name)) + .filter((idx) => idx >= 0); + // stop if no primary key to distinguish unique query + if (primaryColumnIndexes.length <= 0) { + return; + } + + const types = currentStructure.rows.reduce((acc, row) => { + const nameValue = row[nameKeyIdx]; + const name = nameValue.type === 'string' ? nameValue.value : null; + const typeValue = row[typeIdx]; + const type = typeValue.type === 'string' ? typeValue.value : null; + const nullableValue = + nullableIdx < 0 ? {type: 'null', value: null} : row[nullableIdx]; + const nullable = nullableValue.value !== false; + if (name !== null && type !== null) { + acc[name] = {type, nullable}; + } + return acc; + }, {} as {[key: string]: {type: string; nullable: boolean}}); + + const changeValue = Object.entries(change).reduce( + (acc, [key, value]: [string, string | null]) => { + acc[key] = convertStringToValue(types, key, value); + return acc; + }, + {} as {[key: string]: Value}, + ); + instance.execute({ + query: constructUpdateQuery( + selectedDatabaseTable, + primaryColumnIndexes.reduce((acc, idx) => { + acc[columns[idx]] = row[idx]; + return acc; + }, {} as {[key: string]: Value}), + changeValue, ), }); - } - } + instance.updatePage({ + ...produce(currentPage, (draft) => + Object.entries(changeValue).forEach( + ([key, value]: [string, Value]) => { + const columnIdx = draft.columns.indexOf(key); + if (columnIdx >= 0) { + draft.rows[highlightedRowIdx][columnIdx] = value; + } + }, + ), + ), + }); + }, + [instance], + ); - 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, - }); - }; - - onRowEdited(change: {[key: string]: string | null}) { - const { - selectedDatabaseTable, - currentStructure, - viewMode, - currentPage, - } = this.state; - const highlightedRowIdx = currentPage?.highlightedRows[0] ?? -1; - const row = - highlightedRowIdx >= 0 - ? currentPage?.rows[currentPage?.highlightedRows[0]] - : undefined; - const columns = currentPage?.columns; - // currently only allow to edit data shown in Data tab - if ( - viewMode !== 'data' || - selectedDatabaseTable === null || - currentStructure === null || - currentPage === null || - row === undefined || - columns === undefined || - // only trigger when there is change - Object.keys(change).length <= 0 - ) { - return; - } - // check if the table has primary key to use for query - // This is assumed data are in the same format as in SqliteDatabaseDriver.java - const primaryKeyIdx = currentStructure.columns.indexOf('primary_key'); - const nameKeyIdx = currentStructure.columns.indexOf('column_name'); - const typeIdx = currentStructure.columns.indexOf('data_type'); - const nullableIdx = currentStructure.columns.indexOf('nullable'); - if (primaryKeyIdx < 0 && nameKeyIdx < 0 && typeIdx < 0) { - console.error( - 'primary_key, column_name, and/or data_type cannot be empty', - ); - return; - } - const primaryColumnIndexes = currentStructure.rows - .reduce((acc, row) => { - const primary = row[primaryKeyIdx]; - if (primary.type === 'boolean' && primary.value) { - const name = row[nameKeyIdx]; - return name.type === 'string' ? acc.concat(name.value) : acc; - } else { - return acc; - } - }, [] as Array) - .map((name) => columns.indexOf(name)) - .filter((idx) => idx >= 0); - // stop if no primary key to distinguish unique query - if (primaryColumnIndexes.length <= 0) { - return; - } - - const types = currentStructure.rows.reduce((acc, row) => { - const nameValue = row[nameKeyIdx]; - const name = nameValue.type === 'string' ? nameValue.value : null; - const typeValue = row[typeIdx]; - const type = typeValue.type === 'string' ? typeValue.value : null; - const nullableValue = - nullableIdx < 0 ? {type: 'null', value: null} : row[nullableIdx]; - const nullable = nullableValue.value !== false; - if (name !== null && type !== null) { - acc[name] = {type, nullable}; - } - return acc; - }, {} as {[key: string]: {type: string; nullable: boolean}}); - - const changeValue = Object.entries(change).reduce( - (acc, [key, value]: [string, string | null]) => { - acc[key] = convertStringToValue(types, key, value); - return acc; - }, - {} as {[key: string]: Value}, - ); - this.dispatchAction({ - type: 'Execute', - query: constructUpdateQuery( - selectedDatabaseTable, - primaryColumnIndexes.reduce((acc, idx) => { - acc[columns[idx]] = row[idx]; - return acc; - }, {} as {[key: string]: Value}), - changeValue, - ), - }); - this.dispatchAction({ - type: 'UpdatePage', - ...produce(currentPage, (draft) => - Object.entries(changeValue).forEach(([key, value]: [string, Value]) => { - const columnIdx = draft.columns.indexOf(key); - if (columnIdx >= 0) { - draft.rows[highlightedRowIdx][columnIdx] = value; - } - }), - ), - }); - } - - renderTable(page: Page | null) { + const renderTable = (page: Page | null) => { if (!page) { return null; } @@ -1108,44 +892,41 @@ export default class DatabasesPlugin extends FlipperPlugin< horizontallyScrollable={true} multiHighlight={true} onRowHighlighted={(highlightedRows) => - this.setState( - produce((draftState: DatabasesPluginState) => { - if (draftState.currentPage !== null) { - draftState.currentPage.highlightedRows = highlightedRows.map( - parseInt, - ); - } - }), - ) + instance.state.update((draftState: DatabasesPluginState) => { + if (draftState.currentPage !== null) { + draftState.currentPage.highlightedRows = highlightedRows.map( + parseInt, + ); + } + }) } onSort={(sortOrder: TableRowSortOrder) => { - this.dispatchAction({ - type: 'SortByChanged', + instance.sortByChanged({ sortOrder, }); }} - initialSortOrder={this.state.currentSort ?? undefined} + initialSortOrder={state.currentSort ?? undefined} /> {page.highlightedRows.length === 1 && ( )} ); - } + }; - renderQuery(query: QueryResult | null) { + const renderQuery = (query: QueryResult | null) => { if (!query || query === null) { return null; } @@ -1177,7 +958,8 @@ export default class DatabasesPlugin extends FlipperPlugin< )} horizontallyScrollable={true} onRowHighlighted={(highlightedRows) => { - this.setState({ + instance.state.set({ + ...instance.state.get(), queryResult: { table: { columns: columns, @@ -1217,220 +999,211 @@ export default class DatabasesPlugin extends FlipperPlugin< } 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}), - {}, - )) || - {}; + const tableOptions = + (state.selectedDatabase && + state.databases[state.selectedDatabase - 1] && + state.databases[state.selectedDatabase - 1].tables.reduce( + (options, tableName) => ({...options, [tableName]: tableName}), + {}, + )) || + {}; - return ( - + return ( + + + + + + + + + + + {state.viewMode === 'data' || + state.viewMode === 'structure' || + state.viewMode === 'tableInfo' ? ( - - - - - - - + Database + +
+ - {this.state.viewMode === 'data' || - this.state.viewMode === 'structure' || - this.state.viewMode === 'tableInfo' ? ( + ) : null} + {state.viewMode === 'SQL' ? ( +
Database -
- - ) : null} - {this.state.viewMode === 'SQL' ? ( -
- - Database -