Added support to select/deselect objects in the Backup dialog. #642

This commit is contained in:
Nikhil Mohite 2023-09-13 11:07:28 +05:30 committed by GitHub
parent 8c91d40932
commit aa973fc8ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 715 additions and 34 deletions

View File

@ -251,6 +251,16 @@ tab to provide other backup options.
table locks at the beginning of the dump. Instead, fail if unable to lock a
table within the specified timeout.
Click the *Objects* tab to continue.
.. image:: images/backup_object_selection.png
:alt: Select objects in backup dialog
:align: center
* Select the objects from tree to take backup of selected objects only.
* If Schema is selected then it will take the backup of that selected schema only.
* If any Table, View, Materialized View, Sequences, or Foreign Table is selected then it will take the backup of those selected objects.
When youve specified the details that will be incorporated into the pg_dump
command:

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

View File

@ -141,6 +141,7 @@
"raf": "^3.4.1",
"rc-dock": "^3.2.9",
"react": "^17.0.1",
"react-arborist": "^3.2.0",
"react-aspen": "^1.1.0",
"react-checkbox-tree": "^1.7.2",
"react-data-grid": "https://github.com/pgadmin-org/react-data-grid.git#200d2f5e02de694e3e9ffbe177c279bc40240fb8",

View File

@ -0,0 +1,268 @@
import { Checkbox, makeStyles } from '@material-ui/core';
import clsx from 'clsx';
import gettext from 'sources/gettext';
import React, { useEffect, useRef } from 'react';
import { Tree } from 'react-arborist';
import AutoSizer from 'react-virtualized-auto-sizer';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import ChevronRightIcon from '@material-ui/icons/ChevronRight';
import PropTypes from 'prop-types';
import IndeterminateCheckBoxIcon from '@material-ui/icons/IndeterminateCheckBox';
import EmptyPanelMessage from '../components/EmptyPanelMessage';
import CheckBoxIcon from '@material-ui/icons/CheckBox';
const useStyles = makeStyles((theme) => ({
node: {
display: 'inline-block',
paddingLeft: '1.5rem',
height: '1.5rem'
},
checkboxStyle: {
fill: theme.palette.primary.main
},
tree: {
background: theme.palette.background.default,
height: '100%',
width: '100%',
display: 'flex',
flexDirection: 'column',
flex: 1,
},
selectedNode: {
background: theme.otherVars.stepBg,
},
focusedNode: {
background: theme.palette.primary.light,
},
leafNode: {
marginLeft: '1.5rem'
},
}));
export const PgTreeSelectionContext = React.createContext();
export default function PgTreeView({ data = [], hasCheckbox = false, selectionChange = null}) {
let classes = useStyles();
let treeData = data;
const treeObj = useRef();
const [selectedCheckBoxNodes, setSelectedCheckBoxNodes] = React.useState();
const onSelectionChange = () => {
if (hasCheckbox) {
let selectedChildNodes = [];
treeObj.current.selectedNodes.forEach((node) => {
selectedChildNodes.push(node.id);
});
setSelectedCheckBoxNodes(selectedChildNodes);
}
selectionChange?.(treeObj.current.selectedNodes);
};
return (<>
{ treeData.length > 0 ?
<PgTreeSelectionContext.Provider value={_.isUndefined(selectedCheckBoxNodes) ? []: selectedCheckBoxNodes}>
<div className={clsx(classes.tree)}>
<AutoSizer>
{({ width, height }) => (
<Tree
ref={(obj) => {
treeObj.current = obj;
}}
width={width}
height={height}
data={treeData}
>
{
(props) => <Node onNodeSelectionChange={onSelectionChange} hasCheckbox={hasCheckbox} {...props}></Node>
}
</Tree>
)}
</AutoSizer>
</div>
</PgTreeSelectionContext.Provider>
:
<EmptyPanelMessage text={gettext('No objects are found to display')}/>
}
</>
);
}
PgTreeView.propTypes = {
data: PropTypes.array,
selectionChange: PropTypes.func,
hasCheckbox: PropTypes.bool,
};
function Node({ node, style, tree, hasCheckbox, onNodeSelectionChange}) {
const classes = useStyles();
const pgTreeSelCtx = React.useContext(PgTreeSelectionContext);
const [isSelected, setIsSelected] = React.useState(pgTreeSelCtx.includes(node.id) ? true : false);
const [isIndeterminate, setIsIndeterminate] = React.useState(node?.parent.level==0? true: false);
useEffect(()=>{
setIsIndeterminate(node.data.isIndeterminate);
}, [node?.data?.isIndeterminate]);
const onCheckboxSelection = (e) => {
if (hasCheckbox) {
setIsSelected(e.currentTarget.checked);
if (e.currentTarget.checked) {
node.selectMulti(node.id);
if (!node.isLeaf && node.isOpen) {
selectAllChild(node, tree);
} else {
if (node?.parent) {
checkAndSelectParent(node);
}
}
if(node?.level == 0) {
node.data.isIndeterminate = false;
}
} else {
node.deselect(node);
if (!node.isLeaf) {
deselectAllChild(node);
}
if(node?.parent){
delectPrentNode(node.parent);
}
}
}
onNodeSelectionChange();
};
return (
<div style={style} className={clsx(node.isFocused ? classes.focusedNode : '', node.isSelected ? classes.selectedNode : '')} onClick={(e) => {
node.focus();
e.stopPropagation();
}}>
<CollectionArrow node={node} tree={tree} />
{
hasCheckbox ? <Checkbox style={{ padding: 0 }} color="primary" className={clsx(!node.isInternal ? classes.leafNode: null)}
checked={isSelected ? true: false}
checkedIcon={isIndeterminate ? <IndeterminateCheckBoxIcon />: <CheckBoxIcon />}
onChange={onCheckboxSelection}/> :
<span className={clsx(node.data.icon)}></span>
}
<div className={clsx(node.data.icon, classes.node)}>{node.data.name}</div>
</div>
);
}
Node.propTypes = {
node: PropTypes.object,
style: PropTypes.any,
tree: PropTypes.object,
hasCheckbox: PropTypes.bool,
onNodeSelectionChange: PropTypes.func
};
function CollectionArrow({ node, tree }) {
const toggleNode = () => {
node.isInternal && node.toggle();
if (node.isSelected && node.isOpen) {
setTimeout(()=>{
selectAllChild(node, tree);
}, 0);
}
};
return (
<span onClick={toggleNode} >
{node.isInternal ? <ToggleArrowIcon node={node} /> : null}
</span>
);
}
CollectionArrow.propTypes = {
node: PropTypes.object,
tree: PropTypes.object
};
function ToggleArrowIcon({node}){
return (<>{node.isOpen ? <ExpandMoreIcon /> : <ChevronRightIcon />}</>);
}
ToggleArrowIcon.propTypes = {
node: PropTypes.object,
};
function checkAndSelectParent(chNode){
let isAllChildSelected = true;
chNode?.parent?.children?.forEach((child) => {
if (!child.isSelected) {
isAllChildSelected = false;
}
});
if (chNode?.parent) {
if (isAllChildSelected) {
if (chNode.parent?.level == 0) {
chNode.parent.data.isIndeterminate = true;
} else {
chNode.parent.data.isIndeterminate = false;
}
chNode.parent.selectMulti(chNode.parent.id);
} else {
chNode.parent.data.isIndeterminate = true;
chNode.parent.selectMulti(chNode.parent.id);
}
checkAndSelectParent(chNode.parent);
}
}
checkAndSelectParent.propTypes = {
chNode: PropTypes.object
};
function delectPrentNode(chNode){
if (chNode) {
let isAnyChildSelected = false;
chNode.children.forEach((childNode)=>{
if(childNode.isSelected && !isAnyChildSelected){
isAnyChildSelected = true;
}
});
if(isAnyChildSelected){
chNode.data.isIndeterminate = true;
} else {
chNode.deselect(chNode);
}
}
if (chNode?.parent) {
delectPrentNode(chNode.parent);
}
}
function selectAllChild(chNode, tree){
chNode?.children?.forEach(child => {
child.selectMulti(child.id);
if (child?.children) {
selectAllChild(child, tree);
}
});
if (chNode?.parent) {
checkAndSelectParent(chNode);
}
}
function deselectAllChild(chNode){
chNode?.children.forEach(child => {
child.deselect(child);
if (child?.children) {
deselectAllChild(child);
}
});
}

View File

@ -17,7 +17,7 @@ import { MappedFormControl } from './MappedControl';
import TabPanel from '../components/TabPanel';
import DataGridView from './DataGridView';
import { SCHEMA_STATE_ACTIONS, StateUtilsContext } from '.';
import { InputSQL } from '../components/FormComponents';
import { FormNote, InputSQL } from '../components/FormComponents';
import gettext from 'sources/gettext';
import { evalFunc } from 'sources/utils';
import CustomPropTypes from '../custom_prop_types';
@ -39,6 +39,10 @@ const useStyles = makeStyles((theme)=>({
nestedControl: {
height: 'unset',
},
fullControl: {
display: 'flex',
flexDirection: 'column'
},
errorMargin: {
/* Error footer space */
paddingBottom: '36px !important',
@ -306,7 +310,7 @@ export default function FormView({
firstEleID.current = field.id;
}
const currentControl = <MappedFormControl
let currentControl = <MappedFormControl
inputRef={(ele)=>{
if(firstEleRef && firstEleID.current === field.id) {
firstEleRef.current = ele;
@ -344,6 +348,13 @@ export default function FormView({
]}
/>;
if(field.isFullTab && field.helpMessage) {
currentControl = (<React.Fragment key={`coll-${field.id}`}>
<FormNote key={`note-${field.id}`} text={field.helpMessage}/>
{currentControl}
</React.Fragment>);
}
if(field.inlineNext) {
inlineComponents.push(React.cloneElement(currentControl, {
withContainer: false, controlGridBasis: 3
@ -422,6 +433,8 @@ export default function FormView({
let contentClassName = [stateUtils.formErr.message ? classes.errorMargin : null];
if(fullTabs.indexOf(tabName) == -1) {
contentClassName.push(classes.nestedControl);
} else {
contentClassName.push(classes.fullControl);
}
return (
<TabPanel key={tabName} value={tabValue} index={i} classNameRoot={clsx(tabsClassname[tabName], isNested ? classes.nestedTabPanel : null)}

View File

@ -12,7 +12,7 @@ import _ from 'lodash';
import {
FormInputText, FormInputSelect, FormInputSwitch, FormInputCheckbox, FormInputColor,
FormInputFileSelect, FormInputToggle, InputSwitch, FormInputSQL, InputSQL, FormNote, FormInputDateTimePicker, PlainString,
InputSelect, InputText, InputCheckbox, InputDateTimePicker, InputFileSelect, FormInputKeyboardShortcut, FormInputQueryThreshold, FormInputSelectThemes, InputRadio, FormButton
InputSelect, InputText, InputCheckbox, InputDateTimePicker, InputFileSelect, FormInputKeyboardShortcut, FormInputQueryThreshold, FormInputSelectThemes, InputRadio, FormButton, InputTree
} from '../components/FormComponents';
import Privilege from '../components/Privilege';
import { evalFunc } from 'sources/utils';
@ -35,6 +35,10 @@ function MappedFormControlBase({ type, value, id, onChange, className, visible,
onChange && onChange(changedValue);
}, []);
const onTreeSelection = useCallback((selectedValues)=> {
onChange && onChange(selectedValues);
}, []);
if (!visible) {
return <></>;
}
@ -89,6 +93,8 @@ function MappedFormControlBase({ type, value, id, onChange, className, visible,
return <FormInputSelectThemes name={name} value={value} onChange={onTextChange} {...props}/>;
case 'button':
return <FormButton name={name} value={value} className={className} onClick={onClick} {...props} />;
case 'tree':
return <InputTree name={name} treeData={props.treeData} onChange={onTreeSelection} {...props}/>;
default:
return <PlainString value={value} {...props} />;
}
@ -109,7 +115,8 @@ MappedFormControlBase.propTypes = {
noLabel: PropTypes.bool,
onClick: PropTypes.func,
withContainer: PropTypes.bool,
controlGridBasis: PropTypes.number
controlGridBasis: PropTypes.number,
treeData: PropTypes.array,
};
/* Control mapping for grid cell view */
@ -205,7 +212,7 @@ const ALLOWED_PROPS_FIELD_COMMON = [
'label', 'options', 'optionsLoaded', 'controlProps', 'schema', 'inputRef',
'visible', 'autoFocus', 'helpMessage', 'className', 'optionsReloadBasis',
'orientation', 'isvalidate', 'fields', 'radioType', 'hideBrowseButton', 'btnName', 'hidden',
'withContainer', 'controlGridBasis',
'withContainer', 'controlGridBasis', 'hasCheckbox', 'treeData'
];
const ALLOWED_PROPS_FIELD_FORM = [

View File

@ -12,6 +12,7 @@ const useStyles = makeStyles((theme)=>({
fontSize: '0.8rem',
display: 'flex',
alignItems: 'center',
flexDirection: 'column'
},
}));
@ -19,8 +20,7 @@ export default function EmptyPanelMessage({text}) {
const classes = useStyles();
return (
<Box className={classes.root}>
<InfoRoundedIcon style={{height: '1.2rem'}}/>
<span style={{marginLeft: '4px'}}>{text}</span>
<span style={{marginLeft: '4px'}}><InfoRoundedIcon style={{height: '1.2rem'}}/>{text}</span>
</Box>
);
}

View File

@ -43,6 +43,7 @@ import SelectThemes from './SelectThemes';
import { showFileManager } from '../helpers/showFileManager';
import { withColorPicker } from '../helpers/withColorPicker';
import { useWindowSize } from '../custom_hooks';
import PgTreeView from '../PgTreeView';
const useStyles = makeStyles((theme) => ({
@ -1275,3 +1276,14 @@ FormButton.propTypes = {
disabled: PropTypes.bool,
btnName: PropTypes.string
};
export function InputTree({hasCheckbox, treeData, onChange, ...props}){
return <PgTreeView data={treeData} hasCheckbox={hasCheckbox} selectionChange={onChange} {...props}></PgTreeView>;
}
InputTree.propTypes = {
hasCheckbox: PropTypes.bool,
treeData: PropTypes.array,
onChange: PropTypes.func,
selectionChange: PropTypes.func,
};

View File

@ -10,6 +10,7 @@
import json
import os
import copy
import functools
import operator
@ -27,6 +28,8 @@ from config import PG_DEFAULT_DRIVER
from pgadmin.model import Server, SharedServer
from pgadmin.misc.bgprocess import escape_dquotes_process_arg
from pgadmin.utils.constants import MIMETYPE_APP_JS
from pgadmin.tools.grant_wizard import _get_rows_for_type, \
get_node_sql_with_type, properties, get_data
# set template path for sql scripts
MODULE_NAME = 'backup'
@ -56,7 +59,8 @@ class BackupModule(PgAdminModule):
list: URL endpoints for backup module
"""
return ['backup.create_server_job', 'backup.create_object_job',
'backup.utility_exists']
'backup.utility_exists', 'backup.objects',
'backup.schema_objects']
# Create blueprint for BackupModule class
@ -355,6 +359,23 @@ def _get_args_params_values(data, conn, backup_obj_type, backup_file, server,
)
)
if 'objects' in data:
selected_objects = data.get('objects', {})
for _key in selected_objects:
param = 'schema' if _key == 'schema' else 'table'
args.extend(
functools.reduce(operator.iconcat, map(
lambda s: [f'--{param}',
r'{0}.{1}'.format(
driver.qtIdent(conn, s['schema']).replace(
'"', '\"'),
driver.qtIdent(conn, s['name']).replace(
'"', '\"')) if type(
s) is dict else driver.qtIdent(
conn, s).replace('"', '\"')],
selected_objects[_key] or []), [])
)
return args
@ -505,3 +526,124 @@ def check_utility_exists(sid, backup_obj_type):
)
return make_json_response(success=1)
@blueprint.route(
'/objects/<int:sid>/<int:did>', endpoint='objects'
)
@blueprint.route(
'/objects/<int:sid>/<int:did>/<int:scid>', endpoint='schema_objects'
)
@login_required
def objects(sid, did, scid=None):
"""
This function returns backup objects
Args:
sid: Server ID
did: database ID
scid: schema ID
Returns:
list of objects
"""
from pgadmin.tools.schema_diff.node_registry import SchemaDiffRegistry
server = get_server(sid)
if server is None:
return make_json_response(
success=0,
errormsg=_("Could not find the specified server.")
)
from pgadmin.utils.driver import get_driver
from flask_babel import gettext
from pgadmin.utils.ajax import precondition_required
server_info = {}
server_info['manager'] = get_driver(PG_DEFAULT_DRIVER) \
.connection_manager(sid)
server_info['conn'] = server_info['manager'].connection(
did=did)
# If DB not connected then return error to browser
if not server_info['conn'].connected():
return precondition_required(
gettext("Connection to the server has been lost.")
)
# Set template path for sql scripts
server_info['server_type'] = server_info['manager'].server_type
server_info['version'] = server_info['manager'].version
if server_info['server_type'] == 'pg':
server_info['template_path'] = 'grant_wizard/pg/#{0}#'.format(
server_info['version'])
elif server_info['server_type'] == 'ppas':
server_info['template_path'] = 'grant_wizard/ppas/#{0}#'.format(
server_info['version'])
res, msg = get_data(sid, did, scid, 'schema' if scid else 'database',
server_info)
tree_data = {
'table': [],
'view': [],
'materialized view': [],
'foreign table': [],
'sequence': []
}
schema_group = {}
for data in res:
obj_type = data['object_type'].lower()
if obj_type in ['table', 'view', 'materialized view', 'foreign table',
'sequence']:
if data['nspname'] not in schema_group:
schema_group[data['nspname']] = {
'id': data['nspname'],
'name': data['nspname'],
'icon': 'icon-schema',
'children': copy.deepcopy(tree_data),
'is_schema': True,
}
icon_data = {
'materialized view': 'icon-mview',
'foreign table': 'icon-foreign_table'
}
icon = icon_data[obj_type] if obj_type in icon_data \
else data['icon']
schema_group[data['nspname']]['children'][obj_type].append({
'id': f'{data["nspname"]}_{data["name"]}',
'name': data['name'],
'icon': icon,
'schema': data['nspname'],
'type': obj_type,
'_name': '{0}.{1}'.format(data['nspname'], data['name'])
})
schema_group = [dt for k, dt in schema_group.items()]
for ch in schema_group:
children = []
for obj_type, data in ch['children'].items():
if data:
icon_data = {
'materialized view': 'icon-coll-mview',
'foreign table': 'icon-coll-foreign_table'
}
icon = icon_data[obj_type] if obj_type in icon_data \
else f'icon-coll-{obj_type.lower()}',
children.append({
'id': f'{ch["id"]}_{obj_type}',
'name': f'{obj_type.title()}s',
'icon': icon,
'children': data,
'type': obj_type,
'is_collection': True,
})
ch['children'] = children
return make_json_response(
data=schema_group,
success=200
)

View File

@ -180,6 +180,7 @@ define([
gettext(data.errormsg)
);
} else {
pgBrowser.BgProcessManager.startProcess(data.data.job_id, data.data.desc);
}
},
@ -237,18 +238,43 @@ define([
let panel = pgBrowser.Node.addUtilityPanel(pgBrowser.stdW.md, pgBrowser.stdH.lg),
j = panel.$container.find('.obj_properties').first();
let schema = that.getUISchema(treeItem, 'backup_objects');
panel.title(gettext(`Backup (${pgBrowser.Nodes[data._type].label}: ${data.label})`));
panel.focus();
let backup_obj_url = '';
if (data._type == 'database') {
let did = data._id;
backup_obj_url = url_for('backup.objects', {
'sid': sid,
'did': did
});
} else if(data._type == 'schema') {
let did = data._pid;
let scid = data._id;
backup_obj_url = url_for('backup.schema_objects', {
'sid': sid,
'did': did,
'scid': scid
});
}
let typeOfDialog = 'backup_objects',
serverIdentifier = that.retrieveServerIdentifier(),
extraData = that.setExtraParameters(typeOfDialog);
api({
url: backup_obj_url,
method: 'GET'
}).then((response)=> {
let objects = response.data.data;
let schema = that.getUISchema(treeItem, 'backup_objects', objects);
panel.title(gettext(`Backup (${pgBrowser.Nodes[data._type].label}: ${data.label})`));
panel.focus();
let typeOfDialog = 'backup_objects',
serverIdentifier = that.retrieveServerIdentifier(),
extraData = that.setExtraParameters(typeOfDialog);
that.showBackupDialog(schema, treeItem, j, data, panel, typeOfDialog, serverIdentifier, extraData);
});
that.showBackupDialog(schema, treeItem, j, data, panel, typeOfDialog, serverIdentifier, extraData);
});
},
getUISchema: function(treeItem, backupType) {
getUISchema: function(treeItem, backupType, objects) {
let treeNodeInfo = pgBrowser.tree.getTreeNodeHierarchy(treeItem);
const selectedNode = pgBrowser.tree.selected();
let itemNodeData = pgBrowser.tree.findNodeByDomElement(selectedNode).getData();
@ -267,7 +293,8 @@ define([
},
treeNodeInfo,
pgBrowser,
backupType
backupType,
objects
);
},
getGlobalUISchema: function(treeItem) {

View File

@ -416,7 +416,7 @@ export function getMiscellaneousSchema(fieldOptions) {
}
export default class BackupSchema extends BaseUISchema {
constructor(sectionSchema, typeObjSchema, saveOptSchema, disabledOptionSchema, miscellaneousSchema, fieldOptions = {}, treeNodeInfo=[], pgBrowser=null, backupType='server') {
constructor(sectionSchema, typeObjSchema, saveOptSchema, disabledOptionSchema, miscellaneousSchema, fieldOptions = {}, treeNodeInfo=[], pgBrowser=null, backupType='server', objects={}) {
super({
file: undefined,
format: 'custom',
@ -431,6 +431,7 @@ export default class BackupSchema extends BaseUISchema {
...fieldOptions,
};
this.treeData = objects;
this.treeNodeInfo = treeNodeInfo;
this.pgBrowser = pgBrowser;
this.backupType = backupType;
@ -699,6 +700,42 @@ export default class BackupSchema extends BaseUISchema {
label: gettext('Miscellaneous'),
group: gettext('Options'),
schema: obj.getMiscellaneousSchema(),
},
{
id: 'object', label: gettext('Objects'), type: 'group',
visible: isVisibleForServerBackup(obj?.backupType)
},
{
id: 'objects',
label: gettext('objects'),
group: gettext('Objects'),
type: 'tree',
helpMessage: gettext('If Schema(s) is selected then it will take the backup of that selected schema(s) only'),
treeData: this.treeData,
visible: () => {
return isVisibleForServerBackup(obj?.backupType);
},
depChange: (state)=> {
let selectedNodeCollection = {
'schema': [],
'table': [],
'view': [],
'sequence': [],
'foreign table': [],
'materialized view': [],
};
state?.objects?.forEach((node)=> {
if(node.data.is_schema && !node.data?.isIndeterminate) {
selectedNodeCollection['schema'].push(node.data.name);
} else if(['table', 'view', 'materialized view', 'foreign table', 'sequence'].includes(node.data.type) &&
!node.data.is_collection && !selectedNodeCollection['schema'].includes(node.data.schema)) {
selectedNodeCollection[node.data.type].push(node.data);
}
});
return {'objects': selectedNodeCollection};
},
hasCheckbox: true,
isFullTab: true,
}];
}

View File

@ -27,7 +27,7 @@ class BackupJobTest(BaseTestGenerator):
blobs=True,
schemas=[],
tables=[],
database='postgres'
database='postgres',
),
url='/backup/job/{0}/object',
expected_params=dict(
@ -48,7 +48,7 @@ class BackupJobTest(BaseTestGenerator):
blobs=True,
schemas=[],
tables=[],
database='postgres'
database='postgres',
),
url='/backup/job/{0}/object',
expected_params=dict(
@ -60,7 +60,35 @@ class BackupJobTest(BaseTestGenerator):
server_min_version=160000,
message='--large-objects is not supported by EPAS/PG server '
'less than 16'
))
)),
('When backup selected objects ',
dict(
params=dict(
file='test_backup',
format='custom',
verbose=True,
blobs=True,
schemas=[],
tables=[],
database='postgres',
objects={
"schema": [],
"table": [
{"id": "public_test", "name": "test",
"icon": "icon-table", "schema": "public",
"type": "table", "_name": "public.test"}
],
"view": [], "sequence": [], "foreign_table": [],
"mview": []
}
),
url='/backup/job/{0}/object',
expected_params=dict(
expected_cmd_opts=['--verbose', '--format=c', '--blobs'],
not_expected_cmd_opts=[],
expected_exit_code=[1]
)
)),
]
def setUp(self):

View File

@ -270,12 +270,22 @@ def properties(sid, did, node_id, node_type):
and render into selection page of wizard
"""
res_data, msg = get_data(sid, did, node_id, node_type, server_info)
return make_json_response(
result=res_data,
info=msg,
status=200
)
def get_data(sid, did, node_id, node_type, server_data):
get_schema_sql_url = '/sql/get_schemas.sql'
# unquote encoded url parameter
node_type = unquote(node_type)
server_prop = server_info
server_prop = server_data
res_data = []
failed_objects = []
@ -350,12 +360,7 @@ def properties(sid, did, node_id, node_type):
msg = gettext('Unable to fetch the {} objects'.format(
", ".join(failed_objects))
)
return make_json_response(
result=res_data,
info=msg,
status=200
)
return res_data, msg
def get_req_data():

View File

@ -854,7 +854,6 @@ def start_query_tool(trans_id):
Args:
trans_id: unique transaction id
"""
sql = extract_sql_from_network_parameters(
request.data, request.args, request.form
)

View File

@ -109,7 +109,7 @@ export default function QueryToolComponent({params, pgWindow, pgAdmin, selectedN
fgcolor: params.fgcolor,
bgcolor: params.bgcolor,
conn_title: getTitle(
pgAdmin, null, selectedNodeInfo, true, _.unescape(params.server_name), _.unescape(params.database_name) || getDatabaseLabel(selectedNodeInfo),
pgAdmin, null, selectedNodeInfo, true, _.unescape(params.server_name), _.escape(params.database_name) || getDatabaseLabel(selectedNodeInfo),
_.unescape(params.role) || _.unescape(params.user), params.is_query_tool == 'true' ? true : false),
server_name: _.unescape(params.server_name),
database_name: _.unescape(params.database_name) || getDatabaseLabel(selectedNodeInfo),

View File

@ -106,6 +106,7 @@ class StartRunningQuery:
status = False
result = gettext(
'Either transaction object or session object not found.')
return make_json_response(
data={
'status': status, 'result': result,

View File

@ -37,7 +37,8 @@ describe('BackupSchema', ()=>{
},
{server: {version: 11000}},
pgAdmin.pgBrowser,
'backup_objects'
'backup_objects',
[]
);
it('create object backup', ()=>{
@ -61,6 +62,43 @@ describe('BackupSchema', ()=>{
});
let backupSelectedSchemaObj = new BackupSchema(
()=> getSectionSchema(),
()=> getTypeObjSchema(),
()=> getSaveOptSchema({nodeInfo: {server: {version: 11000}}}),
()=> getDisabledOptionSchema({nodeInfo: {server: {version: 11000}}}),
()=> getMiscellaneousSchema({nodeInfo: {server: {version: 11000}}}),
{
role: ()=>[],
encoding: ()=>[],
},
{server: {version: 11000}},
pgAdmin.pgBrowser,
'backup_objects',
[{'id': 'public','name': 'public','icon': 'icon-schema', 'children': [{'id': 'public_table','name': 'table','icon': 'icon-coll-table','children': [{'id': 'public_test','name': 'test','icon': 'icon-table','schema': 'public','type': 'table','_name': 'public.test'}],'type': 'table','is_collection': true}],'is_schema': true}]
);
it('create selected object backup', ()=>{
mount(<Theme>
<SchemaView
formType='dialog'
schema={backupSelectedSchemaObj}
viewHelperProps={{
mode: 'create',
}}
onSave={()=>{/*This is intentional (SonarQube)*/}}
onClose={()=>{/*This is intentional (SonarQube)*/}}
onHelp={()=>{/*This is intentional (SonarQube)*/}}
onDataChange={()=>{/*This is intentional (SonarQube)*/}}
confirmOnCloseReset={false}
hasSQL={false}
disableSqlHelp={false}
disableDialogHelp={false}
/>
</Theme>);
});
let backupServerSchemaObj = new BackupSchema(
()=> getSectionSchema(),
()=> getTypeObjSchema(),
@ -73,7 +111,8 @@ describe('BackupSchema', ()=>{
},
{server: {version: 11000}},
{serverInfo: {}},
'server'
'server',
[]
);
it('create server backup', ()=>{

View File

@ -2313,6 +2313,13 @@ __metadata:
languageName: node
linkType: hard
"@react-dnd/asap@npm:^4.0.0":
version: 4.0.1
resolution: "@react-dnd/asap@npm:4.0.1"
checksum: 757db3b5c436a95383b74f187f503321092909401ce9665d9cc1999308a44de22809bf8dbe82c9126bd73b72dd6665bbc4a788e864fc3c243c59f65057a4f87f
languageName: node
linkType: hard
"@react-dnd/asap@npm:^5.0.1":
version: 5.0.2
resolution: "@react-dnd/asap@npm:5.0.2"
@ -2320,6 +2327,13 @@ __metadata:
languageName: node
linkType: hard
"@react-dnd/invariant@npm:^2.0.0":
version: 2.0.0
resolution: "@react-dnd/invariant@npm:2.0.0"
checksum: ef1e989920d70b15c80dccb01af9b598081d76993311aa22d2e9a3ec41d10a88540eeec4b4de7a8b2a2ea52dfc3495ab45e39192c2d27795a9258bd6b79d000e
languageName: node
linkType: hard
"@react-dnd/invariant@npm:^4.0.1":
version: 4.0.2
resolution: "@react-dnd/invariant@npm:4.0.2"
@ -2327,6 +2341,13 @@ __metadata:
languageName: node
linkType: hard
"@react-dnd/shallowequal@npm:^2.0.0":
version: 2.0.0
resolution: "@react-dnd/shallowequal@npm:2.0.0"
checksum: b5bbdc795d65945bb7ba2322bed5cf8d4c6fe91dced98c3b10e3d16822c438f558751135ff296f8d1aa1eaa9d0037dacab2b522ca5eb812175123b9996966dcb
languageName: node
linkType: hard
"@react-dnd/shallowequal@npm:^4.0.1":
version: 4.0.2
resolution: "@react-dnd/shallowequal@npm:4.0.2"
@ -5855,6 +5876,17 @@ __metadata:
languageName: node
linkType: hard
"dnd-core@npm:14.0.1":
version: 14.0.1
resolution: "dnd-core@npm:14.0.1"
dependencies:
"@react-dnd/asap": ^4.0.0
"@react-dnd/invariant": ^2.0.0
redux: ^4.1.1
checksum: dbc50727f53baad1cb1e0430a2a1b81c5c291389322f90fdc46edeb2fd49cc206ce4fa30a95afb53d88238945228e34866d7465f7ea49285c296baf883551301
languageName: node
linkType: hard
"dnd-core@npm:^16.0.1":
version: 16.0.1
resolution: "dnd-core@npm:16.0.1"
@ -12275,6 +12307,22 @@ __metadata:
languageName: node
linkType: hard
"react-arborist@npm:^3.2.0":
version: 3.2.0
resolution: "react-arborist@npm:3.2.0"
dependencies:
react-dnd: ^14.0.3
react-dnd-html5-backend: ^14.0.1
react-window: ^1.8.6
redux: ^4.1.1
use-sync-external-store: ^1.2.0
peerDependencies:
react: ">= 16.14"
react-dom: ">= 16.14"
checksum: 452e8793520b4d69ad897b7bf2584a6ec763f182d6a4e942229e283fe000d579724a3c5c125504a59b66ef68ff898cef41575ccfca5e18a564c1f9e223ef62ca
languageName: node
linkType: hard
"react-aspen@npm:^1.1.0":
version: 1.2.0
resolution: "react-aspen@npm:1.2.0"
@ -12317,6 +12365,15 @@ __metadata:
languageName: node
linkType: hard
"react-dnd-html5-backend@npm:^14.0.1":
version: 14.1.0
resolution: "react-dnd-html5-backend@npm:14.1.0"
dependencies:
dnd-core: 14.0.1
checksum: 6aa8d62c6b2288893b3f216d476d2f84495b40d33578ba9e3a5051dc093a71dc59700e6927ed7ac596ff8d7aa3b3f29404f7d173f844bd6144ed633403dd8e96
languageName: node
linkType: hard
"react-dnd-html5-backend@npm:^16.0.1":
version: 16.0.1
resolution: "react-dnd-html5-backend@npm:16.0.1"
@ -12326,6 +12383,31 @@ __metadata:
languageName: node
linkType: hard
"react-dnd@npm:^14.0.3":
version: 14.0.5
resolution: "react-dnd@npm:14.0.5"
dependencies:
"@react-dnd/invariant": ^2.0.0
"@react-dnd/shallowequal": ^2.0.0
dnd-core: 14.0.1
fast-deep-equal: ^3.1.3
hoist-non-react-statics: ^3.3.2
peerDependencies:
"@types/hoist-non-react-statics": ">= 3.3.1"
"@types/node": ">= 12"
"@types/react": ">= 16"
react: ">= 16.14"
peerDependenciesMeta:
"@types/hoist-non-react-statics":
optional: true
"@types/node":
optional: true
"@types/react":
optional: true
checksum: 464e231de8c2b79546049a1600b67b1df0b7f762f23c688d3e9aeddbf334b1e64931ef91d0129df3c8be255f0af76e89426729dbcbefe4bdc09b0f665d2da368
languageName: node
linkType: hard
"react-dnd@npm:^16.0.1":
version: 16.0.1
resolution: "react-dnd@npm:16.0.1"
@ -12580,7 +12662,7 @@ __metadata:
languageName: node
linkType: hard
"react-window@npm:^1.3.1, react-window@npm:^1.8.5":
"react-window@npm:^1.3.1, react-window@npm:^1.8.5, react-window@npm:^1.8.6":
version: 1.8.9
resolution: "react-window@npm:1.8.9"
dependencies:
@ -12710,7 +12792,7 @@ __metadata:
languageName: node
linkType: hard
"redux@npm:^4.2.0":
"redux@npm:^4.1.1, redux@npm:^4.2.0":
version: 4.2.1
resolution: "redux@npm:4.2.1"
dependencies:
@ -13112,6 +13194,7 @@ __metadata:
raf: ^3.4.1
rc-dock: ^3.2.9
react: ^17.0.1
react-arborist: ^3.2.0
react-aspen: ^1.1.0
react-checkbox-tree: ^1.7.2
react-data-grid: "https://github.com/pgadmin-org/react-data-grid.git#200d2f5e02de694e3e9ffbe177c279bc40240fb8"
@ -14928,6 +15011,15 @@ __metadata:
languageName: node
linkType: hard
"use-sync-external-store@npm:^1.2.0":
version: 1.2.0
resolution: "use-sync-external-store@npm:1.2.0"
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
checksum: 5c639e0f8da3521d605f59ce5be9e094ca772bd44a4ce7322b055a6f58eeed8dda3c94cabd90c7a41fb6fa852210092008afe48f7038792fd47501f33299116a
languageName: node
linkType: hard
"util-deprecate@npm:^1.0.1, util-deprecate@npm:^1.0.2, util-deprecate@npm:~1.0.1":
version: 1.0.2
resolution: "util-deprecate@npm:1.0.2"