desktop/plugins/public/databases/index.tsx (485 lines of code) (raw):

/** * Copyright (c) Meta Platforms, Inc. and 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 {TableRowSortOrder, TableHighlightedRows} from 'flipper'; import {Value} from './TypeBasedValueRenderer'; import {Methods, Events} from './ClientProtocol'; import dateFormat from 'dateformat'; import {createState, PluginClient} from 'flipper-plugin'; export {Component} from './DatabasesPlugin'; const PAGE_SIZE = 50; const FAVORITES_LOCAL_STORAGE_KEY = 'plugin-database-favorites-sql-queries'; 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; executionTime: number; tableInfo: string; queryHistory: Array<Query>; }; export 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>>; }; export type QueryResult = { table: QueriedTable | null; id: number | null; count: number | null; }; export type QueriedTable = { columns: Array<string>; rows: Array<Array<Value>>; highlightedRows: Array<number>; }; export type DatabaseEntry = { id: number; name: string; tables: Array<string>; }; export type Query = { value: string; time: string; }; export function plugin(client: PluginClient<Events, Methods>) { const pluginState = createState<DatabasesPluginState>({ selectedDatabase: 0, selectedDatabaseTable: null, pageRowNumber: 0, databases: [], outdatedDatabaseList: true, viewMode: 'data', error: null, currentPage: null, currentStructure: null, currentSort: null, query: null, queryResult: null, executionTime: 0, tableInfo: '', queryHistory: [], }); const favoritesState = createState<string[]>([], {persist: 'favorites'}); favoritesState.subscribe((favorites) => { localStorage.setItem( FAVORITES_LOCAL_STORAGE_KEY, JSON.stringify(favorites), ); }); const updateDatabases = (event: { databases: Array<{name: string; id: number; tables: Array<string>}>; }) => { 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, }); }; 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, }); }; 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<string>; rows: Array<Array<Value>>; indexesColumns: Array<string>; indexesValues: Array<Array<Value>>; }) => { 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<string>; values: Array<Array<Value>>; }) => { 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, }, }); }; 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 addOrRemoveQueryToFavorites = (query: string) => { favoritesState.update((favorites) => { const index = favorites.indexOf(query); if (index < 0) { favorites.push(query); } else { favorites.splice(index, 1); } }); }; 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'), }, }); }; const pageHighlightedRowsChanged = (event: TableHighlightedRows) => { pluginState.update((draftState: DatabasesPluginState) => { if (draftState.currentPage !== null) { draftState.currentPage.highlightedRows = event.map(parseInt); } }); }; const queryHighlightedRowsChanged = (event: TableHighlightedRows) => { pluginState.update((state) => { if (state.queryResult) { if (state.queryResult.table) { state.queryResult.table.highlightedRows = event.map(parseInt); } state.queryResult.id = null; state.queryResult.count = null; } }); }; 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) => { updatePage({ databaseId: databaseId, table: table, columns: data.columns, rows: data.values, start: data.start, count: data.count, total: data.total, highlightedRows: [], }); }) .catch((e) => { pluginState.update((state) => { state.error = e; }); }); } 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; }); }); } if ( !previousState.outdatedDatabaseList && newState.outdatedDatabaseList ) { client .send('databaseList', {}) .then((databases) => { updateDatabases({ databases, }); }) .catch((e) => console.error('databaseList request failed:', e)); } }, ); client.onConnect(() => { client .send('databaseList', {}) .then((databases) => { updateDatabases({ databases, }); }) .catch((e) => console.error('initial databaseList request failed:', e)); const loadedFavoritesJson = localStorage.getItem( FAVORITES_LOCAL_STORAGE_KEY, ); if (loadedFavoritesJson) { try { favoritesState.set(JSON.parse(loadedFavoritesJson)); } catch (err) { console.error('Failed to load favorite queries from local storage'); } } }); return { state: pluginState, favoritesState, updateDatabases, updateSelectedDatabase, updateSelectedDatabaseTable, updateViewMode, updatePage, updateStructure, displaySelect, displayInsert, displayUpdateDelete, updateTableInfo, nextPage, previousPage, execute, goToRow, refresh, addOrRemoveQueryToFavorites, sortByChanged, updateQuery, pageHighlightedRowsChanged, queryHighlightedRowsChanged, }; }