diff --git a/docs/en_US/restore_dialog.rst b/docs/en_US/restore_dialog.rst index 9641e7ff2..3d88ae488 100644 --- a/docs/en_US/restore_dialog.rst +++ b/docs/en_US/restore_dialog.rst @@ -27,6 +27,8 @@ restore process: * Select *Custom or tar* to restore from a custom archive file to create a copy of the backed-up object. + * Select *Plain* to restore a plain SQL backup. When selecting this option + all the other options will not be applicable. * Select *Directory* to restore from a compressed directory-format archive. * Enter the complete path to the backup file in the *Filename* field. diff --git a/web/pgadmin/static/js/SchemaView/FormView.jsx b/web/pgadmin/static/js/SchemaView/FormView.jsx index e9edd6e16..5e2495da4 100644 --- a/web/pgadmin/static/js/SchemaView/FormView.jsx +++ b/web/pgadmin/static/js/SchemaView/FormView.jsx @@ -32,6 +32,7 @@ import { } from './hooks'; import { registerView, View } from './registry'; import { createFieldControls, listenDepChanges } from './utils'; +import FormViewTab from './FormViewTab'; const ErrorMessageBox = () => { const [key, setKey] = useState(0); @@ -181,14 +182,8 @@ export default function FormView({ action={(ref) => ref?.updateIndicator()} >{ finalGroups.map((tabGroup, idx) => - ) }{hasSQLTab && diff --git a/web/pgadmin/static/js/SchemaView/FormViewTab.jsx b/web/pgadmin/static/js/SchemaView/FormViewTab.jsx new file mode 100644 index 000000000..f63c1281c --- /dev/null +++ b/web/pgadmin/static/js/SchemaView/FormViewTab.jsx @@ -0,0 +1,45 @@ +///////////////////////////////////////////////////////////// +// +// pgAdmin 4 - PostgreSQL Tools +// +// Copyright (C) 2013 - 2025, The pgAdmin Development Team +// This software is released under the PostgreSQL Licence +// +////////////////////////////////////////////////////////////// + +import { Tab } from '@mui/material'; +import React, { useContext, useState } from 'react'; +import { useFieldOptions, useSchemaStateSubscriber } from './hooks'; +import { SchemaStateContext } from './SchemaState'; +import PropTypes from 'prop-types'; + +export default function FormViewTab({tabGroup, idx, tabValue, ...props}) { + const accessPath = [tabGroup.id]; + const [refreshKey, setRefreshKey] = useState(0); + const subscriberManager = useSchemaStateSubscriber(setRefreshKey); + const schemaState = useContext(SchemaStateContext); + const options = useFieldOptions(accessPath, schemaState, subscriberManager); + + return ( + + ); +}; + +FormViewTab.muiName = Tab.muiName; + +FormViewTab.propTypes = { + tabGroup: PropTypes.object, + idx: PropTypes.number, + tabValue: PropTypes.number, +}; diff --git a/web/pgadmin/static/js/Theme/index.jsx b/web/pgadmin/static/js/Theme/index.jsx index 8d437c685..40ce813ef 100644 --- a/web/pgadmin/static/js/Theme/index.jsx +++ b/web/pgadmin/static/js/Theme/index.jsx @@ -788,13 +788,17 @@ function getFinalTheme(baseTheme) { MuiTab: { styleOverrides: { root: { - '&.MuiTab-textColorPrimary':{ + '&:not(.Mui-disabled).MuiTab-textColorPrimary':{ color: baseTheme.palette.text.primary, }, '&.Mui-selected': { color: baseTheme.otherVars.activeColor, }, - } + }, + icon: { + fontSize: '1rem', + marginRight: '2px', + }, } }, MuiBackdrop: { diff --git a/web/pgadmin/tools/restore/__init__.py b/web/pgadmin/tools/restore/__init__.py index ed190158d..74c5b45da 100644 --- a/web/pgadmin/tools/restore/__init__.py +++ b/web/pgadmin/tools/restore/__init__.py @@ -141,18 +141,18 @@ def _get_create_req_data(): data = json.loads(request.data) try: - _file = filename_with_file_manager_path(data['file']) + filepath = filename_with_file_manager_path(data['file']) except Exception as e: return True, internal_server_error(errormsg=str(e)), data, None - if _file is None: + if filepath is None: return True, make_json_response( status=410, success=0, errormsg=_("File could not be found.") - ), data, _file + ), data, filepath - return False, '', data, _file + return False, '', data, filepath def _connect_server(sid): @@ -263,15 +263,15 @@ def set_multiple(key, param, data, args, driver, conn, with_schema=True): return False -def _set_args_param_values(data, manager, server, driver, conn, _file): +def get_restore_util_args(data, manager, server, driver, conn, filepath): """ - add args to the list. + return the args for the command :param data: Data. :param manager: Manager. :param server: Server. :param driver: Driver. :param conn: Connection. - :param _file: File. + :param filepath: File. :return: args list. """ args = [] @@ -347,11 +347,56 @@ def _set_args_param_values(data, manager, server, driver, conn, _file): False) set_multiple('indexes', '--index', data, args, driver, conn, False) - args.append(fs_short_path(_file)) + args.append(fs_short_path(filepath)) return args +def get_sql_util_args(data, manager, server, filepath): + """ + return the args for the command + :param data: Data. + :param manager: Manager. + :param server: Server. + :param filepath: File. + :return: args list. + """ + args = [ + '--host', + manager.local_bind_host if manager.use_ssh_tunnel else server.host, + '--port', + str(manager.local_bind_port) if manager.use_ssh_tunnel + else str(server.port), + '--username', server.username, '--dbname', + data['database'], + '--file', fs_short_path(filepath) + ] + + return args + + +def use_restore_utility(data, manager, server, driver, conn, filepath): + utility = manager.utility('restore') + ret_val = does_utility_exist(utility) + if ret_val: + return ret_val, None, None + + args = get_restore_util_args(data, manager, server, driver, conn, filepath) + + return None, utility, args + + +def use_sql_utility(data, manager, server, filepath): + utility = manager.utility('sql') + ret_val = does_utility_exist(utility) + if ret_val: + return ret_val, None, None + + args = get_sql_util_args(data, manager, server, filepath) + + return None, utility, args + + @blueprint.route('/job/', methods=['POST'], endpoint='create_job') @pga_login_required def create_restore_job(sid): @@ -364,7 +409,7 @@ def create_restore_job(sid): Returns: None """ - is_error, errmsg, data, _file = _get_create_req_data() + is_error, errmsg, data, filepath = _get_create_req_data() if is_error: return errmsg @@ -372,16 +417,19 @@ def create_restore_job(sid): if is_error: return errmsg - utility = manager.utility('restore') - ret_val = does_utility_exist(utility) - if ret_val: + if data['format'] == 'plain': + error_msg, utility, args = use_sql_utility( + data, manager, server, filepath) + else: + error_msg, utility, args = use_restore_utility( + data, manager, server, driver, conn, filepath) + + if error_msg is not None: return make_json_response( success=0, - errormsg=ret_val + errormsg=error_msg ) - args = _set_args_param_values(data, manager, server, driver, conn, _file) - try: p = BatchProcess( desc=RestoreMessage( diff --git a/web/pgadmin/tools/restore/static/js/restore.js b/web/pgadmin/tools/restore/static/js/restore.js index f62c29b51..c262e5b83 100644 --- a/web/pgadmin/tools/restore/static/js/restore.js +++ b/web/pgadmin/tools/restore/static/js/restore.js @@ -81,7 +81,8 @@ define('tools.restore', [ ()=>getRestoreDisableOptionSchema({nodeInfo: treeNodeInfo}), ()=>getRestoreMiscellaneousSchema({nodeInfo: treeNodeInfo}), { - role: ()=>getNodeListByName('role', treeNodeInfo, itemNodeData) + role: ()=>getNodeListByName('role', treeNodeInfo, itemNodeData), + nodeType: itemNodeData._type, }, treeNodeInfo, pgBrowser diff --git a/web/pgadmin/tools/restore/static/js/restore.ui.js b/web/pgadmin/tools/restore/static/js/restore.ui.js index 685a88360..f124967d3 100644 --- a/web/pgadmin/tools/restore/static/js/restore.ui.js +++ b/web/pgadmin/tools/restore/static/js/restore.ui.js @@ -343,12 +343,37 @@ export default class RestoreSchema extends BaseUISchema { this.getRestoreMiscellaneousSchema = restoreMiscellaneousSchema; this.treeNodeInfo = treeNodeInfo; this.pgBrowser = pgBrowser; + + this.formatOptions = this.getFormatOptions(); } get idAttribute() { return 'id'; } + isPlainFormat(state) { + return state.format == 'plain'; + } + + getFormatOptions() { + const options = [{ + label: gettext('Custom or tar'), + value: 'custom', + }, { + label: gettext('Directory'), + value: 'directory', + }]; + + if(this.fieldOptions.nodeType == 'database') { + options.splice(1, 0, { + label: gettext('Plain'), + value: 'plain', + }); + } + + return options; + } + get baseFields() { let obj = this; return [{ @@ -356,14 +381,15 @@ export default class RestoreSchema extends BaseUISchema { label: gettext('Format'), disabled: false,type: 'select', controlProps: { allowClear: false, width: '100%' }, - options: [{ - label: gettext('Custom or tar'), - value: 'custom', - }, - { - label: gettext('Directory'), - value: 'directory', - }], + options: this.formatOptions, + depChange: (state) => { + if(state.format == 'plain') { + return { + no_of_jobs: undefined, + role: undefined, + }; + } + } }, { id: 'file', label: gettext('Filename'), @@ -378,11 +404,12 @@ export default class RestoreSchema extends BaseUISchema { }; }, disabled: false, - deps: ['format'] + deps: ['format'], }, { id: 'no_of_jobs', label: gettext('Number of jobs'), type: 'int', + disabled: obj.isPlainFormat, }, { id: 'role', label: gettext('Role name'), @@ -392,35 +419,45 @@ export default class RestoreSchema extends BaseUISchema { controlProps: { allowClear: false, }, + disabled: obj.isPlainFormat, + }, { + id: 'data-options', type: 'group', label: gettext('Data Options'), + disabled: function(state) { + return obj.isPlainFormat(state); + }, deps: ['format'] }, { type: 'nested-fieldset', label: gettext('Sections'), - group: gettext('Data Options'), + group: 'data-options', schema:obj.getSectionSchema(), - visible: true + visible: true, }, { type: 'nested-fieldset', label: gettext('Type of objects'), - group: gettext('Data Options'), + group: 'data-options', schema:obj.getRestoreTypeObjSchema(), - visible: true + visible: true, }, { type: 'nested-fieldset', label: gettext('Do not save'), - group: gettext('Data Options'), + group: 'data-options', schema:obj.getRestoreSaveOptSchema(), - visible: true + visible: true, + }, { + id: 'query-options', type: 'group', label: gettext('Query Options'), + disabled: function(state) { + return obj.isPlainFormat(state); + }, deps: ['format'] }, { id: 'include_create_database', label: gettext('Include CREATE DATABASE statement'), type: 'switch', - disabled: false, - group: gettext('Query Options') + group: 'query-options' }, { id: 'clean', label: gettext('Clean before restore'), type: 'switch', - group: gettext('Query Options'), + group: 'query-options', inlineGroup: 'clean', disabled: function(state) { if(obj.selectedNodeType === 'function' || obj.selectedNodeType === 'trigger_function') { @@ -432,7 +469,7 @@ export default class RestoreSchema extends BaseUISchema { id: 'if_exists', label: gettext('Include IF EXISTS clause'), type: 'switch', - group: gettext('Query Options'), + group: 'query-options', inlineGroup: 'clean', deps: ['clean'], disabled: function(state) { @@ -447,29 +484,39 @@ export default class RestoreSchema extends BaseUISchema { label: gettext('Single transaction'), type: 'switch', disabled: false, - group: gettext('Query Options'), + group: 'query-options', + }, { + id: 'table-options', type: 'group', label: gettext('Table Options'), + disabled: function(state) { + return obj.isPlainFormat(state); + }, deps: ['format'] }, { id: 'enable_row_security', label: gettext('Enable row security'), type: 'switch', disabled: false, - group: gettext('Table Options'), + group: 'table-options', }, { id: 'no_data_fail_table', label: gettext('No data for failed tables'), type: 'switch', disabled: false, - group: gettext('Table Options'), + group: 'table-options', + }, { + id: 'options', type: 'group', label: gettext('Options'), + disabled: function(state) { + return obj.isPlainFormat(state); + }, deps: ['format'] }, { type: 'nested-fieldset', label: gettext('Disable'), - group: gettext('Options'), + group: 'options', schema:obj.getRestoreDisableOptionSchema(), visible: true }, { type: 'nested-fieldset', label: gettext('Miscellaneous / Behavior'), - group: gettext('Options'), + group: 'options', schema:obj.getRestoreMiscellaneousSchema(), visible: true }]; diff --git a/web/pgadmin/tools/restore/tests/test_restore_create_job_unit_test.py b/web/pgadmin/tools/restore/tests/test_restore_create_job_unit_test.py index f2b94fe7d..99bbdb620 100644 --- a/web/pgadmin/tools/restore/tests/test_restore_create_job_unit_test.py +++ b/web/pgadmin/tools/restore/tests/test_restore_create_job_unit_test.py @@ -24,6 +24,28 @@ RESTORE_JOB_URL = '/restore/job/{0}' class RestoreCreateJobTest(BaseTestGenerator): """Test the RestoreCreateJob class""" scenarios = [ + ('When restore object with plain format', + dict( + class_params=dict( + sid=1, + name='test_restore_server', + port=5444, + host='localhost', + database='postgres', + bfile='test_restore', + username='postgres' + ), + params=dict( + file='test_restore_file', + format='plain', + database='postgres' + ), + url=RESTORE_JOB_URL, + expected_cmd='psql', + expected_cmd_opts=['--file'], + not_expected_cmd_opts=[], + expected_exit_code=[0, None] + )), ('When restore object with default options', dict( class_params=dict( @@ -46,6 +68,7 @@ class RestoreCreateJobTest(BaseTestGenerator): database='postgres' ), url=RESTORE_JOB_URL, + expected_cmd='pg_restore', expected_cmd_opts=['--verbose'], not_expected_cmd_opts=[], expected_exit_code=[0, None] @@ -72,6 +95,7 @@ class RestoreCreateJobTest(BaseTestGenerator): database='postgres' ), url=RESTORE_JOB_URL, + expected_cmd='pg_restore', expected_cmd_opts=['--verbose', '--format=d'], not_expected_cmd_opts=[], expected_exit_code=[0, None] @@ -103,6 +127,7 @@ class RestoreCreateJobTest(BaseTestGenerator): only_schema=True ), url=RESTORE_JOB_URL, + expected_cmd='pg_restore', expected_cmd_opts=['--verbose', '--jobs', '2', '--section=pre-data', '--section=data', '--section=post-data'], @@ -136,6 +161,7 @@ class RestoreCreateJobTest(BaseTestGenerator): dns_owner=True ), url=RESTORE_JOB_URL, + expected_cmd='pg_restore', expected_cmd_opts=['--verbose', '--data-only'], not_expected_cmd_opts=[], # Below options should be enabled once we fix the issue #3368 @@ -167,6 +193,7 @@ class RestoreCreateJobTest(BaseTestGenerator): only_data=False ), url=RESTORE_JOB_URL, + expected_cmd='pg_restore', expected_cmd_opts=['--no-owner', '--no-tablespaces', '--no-privileges'], @@ -200,6 +227,7 @@ class RestoreCreateJobTest(BaseTestGenerator): only_data=False ), url=RESTORE_JOB_URL, + expected_cmd='pg_restore', expected_cmd_opts=['--no-comments', '--no-publications', '--no-subscriptions', '--no-security-labels'], not_expected_cmd_opts=[], @@ -231,6 +259,7 @@ class RestoreCreateJobTest(BaseTestGenerator): dns_table_access_method=True ), url=RESTORE_JOB_URL, + expected_cmd='pg_restore', expected_cmd_opts=['--no-table-access-method'], not_expected_cmd_opts=[], expected_exit_code=[0, None], @@ -262,6 +291,7 @@ class RestoreCreateJobTest(BaseTestGenerator): if_exists=True, ), url=RESTORE_JOB_URL, + expected_cmd='pg_restore', expected_cmd_opts=['--create', '--clean', '--single-transaction', '--if-exists'], not_expected_cmd_opts=[], @@ -291,6 +321,7 @@ class RestoreCreateJobTest(BaseTestGenerator): only_schema=False ), url=RESTORE_JOB_URL, + expected_cmd='pg_restore', expected_cmd_opts=['--enable-row-security', '--no-data-for-failed-tables'], not_expected_cmd_opts=[], @@ -318,6 +349,7 @@ class RestoreCreateJobTest(BaseTestGenerator): only_schema=False ), url=RESTORE_JOB_URL, + expected_cmd='pg_restore', expected_cmd_opts=['--disable-triggers'], not_expected_cmd_opts=[], expected_exit_code=[0, None] @@ -344,6 +376,7 @@ class RestoreCreateJobTest(BaseTestGenerator): exit_on_error=True, ), url=RESTORE_JOB_URL, + expected_cmd='pg_restore', expected_cmd_opts=['--exit-on-error', '--use-set-session-authorization'], not_expected_cmd_opts=[], @@ -370,6 +403,7 @@ class RestoreCreateJobTest(BaseTestGenerator): exclude_schema="sch*" ), url=RESTORE_JOB_URL, + expected_cmd='pg_restore', expected_cmd_opts=['--exclude-schema', 'sch*'], not_expected_cmd_opts=[], expected_exit_code=[0, None] @@ -459,6 +493,11 @@ class RestoreCreateJobTest(BaseTestGenerator): self.assertTrue(restore_message_mock.called) self.assertTrue(batch_process_mock.called) + if self.expected_cmd: + self.assertIn( + self.expected_cmd, + batch_process_mock.call_args_list[0][1]['cmd'] + ) if self.expected_cmd_opts: for opt in self.expected_cmd_opts: self.assertIn( diff --git a/web/regression/javascript/schema_ui_files/restore.ui.spec.js b/web/regression/javascript/schema_ui_files/restore.ui.spec.js index f9f93e9ba..1e824e910 100644 --- a/web/regression/javascript/schema_ui_files/restore.ui.spec.js +++ b/web/regression/javascript/schema_ui_files/restore.ui.spec.js @@ -23,6 +23,7 @@ describe('RestoreSchema', ()=>{ { role: ()=>[], encoding: ()=>[], + nodeType: '', }, {server: {version: 11000}}, pgAdmin.pgBrowser