diff --git a/app/package.json b/app/package.json index 5f29eaaa..b031b339 100644 --- a/app/package.json +++ b/app/package.json @@ -20,6 +20,6 @@ "babel-core": "^5.8.29", "babel-runtime": "^5.8.29", "debug": "^2.2.0", - "sqlectron-core": "^3.4.0" + "sqlectron-core": "^3.6.0" } } diff --git a/src/renderer/actions/sqlscripts.js b/src/renderer/actions/sqlscripts.js new file mode 100644 index 00000000..fefc93f6 --- /dev/null +++ b/src/renderer/actions/sqlscripts.js @@ -0,0 +1,72 @@ +import { getDBConnByName } from './connections'; +import { updateQueryIfNeeded } from './queries'; + + +export const GET_SCRIPT_REQUEST = 'GET_SCRIPT_REQUEST'; +export const GET_SCRIPT_SUCCESS = 'GET_SCRIPT_SUCCESS'; +export const GET_SCRIPT_FAILURE = 'GET_SCRIPT_FAILURE'; + + +export function getSQLScriptIfNeeded(database, item, actionType, objectType) { + return (dispatch, getState) => { + const state = getState(); + if (shouldFetchScript(state, database, item, actionType)) { + return dispatch(getSQLScript(database, item, actionType, objectType)); + } else if (isScriptAlreadyFetched(state, database, item, actionType)) { + const script = getAlreadyFetchedScript(state, database, item, actionType); + return dispatch(updateQueryIfNeeded(script)); + } + }; +} + +function shouldFetchScript (state, database, item, actionType) { + const scripts = state.sqlscripts; + if (!scripts) return true; + if (scripts.isFetching) return false; + if (!scripts.scriptsByObject[database]) return true; + if (!scripts.scriptsByObject[database][item]) return true; + if (!scripts.scriptsByObject[database][item][actionType]) return true; + return scripts.didInvalidate; +} + +function isScriptAlreadyFetched (state, database, item, actionType) { + const scripts = state.sqlscripts; + if (!scripts.scriptsByObject[database]) return false; + if (!scripts.scriptsByObject[database][item]) return false; + if (scripts.scriptsByObject[database][item][actionType]) return true; + return false; +} + +function getAlreadyFetchedScript (state, database, item, actionType) { + return state.sqlscripts.scriptsByObject[database][item][actionType]; +} + + +function getSQLScript (database, item, actionType, objectType) { + return async (dispatch) => { + dispatch({ type: GET_SCRIPT_REQUEST, database, item, actionType, objectType }); + try { + const dbConn = getDBConnByName(database); + let script; + if (actionType === 'CREATE') { + [script] = objectType === 'Table' + ? await dbConn.getTableCreateScript(item) + : (objectType === 'View') + ? await dbConn.getViewCreateScript(item) + : await dbConn.getRoutineCreateScript(item, objectType); + } else if (actionType === 'SELECT') { + script = await dbConn.getTableSelectScript(item); + } else if (actionType === 'INSERT') { + script = await dbConn.getTableInsertScript(item); + } else if (actionType === 'UPDATE') { + script = await dbConn.getTableUpdateScript(item); + } else if (actionType === 'DELETE') { + script = await dbConn.getTableDeleteScript(item); + } + dispatch({ type: GET_SCRIPT_SUCCESS, database, item, script, actionType, objectType }); + dispatch(updateQueryIfNeeded(script)); + } catch (error) { + dispatch({ type: GET_SCRIPT_FAILURE, error }); + } + }; +} diff --git a/src/renderer/components/database-item.jsx b/src/renderer/components/database-item.jsx new file mode 100644 index 00000000..041bf94b --- /dev/null +++ b/src/renderer/components/database-item.jsx @@ -0,0 +1,135 @@ +import React, { Component, PropTypes } from 'react'; +import TableSubmenu from './table-submenu.jsx'; +import { Menu, MenuItem } from 'remote'; + + +const STYLE = { + item: { wordBreak: 'break-all', cursor: 'default' }, +}; + + +export default class DatabaseItem extends Component { + static propTypes = { + database: PropTypes.object.isRequired, + item: PropTypes.object.isRequired, + dbObjectType: PropTypes.string.isRequired, + style: PropTypes.object, + columnsByTable: PropTypes.object, + triggersByTable: PropTypes.object, + onSelectItem: PropTypes.func, + onExecuteDefaultQuery: PropTypes.func, + onGetSQLScript: PropTypes.func, + } + + constructor(props, context) { + super(props, context); + this.state = {}; + this.contextMenu; + } + + // Context menu is built dinamically on click (if it does not exist), because building + // menu onComponentDidMount or onComponentWillMount slows table listing when database + // has a loads of tables, because menu will be created (unnecessarily) for every table shown + onContextMenu(e){ + e.preventDefault(); + if (!this.contextMenu){ + this.buildContextMenu(); + } + this.contextMenu.popup(e.clientX, e.clientY); + } + + buildContextMenu(){ + const { + database, + item, + dbObjectType, + onExecuteDefaultQuery, + onGetSQLScript, + } = this.props; + const actionTypes = ['SELECT', 'INSERT', 'UPDATE', 'DELETE']; + + this.contextMenu = new Menu(); + if (dbObjectType === 'Table' || dbObjectType === 'View') { + this.contextMenu.append(new MenuItem({ + label: 'Execute default query', + click: onExecuteDefaultQuery.bind(this, database, item) + })); + } + this.contextMenu.append(new MenuItem({ + label: 'CREATE script', + click: onGetSQLScript.bind(this, database, item, 'CREATE', dbObjectType) + })); + if (dbObjectType === 'Table') { + actionTypes.map((actionType, index) => { + this.contextMenu.append(new MenuItem({ + label: `${actionType} script`, + click: onGetSQLScript.bind(this, database, item, actionType, dbObjectType) + })); + }); + } + } + + toggleTableCollapse() { + this.setState({ tableCollapsed: !this.state.tableCollapsed }); + } + + renderSubItems(table) { + const { columnsByTable, triggersByTable, database } = this.props; + + if (!columnsByTable || !columnsByTable[table]) { + return null; + } + + const displayStyle = {}; + if (!this.state.tableCollapsed) { + displayStyle.display = 'none'; + } + + return ( +
+ + +
+ ); + } + + render() { + const { database, item, style, onSelectItem, dbObjectType } = this.props; + const hasChildElements = !!onSelectItem; + const onSingleClick = hasChildElements + ? () => {onSelectItem(database, item); this.toggleTableCollapse();} + : () => {}; + + const collapseCssClass = this.state.tableCollapsed ? 'down' : 'right'; + const collapseIcon = ( + + ); + const tableIcon = ( + + ); + + return ( +
+ + { dbObjectType === 'Table' ? collapseIcon : null } + { dbObjectType === 'Table' ? tableIcon : null } + {item.name} + + {this.renderSubItems(item.name)} +
+ ); + } +} diff --git a/src/renderer/components/database-list-item-metadata.jsx b/src/renderer/components/database-list-item-metadata.jsx index f80c77bd..d16812fc 100644 --- a/src/renderer/components/database-list-item-metadata.jsx +++ b/src/renderer/components/database-list-item-metadata.jsx @@ -1,5 +1,6 @@ import React, { Component, PropTypes } from 'react'; -import TableSubmenu from './table-submenu.jsx'; +import DatabaseItem from './database-item.jsx'; + const STYLE = { header: { fontSize: '0.85em', color: '#636363' }, @@ -16,8 +17,9 @@ export default class DbMetadataList extends Component { triggersByTable: PropTypes.object, collapsed: PropTypes.bool, database: PropTypes.object.isRequired, - onDoubleClickItem: PropTypes.func, + onExecuteDefaultQuery: PropTypes.func, onSelectItem: PropTypes.func, + onGetSQLScript: PropTypes.func, } constructor(props, context) { @@ -35,10 +37,6 @@ export default class DbMetadataList extends Component { this.setState({ collapsed: !this.state.collapsed }); } - toggleTableCollapse(tableName) { - this.setState({ [tableName]: !this.state[tableName] }); - } - renderHeader() { const title = this.state.collapsed ? 'Expand' : 'Collapse'; const cssClass = this.state.collapsed ? 'right' : 'down'; @@ -56,7 +54,13 @@ export default class DbMetadataList extends Component { } renderItems() { - const { onDoubleClickItem, onSelectItem, items, database } = this.props; + const { + onExecuteDefaultQuery, + onSelectItem, + items, + database, + onGetSQLScript, + } = this.props; if (!items || this.state.collapsed) { return null; @@ -69,79 +73,30 @@ export default class DbMetadataList extends Component { } return items.map(item => { - const isClickable = !!onDoubleClickItem; const hasChildElements = !!onSelectItem; - const title = isClickable ? 'Click twice to select default query' : ''; - const onDoubleClick = isClickable - ? onDoubleClickItem.bind(this, database, item) - : () => {}; - const onSingleClick = hasChildElements - ? () => {onSelectItem(database, item); this.toggleTableCollapse(item.name);} - : () => {}; const cssStyle = {...STYLE.item}; if (this.state.collapsed) { cssStyle.display = 'none'; } cssStyle.cursor = hasChildElements ? 'pointer' : 'default'; - const collapseCssClass = this.state[item.name] ? 'down' : 'right'; - const collapseIcon = ( - - ); - const tableIcon = ( - - ); - /* - TODO: Move standard table query to context menu - */ return ( -
- - { this.props.title === 'Tables' ? collapseIcon : null } - { this.props.title === 'Tables' ? tableIcon : null } - {item.name} - - {this.renderSubItems(item.name)} -
+ ); }); } - renderSubItems(table) { - const { columnsByTable, triggersByTable, database } = this.props; - - if (!columnsByTable || !columnsByTable[table]) { - return null; - } - - const displayStyle = {}; - if (!this.state[table]) { - displayStyle.display = 'none'; - } - - return ( -
- - -
- ); - } - render() { return (
diff --git a/src/renderer/components/database-list-item.jsx b/src/renderer/components/database-list-item.jsx index c4c4307e..b1ee861f 100644 --- a/src/renderer/components/database-list-item.jsx +++ b/src/renderer/components/database-list-item.jsx @@ -20,9 +20,10 @@ export default class DatabaseListItem extends Component { functions: PropTypes.array, procedures: PropTypes.array, database: PropTypes.object.isRequired, - onDoubleClickTable: PropTypes.func.isRequired, + onExecuteDefaultQuery: PropTypes.func.isRequired, onSelectTable: PropTypes.func.isRequired, onSelectDatabase: PropTypes.func.isRequired, + onGetSQLScript: PropTypes.func.isRequired, } constructor(props, context) { @@ -73,9 +74,10 @@ export default class DatabaseListItem extends Component { functions, procedures, database, - onDoubleClickTable, + onExecuteDefaultQuery, onSelectTable, - onSelectDatabase + onSelectDatabase, + onGetSQLScript, } = this.props; let filteredTables, filteredViews, filteredFunctions, filteredProcedures; @@ -112,24 +114,28 @@ export default class DatabaseListItem extends Component { columnsByTable={columnsByTable} triggersByTable={triggersByTable} database={database} - onDoubleClickItem={onDoubleClickTable} - onSelectItem={onSelectTable} /> + onExecuteDefaultQuery={onExecuteDefaultQuery} + onSelectItem={onSelectTable} + onGetSQLScript={onGetSQLScript} /> + onExecuteDefaultQuery={onExecuteDefaultQuery} + onGetSQLScript={onGetSQLScript} /> + database={database} + onGetSQLScript={onGetSQLScript} /> + database={database} + onGetSQLScript={onGetSQLScript} />
); diff --git a/src/renderer/components/database-list.jsx b/src/renderer/components/database-list.jsx index 46071bc5..acb95e72 100644 --- a/src/renderer/components/database-list.jsx +++ b/src/renderer/components/database-list.jsx @@ -13,8 +13,9 @@ export default class DatabaseList extends Component { functionsByDatabase: PropTypes.object.isRequired, proceduresByDatabase: PropTypes.object.isRequired, onSelectDatabase: PropTypes.func.isRequired, - onDoubleClickTable: PropTypes.func.isRequired, + onExecuteDefaultQuery: PropTypes.func.isRequired, onSelectTable: PropTypes.func.isRequired, + onGetSQLScript: PropTypes.func.isRequired, } constructor(props, context) { @@ -32,9 +33,10 @@ export default class DatabaseList extends Component { viewsByDatabase, functionsByDatabase, proceduresByDatabase, - onDoubleClickTable, + onExecuteDefaultQuery, onSelectTable, onSelectDatabase, + onGetSQLScript, } = this.props; if (isFetching) { @@ -62,9 +64,10 @@ export default class DatabaseList extends Component { views={viewsByDatabase[database.name]} functions={functionsByDatabase[database.name]} procedures={proceduresByDatabase[database.name]} - onDoubleClickTable={onDoubleClickTable} + onExecuteDefaultQuery={onExecuteDefaultQuery} onSelectTable={onSelectTable} - onSelectDatabase={onSelectDatabase} /> + onSelectDatabase={onSelectDatabase} + onGetSQLScript={onGetSQLScript} /> )) } diff --git a/src/renderer/containers/query-browser.jsx b/src/renderer/containers/query-browser.jsx index 29ae0674..db457f05 100644 --- a/src/renderer/containers/query-browser.jsx +++ b/src/renderer/containers/query-browser.jsx @@ -11,6 +11,7 @@ import { fetchTableColumnsIfNeeded } from '../actions/columns'; import { fetchTableTriggersIfNeeded } from '../actions/triggers'; import { fetchViewsIfNeeded } from '../actions/views'; import { fetchRoutinesIfNeeded } from '../actions/routines'; +import { getSQLScriptIfNeeded } from '../actions/sqlscripts'; import DatabaseFilter from '../components/database-filter.jsx'; import DatabaseList from '../components/database-list.jsx'; import Header from '../components/header.jsx'; @@ -51,6 +52,7 @@ class QueryBrowserContainer extends Component { views: PropTypes.object.isRequired, routines: PropTypes.object.isRequired, queries: PropTypes.object.isRequired, + sqlscripts: PropTypes.object.isRequired, dispatch: PropTypes.func.isRequired, history: PropTypes.object.isRequired, route: PropTypes.object.isRequired, @@ -110,7 +112,7 @@ class QueryBrowserContainer extends Component { dispatch(ConnActions.connect(params.id, database.name)); } - onDoubleClickTable(database, table) { + onExecuteDefaultQuery(database, table) { this.props.dispatch(QueryActions.executeDefaultSelectQueryIfNeeded(database.name, table.name)); } @@ -119,6 +121,10 @@ class QueryBrowserContainer extends Component { this.props.dispatch(fetchTableTriggersIfNeeded(database.name, table.name)); } + onGetSQLScript(database, item, actionType, objectType) { + this.props.dispatch(getSQLScriptIfNeeded(database.name, item.name, actionType, objectType)); + } + onSQLChange (sqlQuery) { this.props.dispatch(QueryActions.updateQueryIfNeeded(sqlQuery)); } @@ -308,8 +314,9 @@ class QueryBrowserContainer extends Component { functionsByDatabase={routines.functionsByDatabase} proceduresByDatabase={routines.proceduresByDatabase} onSelectDatabase={::this.onSelectDatabase} - onDoubleClickTable={::this.onDoubleClickTable} - onSelectTable={::this.onSelectTable} /> + onExecuteDefaultQuery={::this.onExecuteDefaultQuery} + onSelectTable={::this.onSelectTable} + onGetSQLScript={::this.onGetSQLScript} /> @@ -327,7 +334,7 @@ class QueryBrowserContainer extends Component { function mapStateToProps (state) { - const { connections, databases, tables, columns, triggers, views, routines, queries, status } = state; + const { connections, databases, tables, columns, triggers, views, routines, queries, sqlscripts, status } = state; return { connections, @@ -338,6 +345,7 @@ function mapStateToProps (state) { views, routines, queries, + sqlscripts, status, }; } diff --git a/src/renderer/reducers/index.js b/src/renderer/reducers/index.js index d83b95a1..3209d4dc 100644 --- a/src/renderer/reducers/index.js +++ b/src/renderer/reducers/index.js @@ -9,6 +9,7 @@ import views from './views'; import routines from './routines'; import columns from './columns'; import triggers from './triggers'; +import sqlscripts from './sqlscripts'; const rootReducer = combineReducers({ @@ -22,6 +23,7 @@ const rootReducer = combineReducers({ routines, columns, triggers, + sqlscripts, }); diff --git a/src/renderer/reducers/routines.js b/src/renderer/reducers/routines.js index 224fc236..3f26ee51 100644 --- a/src/renderer/reducers/routines.js +++ b/src/renderer/reducers/routines.js @@ -28,12 +28,14 @@ export default function (state = INITIAL_STATE, action) { functionsByDatabase: { ...state.functionsByDatabase, [action.database]: action.routines.filter(_isFunction).map(routine => ({ - name: routine.routineName })), + name: routine.routineName, + routineDefinition: routine.routineDefinition })), }, proceduresByDatabase: { ...state.proceduresByDatabase, [action.database]: action.routines.filter(_isProcedure).map(routine => ({ - name: routine.routineName })), + name: routine.routineName, + routineDefinition: routine.routineDefinition })), }, error: null, }; diff --git a/src/renderer/reducers/sqlscripts.js b/src/renderer/reducers/sqlscripts.js new file mode 100644 index 00000000..bfb1f553 --- /dev/null +++ b/src/renderer/reducers/sqlscripts.js @@ -0,0 +1,60 @@ +import * as connTypes from '../actions/connections'; +import * as types from '../actions/sqlscripts'; + + +const INITIAL_STATE = { + isFetching: false, + didInvalidate: false, + scriptsByObject: {}, +}; + + +export default function (state = INITIAL_STATE, action) { + switch (action.type) { + case connTypes.CONNECTION_REQUEST: { + return action.isServerConnection + ? { ...INITIAL_STATE, didInvalidate: true } + : state; + } + case types.GET_SCRIPT_REQUEST: { + return { + ...state, + scriptType: action.scriptType, + isFetching: true, + didInvalidate: false, + error: null, + }; + } + case types.GET_SCRIPT_SUCCESS: { + const scriptsByItem = !state.scriptsByObject[action.database] + ? null + : state.scriptsByObject[action.database][action.item]; + return { + ...state, + isFetching: false, + didInvalidate: false, + error: null, + scriptsByObject: { + ...state.scriptsByObject, + [action.database]: { + ...state.scriptsByObject[action.database], + [action.item]: { + ...scriptsByItem, + objectType: action.objectType, + [action.actionType]: action.script, + }, + }, + }, + }; + } + case types.GET_SCRIPT_FAILURE: { + return { + ...state, + isFetching: false, + didInvalidate: true, + error: action.error, + }; + } + default : return state; + } +}