mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2025-02-25 18:55:31 -06:00
Added support to select/deselect objects in the Backup dialog. #642
This commit is contained in:
parent
8c91d40932
commit
aa973fc8ae
@ -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 you’ve specified the details that will be incorporated into the pg_dump
|
||||
command:
|
||||
|
||||
|
BIN
docs/en_US/images/backup_object_selection.png
Normal file
BIN
docs/en_US/images/backup_object_selection.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 126 KiB |
@ -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",
|
||||
|
268
web/pgadmin/static/js/PgTreeView/index.jsx
Normal file
268
web/pgadmin/static/js/PgTreeView/index.jsx
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
@ -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)}
|
||||
|
@ -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 = [
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
}];
|
||||
}
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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():
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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),
|
||||
|
@ -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,
|
||||
|
@ -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', ()=>{
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user