1) Port Grant Wizard to react. Fixes #6687

2) Fixed an issue where grant wizard is unresponsive if the database size is huge. Fixes #2097
pull/61/head
Nikhil Mohite 2021-09-20 13:02:41 +05:30 committed by Akshay Joshi
parent 44e770aa0b
commit 7aa213a5ce
19 changed files with 1175 additions and 1098 deletions

View File

@ -10,8 +10,8 @@ search box, dropdown lists, and checkboxes facilitate quick selections of
database objects, roles and privileges.
The wizard organizes privilege management through a sequence of windows:
*Object Selection (step 1 of 3)*, *Privileges Selection (step 2 of 3)* and
*Final (Review Selection) (step 3 of 3)*. The *Final (Review Selection)* window
*Object Selection*, *Privileges Selection* and
*Review Selection*. The *Review Selection* window
displays the SQL code generated by wizard selections.
To launch the *Grant Wizard* tool, select a database object in the *pgAdmin*
@ -22,7 +22,7 @@ tree control, then navigate through *Tools* on the menu bar to click on the
:alt: Grant wizard step one page
:align: center
Use the fields in the *Object Selection (step 1 of 3)* window to select the
Use the fields in the *Object Selection* window to select the
object or objects on which you are modifying privileges. Use the *Search by
object type or name* field to locate a database object, or use the scrollbar
to scroll through the list of all accessible objects.
@ -42,7 +42,7 @@ without modifying privileges.
:alt: Grant wizard step two page
:align: center
Use the fields in the *Privileges Selection (step 2 of 3)* window to grant
Use the fields in the *Privileges Selection* window to grant
privileges. If you grant a privilege WITH GRANT OPTION, the Grantee will have
the right to grant privileges on the object to others. If WITH GRANT OPTION is
subsequently revoked, any role who received access to that object from that
@ -68,7 +68,7 @@ additional database objects, or the *Cancel* button to close the wizard without
modifying privileges.
Your entries in the *Grant Wizard* tool generate a SQL command; you can review
the command in the *Final (Review Selection) (step 3 of 3)* window (see an
the command in the *Review Selection* window (see an
example below).
Example

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 80 KiB

View File

@ -16,11 +16,13 @@ Housekeeping
| `Issue #5741 <https://redmine.postgresql.org/issues/5741>`_ - Revisit all the CREATE and DROP DDL's to add appropriate 'IF EXISTS', 'CASCADE' and 'CREATE OR REPLACE'.
| `Issue #6588 <https://redmine.postgresql.org/issues/6588>`_ - Port object nodes and properties dialogs to React.
| `Issue #6687 <https://redmine.postgresql.org/issues/6687>`_ - Port Grant Wizard to react.
| `Issue #6692 <https://redmine.postgresql.org/issues/6692>`_ - Remove GPDB support completely.
Bug fixes
*********
| `Issue #2097 <https://redmine.postgresql.org/issues/2097>`_ - Fixed an issue where grant wizard is unresponsive if the database size is huge.
| `Issue #2546 <https://redmine.postgresql.org/issues/2546>`_ - Added support to create the Partitioned table using COLLATE and opclass.
| `Issue #3827 <https://redmine.postgresql.org/issues/3827>`_ - Ensure that in the Query History tab, query details should be scrollable.
| `Issue #6712 <https://redmine.postgresql.org/issues/6712>`_ - Fixed an issue where collapse and expand arrows mismatch in case of nested IF.

View File

@ -126,6 +126,8 @@
"react-dom": "^17.0.1",
"react-select": "^4.2.1",
"react-table": "^7.6.3",
"react-virtualized-auto-sizer": "^1.0.6",
"react-window": "^1.8.5",
"select2": "^4.0.13",
"shim-loader": "^1.0.1",
"slickgrid": "git+https://github.com/6pac/SlickGrid.git#2.3.16",

View File

@ -36,6 +36,9 @@ export default class PrivilegeRoleSchema extends BaseUISchema {
this.supportedPrivs = supportedPrivs || [];
}
updateSupportedPrivs = (updatedPrivs) => {
this.supportedPrivs = updatedPrivs;
}
get baseFields() {
let obj = this;
@ -65,7 +68,8 @@ export default class PrivilegeRoleSchema extends BaseUISchema {
},
{
id: 'grantor', label: gettext('Grantor'), type: 'text', readonly: true,
cell: ()=>({cell: 'select', options: obj.grantorOptions}), minWidth: 150,
editable: false, cell: ()=>({cell: 'select', options: obj.grantorOptions}),
minWidth: 150,
}];
}

View File

@ -83,3 +83,18 @@
margin-top: 20px !important;
margin-bottom: 10px !important;
}
.wizard {
width: 100%;
/*height: 550px;*/
}
.step {
height: inherit;
width: initial;
}
.wizard-footer {
border-top: 1px solid #dde0e6 !important;
padding: 0.5rem;
}

View File

@ -0,0 +1,285 @@
/* eslint-disable react/display-name */
/* eslint-disable react/prop-types */
import React from 'react';
import { useTable, useBlockLayout, useRowSelect, useSortBy, useResizeColumns, useFlexLayout, useGlobalFilter } from 'react-table';
import { FixedSizeList } from 'react-window';
import { makeStyles } from '@material-ui/core/styles';
import clsx from 'clsx';
import PropTypes from 'prop-types';
import AutoSizer from 'react-virtualized-auto-sizer';
import { Checkbox } from '@material-ui/core';
const useStyles = makeStyles((theme) => ({
root: {
display: 'flex',
flexDirection: 'column',
height: '100%',
...theme.mixins.panelBorder,
backgroundColor: theme.palette.background.default,
},
autoResizer: {
height: '100% !important',
width: '100% !important',
},
table: {
borderSpacing: 0,
width: '100%',
overflow: 'hidden',
height: '99.7%',
backgroundColor: theme.otherVars.tableBg,
...theme.mixins.panelBorder,
//backgroundColor: theme.palette.background.default,
},
tableCell: {
margin: 0,
padding: theme.spacing(0.5),
...theme.mixins.panelBorder.bottom,
...theme.mixins.panelBorder.right,
position: 'relative',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
},
selectCell: {
textAlign: 'center'
},
tableCellHeader: {
fontWeight: theme.typography.fontWeightBold,
padding: theme.spacing(1, 0.5),
textAlign: 'left',
overflowY: 'auto',
overflowX: 'hidden',
alignContent: 'center',
...theme.mixins.panelBorder.bottom,
...theme.mixins.panelBorder.right,
},
resizer: {
display: 'inline-block',
width: '5px',
height: '100%',
position: 'absolute',
right: 0,
top: 0,
transform: 'translateX(50%)',
zIndex: 1,
touchAction: 'none',
},
cellIcon: {
paddingLeft: '1.5em',
height: 35
}
}),
);
export default function pgTable({ columns, data, isSelectRow, ...props }) {
// Use the state and functions returned from useTable to build your UI
const classes = useStyles();
const defaultColumn = React.useMemo(
() => ({
minWidth: 150,
}),
[]
);
const scrollBarSize = React.useMemo(() => scrollbarWidth(), []);
const IndeterminateCheckbox = React.forwardRef(
({ indeterminate, ...rest }, ref) => {
const defaultRef = React.useRef();
const resolvedRef = ref || defaultRef;
React.useEffect(() => {
resolvedRef.current.indeterminate = indeterminate;
}, [resolvedRef, indeterminate]);
return (
<>
<Checkbox
color="primary"
ref={resolvedRef} {...rest} />
</>
);
},
);
IndeterminateCheckbox.displayName = 'SelectCheckbox';
IndeterminateCheckbox.propTypes = {
indeterminate: PropTypes.bool,
rest: PropTypes.func,
getToggleAllRowsSelectedProps: PropTypes.func,
row: PropTypes.object,
};
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
totalColumnsWidth,
prepareRow,
selectedFlatRows,
state: { selectedRowIds },
setGlobalFilter
} = useTable(
{
columns,
data,
defaultColumn,
isSelectRow,
},
useBlockLayout,
useGlobalFilter,
useSortBy,
useRowSelect,
useResizeColumns,
useFlexLayout,
hooks => {
hooks.visibleColumns.push(CLOUMNS => {
if (isSelectRow) {
return [
// Let's make a column for selection
{
id: 'selection',
// The header can use the table's getToggleAllRowsSelectedProps method
// to render a checkbox
Header: ({ getToggleAllRowsSelectedProps }) => (
<div className={classes.selectCell}>
<IndeterminateCheckbox {...getToggleAllRowsSelectedProps()} />
</div>
),
// The cell can use the individual row's getToggleRowSelectedProps method
// to the render a checkbox
Cell: ({ row }) => (
<div className={classes.selectCell}>
<IndeterminateCheckbox {...row.getToggleRowSelectedProps()} />
</div>
),
sortble: false,
width: 50,
minWidth: 0,
},
...CLOUMNS,
];
} else {
return [...CLOUMNS];
}
});
hooks.useInstanceBeforeDimensions.push(({ headerGroups }) => {
// fix the parent group of the selection button to not be resizable
const selectionGroupHeader = headerGroups[0].headers[0];
selectionGroupHeader.resizable = false;
});
}
);
React.useEffect(() => {
if (props.setSelectedRows) {
props.setSelectedRows(selectedFlatRows);
}
}, [selectedRowIds]);
React.useEffect(() => {
if (props.getSelectedRows) {
props.getSelectedRows(selectedFlatRows);
}
}, [selectedRowIds]);
React.useEffect(() => {
setGlobalFilter(props.searchText || undefined);
}, [props.searchText]);
const RenderRow = React.useCallback(
({ index, style }) => {
const row = rows[index];
prepareRow(row);
return (
<div
{...row.getRowProps({
style,
})}
className={classes.tr}
>
{row.cells.map((cell) => {
return (
<div key={cell.column.id} {...cell.getCellProps()} className={clsx(classes.tableCell, row.original.icon && row.original.icon[cell.column.id], row.original.icon[cell.column.id] && classes.cellIcon)} title={cell.value}>
{cell.render('Cell')}
</div>
);
})}
</div>
);
},
[prepareRow, rows, selectedRowIds]
);
// Render the UI for your table
return (
<AutoSizer className={classes.autoResizer}>
{({ height }) => (
<div {...getTableProps()} className={classes.table}>
<div>
{headerGroups.map(headerGroup => (
<div key={''} {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
<div key={column.id} {...column.getHeaderProps()} className={clsx(classes.tableCellHeader, column.className)}>
<div {...(column.sortble ? column.getSortByToggleProps() : {})}>
{column.render('Header')}
<span>
{column.isSorted
? column.isSortedDesc
? ' 🔽'
: ' 🔼'
: ''}
</span>
{column.resizable &&
<div
{...column.getResizerProps()}
className={classes.resizer}
/>}
</div>
</div>
))}
</div>
))}
</div>
<div {...getTableBodyProps()}>
<FixedSizeList
height={height - 75}
itemCount={rows.length}
itemSize={35}
width={rows.length * 35 > 385 ? totalColumnsWidth + scrollBarSize : totalColumnsWidth}
sorted={props?.sortOptions}
>
{RenderRow}
</FixedSizeList>
</div>
</div>
)}
</AutoSizer>
);
}
const scrollbarWidth = () => {
// thanks too https://davidwalsh.name/detect-scrollbar-width
const scrollDiv = document.createElement('div');
scrollDiv.setAttribute('style', 'width: 100px; height: 100px; overflow: scroll; position:absolute; top:-9999px;');
document.body.appendChild(scrollDiv);
const scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth;
document.body.removeChild(scrollDiv);
return scrollbarWidth;
};
pgTable.propTypes = {
stepId: PropTypes.number,
height: PropTypes.number,
className: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]),
getToggleAllRowsSelectedProps: PropTypes.func,
};

View File

@ -0,0 +1,44 @@
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import { Box } from '@material-ui/core';
import clsx from 'clsx';
import PropTypes from 'prop-types';
const useStyles = makeStyles(() =>
({
stepPanel: {
height: '100%',
width: '100%',
// paddingLeft: '2em',
minHeight: '100px',
// paddingTop: '1em',
paddingBottom: '1em',
paddingRight: '1em',
overflow: 'auto',
}
}));
export default function WizardStep({stepId, className, ...props }) {
const classes = useStyles();
return (
<Box id={stepId} className={clsx(classes.stepPanel, className)} style={props?.height ? {height: props.height} : null}>
{
React.Children.map(props.children, (child) => {
return (
<>
{child}
</>
);
})
}
</Box>
);
}
WizardStep.propTypes = {
stepId: PropTypes.number,
height: PropTypes.number,
className: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]),
};

View File

@ -0,0 +1,196 @@
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import clsx from 'clsx';
import FastForwardIcon from '@material-ui/icons/FastForward';
import FastRewindIcon from '@material-ui/icons/FastRewind';
import ChevronRightIcon from '@material-ui/icons/ChevronRight';
import HelpIcon from '@material-ui/icons/HelpRounded';
import CheckIcon from '@material-ui/icons/Check';
import { DefaultButton, PrimaryButton, PgIconButton } from '../../../static/js/components/Buttons';
import PropTypes from 'prop-types';
import { Box } from '@material-ui/core';
import gettext from 'sources/gettext';
const useStyles = makeStyles((theme) =>
({
root: {
display: 'flex',
flexDirection: 'column',
height: '100%',
},
rightPanel: {
position: 'relative',
minHeight: 100,
display: 'flex',
paddingLeft: '1.5em',
paddingTop: '0em',
flex: 5,
overflow: 'auto',
height: '100%',
},
leftPanel: {
display: 'flex',
// padding: '2em',
flexDirection: 'column',
alignItems: 'flex-start',
borderRight: '1px solid',
...theme.mixins.panelBorder.right,
flex: 1.6
},
label: {
display: 'inline-block',
position: 'relative',
paddingLeft: '0.5em',
flex: 6
},
labelArrow: {
display: 'inline-block',
position: 'relative',
flex: 1
},
stepLabel: {
padding: '1em',
},
active: {
fontWeight: 600
},
activeIndex: {
backgroundColor: '#326690 !important',
color: '#fff'
},
stepIndex: {
padding: '0.5em 1em ',
height: '2.5em',
borderRadius: '2em',
backgroundColor: '#ddd',
display: 'inline-block',
flex: 0.5,
},
wizard: {
width: '100%',
height: '100%',
minHeight: 100,
display: 'flex',
flexWrap: 'wrap',
},
wizardFooter: {
borderTop: '1px solid #dde0e6 !important',
padding: '0.5rem',
display: 'flex',
flexDirection: 'row',
flex: 1
},
backButton: {
marginRight: theme.spacing(1),
},
instructions: {
marginTop: theme.spacing(1),
marginBottom: theme.spacing(1),
},
actionBtn: {
alignItems: 'flex-start',
},
buttonMargin: {
marginLeft: '0.5em'
},
stepDefaultStyle: {
width: '100%',
height: '100%'
}
}),
);
function Wizard({ stepList, onStepChange, onSave, className, ...props }) {
const classes = useStyles();
const [activeStep, setActiveStep] = React.useState(0);
const steps = stepList && stepList.length > 0 ? stepList : [];
const [disableNext, setdisableNext] = React.useState(false);
const handleNext = () => {
setActiveStep((prevActiveStep) => prevActiveStep + 1);
};
const handleBack = () => {
setActiveStep((prevActiveStep) => prevActiveStep - 1 < 0 ? prevActiveStep : prevActiveStep - 1);
};
React.useEffect(() => {
if (onStepChange) {
onStepChange({ currentStep: activeStep });
}
}, [activeStep]);
React.useEffect(() => {
if (props.disableNextStep) {
setdisableNext(props.disableNextStep());
}
});
return (
<div className={clsx(classes.root, props?.rootClass)}>
<div className={clsx(classes.wizard, className)}>
<Box className={classes.leftPanel}>
{steps.map((label, index) => (
<Box key={label} className={clsx(classes.stepLabel, index === activeStep ? classes.active : '')}>
<Box className={clsx(classes.stepIndex, index === activeStep ? classes.activeIndex : '')}>{index + 1}</Box>
<Box className={classes.label}>{label} </Box>
<Box className={classes.labelArrow}>{index === activeStep ? <ChevronRightIcon /> : null}</Box>
</Box>
))}
</Box>
<div className={clsx(classes.rightPanel, props.stepPanelCss)}>
{
React.Children.map(props.children, (child) => {
return (
<div hidden={child.props.stepId !== activeStep} className={clsx(child.props.className, classes.stepDefaultStyle)}>
{child}
</div>
);
})
}
</div>
</div>
<div className={classes.wizardFooter}>
<Box >
<PgIconButton data-test="dialog-help" onClick={() => props.onHelp()} icon={<HelpIcon />} title="Help for this dialog."
disabled={props.disableDialogHelp} />
</Box>
<Box className={classes.actionBtn} marginLeft="auto">
<DefaultButton onClick={handleBack} disabled={activeStep === 0} className={classes.buttonMargin} startIcon={<FastRewindIcon />}>
{gettext('Back')}
</DefaultButton>
<DefaultButton onClick={() => handleNext()} className={classes.buttonMargin} startIcon={<FastForwardIcon />} disabled={activeStep == steps.length - 1 || disableNext}>
{gettext('Next')}
</DefaultButton>
<PrimaryButton className={classes.buttonMargin} startIcon={<CheckIcon />} disabled={activeStep == steps.length - 1 ? false : true} onClick={onSave}>
{gettext('Finish')}
</PrimaryButton>
</Box>
</div>
</div>
);
}
export default Wizard;
Wizard.propTypes = {
props: PropTypes.object,
stepList: PropTypes.array,
onSave: PropTypes.func,
onHelp: PropTypes.func,
onStepChange: PropTypes.func,
className: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
disableNextStep: PropTypes.func,
stepPanelCss: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
rootClass: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]),
disableDialogHelp: PropTypes.bool
};

View File

View File

@ -0,0 +1,383 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import gettext from 'sources/gettext';
import _ from 'lodash';
import url_for from 'sources/url_for';
import React from 'react';
import { Box } from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
import Wizard from '../../../../browser/static/js/WizardView';
import WizardStep from '../../../../browser/static/js/WizardStep';
import PgTable from '../../../../browser/static/js/PgTable';
import { getNodePrivilegeRoleSchema } from '../../../../browser/server_groups/servers/static/js/privilege.ui.js';
import { InputSQL, InputText, FormFooterMessage, MESSAGE_TYPE } from '../../../../static/js/components/FormComponents';
import getApiInstance from '../../../../static/js/api_instance';
import SchemaView from '../../../../static/js/SchemaView';
import clsx from 'clsx';
import Loader from 'sources/components/Loader';
import Alertify from 'pgadmin.alertifyjs';
import PropTypes from 'prop-types';
import PrivilegeSchema from './privilege_schema.ui';
const useStyles = makeStyles(() =>
({
grantWizardStep: {
height: '100%'
},
grantWizardTitle: {
top: '0 !important',
opacity: '1 !important',
borderRadius: '6px 6px 0px 0px !important',
margin: '0 !important',
width: '100%',
height: '6%'
},
grantWizardContent: {
height: '94% !important'
},
stepPanelCss: {
height: 500,
overflow: 'hidden'
},
objectSelection: {
display: 'flex',
flexDirection: 'column',
height: '100%',
overflow: 'hidden',
marginBottom: '1em'
},
searchBox: {
marginBottom: '1em',
display: 'flex',
},
table: {
},
searchPadding: {
flex: 2.5
},
searchInput: {
flex: 1,
marginTop: 2,
borderLeft: 'none',
paddingLeft: 5
},
grantWizardPanelContent: {
paddingTop: '0.9em !important',
overflow: 'hidden'
},
grantWizardSql: {
height: '90% !important',
width: '100%'
},
privilegeStep: {
height: '100%',
}
}),
);
export default function GrantWizard({ sid, did, nodeInfo, nodeData }) {
const classes = useStyles();
var columns = [
{
Header: 'Object Type',
accessor: 'object_type',
sortble: true,
resizable: false,
disableGlobalFilter: true
},
{
Header: 'Schema',
accessor: 'nspname',
sortble: true,
resizable: false,
disableGlobalFilter: true
},
{
Header: 'Name',
accessor: 'name',
sortble: true,
resizable: true,
disableGlobalFilter: false,
minWidth: 280
}
];
var steps = ['Object Selection', 'Privilege Selection', 'Review Selection'];
const [selectedObject, setSelectedObject] = React.useState([]);
const [selectedAcl, setSelectedAcl] = React.useState({});
const [msqlData, setSQL] = React.useState('');
const [stepType, setStepType] = React.useState('');
const [searchVal, setSearchVal] = React.useState('');
const [loaderText, setLoaderText] = React.useState('');
const [tablebData, setTableData] = React.useState([]);
const [privOptions, setPrivOptions] = React.useState({});
const [privileges, setPrivileges] = React.useState([]);
const [privSchemaInstance, setPrivSchemaInstance] = React.useState();
const [errMsg, setErrMsg] = React.useState('');
const api = getApiInstance();
const validatePrivilege = () => {
var isValid = true;
selectedAcl.privilege.forEach((priv) => {
if ((_.isUndefined(priv.grantee) || _.isUndefined(priv.privileges) || priv.privileges.length === 0) && isValid) {
isValid = false;
}
});
return !isValid;
};
React.useEffect(() => {
privSchemaInstance?.privilegeRoleSchema.updateSupportedPrivs(privileges);
}, [privileges]);
React.useEffect(() => {
const privSchema = new PrivilegeSchema((privileges) => getNodePrivilegeRoleSchema('', nodeInfo, nodeData, privileges));
setPrivSchemaInstance(privSchema);
setLoaderText('Loading...');
api.get(url_for(
'grant_wizard.acl', {
'sid': encodeURI(sid),
'did': encodeURI(did),
}
)).then(res => {
setPrivOptions(res.data);
});
var node_type = nodeData._type.replace('coll-', '').replace(
'materialized_', ''
);
var _url = url_for(
'grant_wizard.objects', {
'sid': encodeURI(sid),
'did': encodeURI(did),
'node_id': encodeURI(nodeData._id),
'node_type': encodeURI(node_type),
});
api.get(_url)
.then(res => {
var data = res.data.result;
data.forEach(element => {
if (element.icon)
element['icon'] = {
'object_type': element.icon
};
});
setTableData(data);
setLoaderText('');
})
.catch(() => {
Alertify.error(gettext('Error while fetching grant wizard data.'));
setLoaderText('');
});
}, [nodeData]);
const wizardStepChange = (data) => {
switch (data.currentStep) {
case 0:
setStepType('object_type');
break;
case 1:
setStepType('privileges');
break;
case 2:
setLoaderText('Loading SQL ...');
var msql_url = url_for(
'grant_wizard.modified_sql', {
'sid': encodeURI(sid),
'did': encodeURI(did),
});
var post_data = {
acl: selectedAcl.privilege,
objects: selectedObject
};
api.post(msql_url, post_data)
.then(res => {
setSQL(res.data.data);
setLoaderText('');
})
.catch(() => {
Alertify.error(gettext('Error while fetching SQL.'));
});
break;
default:
setStepType('');
}
};
const onSave = () => {
setLoaderText('Saving...');
var _url = url_for(
'grant_wizard.apply', {
'sid': encodeURI(sid),
'did': encodeURI(did),
});
const post_data = {
acl: selectedAcl.privilege,
objects: selectedObject
};
api.post(_url, post_data)
.then(() => {
setLoaderText('');
Alertify.wizardDialog().close();
})
.catch(() => {
setLoaderText('');
Alertify.error(gettext('Error while saving grant wizard data.'));
});
};
const disableNextCheck = () => {
return selectedObject.length > 0 && stepType === 'object_type' ?
false : selectedAcl?.privilege?.length > 0 && stepType === 'privileges' ? validatePrivilege() : true;
};
const onDialogHelp= () => {
window.open(url_for('help.static', { 'filename': 'grant_wizard.html' }), 'pgadmin_help');
};
const getTableSelectedRows = (selRows) => {
var selObj = [];
var objectTypes = new Set();
if (selRows.length > 0) {
selRows.forEach((row) => {
var object_type = '';
switch (row.values.object_type) {
case 'Function':
object_type = 'function';
break;
case 'Trigger Function':
object_type = 'function';
break;
case 'Procedure':
object_type = 'procedure';
break;
case 'Table':
object_type = 'table';
break;
case 'Sequence':
object_type = 'sequence';
break;
case 'View':
object_type = 'table';
break;
case 'Materialized View':
object_type = 'table';
break;
case 'Foreign Table':
object_type = 'foreign_table';
break;
case 'Package':
object_type = 'package';
break;
default:
break;
}
objectTypes.add(object_type);
selObj.push(row.values);
});
}
var privileges = new Set();
objectTypes.forEach((objType) => {
privOptions[objType]?.acl.forEach((priv) => {
privileges.add(priv);
});
});
setPrivileges(Array.from(privileges));
setSelectedObject(selObj);
setErrMsg(selObj.length === 0 ? gettext('Please select any database object.') : '');
};
const onErrClose = React.useCallback(()=>{
setErrMsg('');
});
return (
<>
<Box className={clsx('wizard-header', classes.grantWizardTitle)}>{gettext('Grant Wizard')}</Box>
<Loader message={loaderText} />
<Wizard
stepList={steps}
rootClass={clsx(classes.grantWizardContent)}
stepPanelCss={classes.grantWizardPanelContent}
disableNextStep={disableNextCheck}
onStepChange={wizardStepChange}
onSave={onSave}
onHelp={onDialogHelp}
>
<WizardStep
stepId={0}
className={clsx(classes.objectSelection, classes.grantWizardStep, classes.stepPanelCss)} >
<Box className={classes.searchBox}>
<Box className={classes.searchPadding}></Box>
<InputText
placeholder={'Search'}
className={classes.searchInput}
value={searchVal}
onChange={(val) => {
setSearchVal(val);}
}>
</InputText>
</Box>
<PgTable
className={classes.table}
height={window.innerHeight - 450}
columns={columns}
data={tablebData}
isSelectRow={true}
searchText={searchVal}
getSelectedRows={getTableSelectedRows}>
</PgTable>
<FormFooterMessage type={MESSAGE_TYPE.ERROR} message={errMsg} onClose={onErrClose} />
</WizardStep>
<WizardStep
stepId={1}
className={clsx(classes.grantWizardStep, classes.privilegeStep)}>
{privSchemaInstance &&
<SchemaView
formType={'dialog'}
getInitData={() => { }}
viewHelperProps={{ mode: 'create' }}
schema={privSchemaInstance}
showFooter={false}
isTabView={false}
onDataChange={(isChanged, changedData) => {
setSelectedAcl(changedData);
}}
/>
}
</WizardStep>
<WizardStep
stepId={2}
className={classes.grantWizardStep}>
<Box>{gettext('The SQL below will be executed on the database server to grant the selected privileges. Please click on Finish to complete the process.')}</Box>
<InputSQL
onLable={true}
className={classes.grantWizardSql}
readonly={true}
value={msqlData.toString()} />
</WizardStep>
</Wizard>
</>
);
}
GrantWizard.propTypes = {
sid: PropTypes.string,
did: PropTypes.number,
nodeInfo: PropTypes.object,
nodeData: PropTypes.object,
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,34 @@
import gettext from 'sources/gettext';
import BaseUISchema from 'sources/SchemaView/base_schema.ui';
export default class PrivilegeSchema extends BaseUISchema {
constructor(getPrivilegeRoleSchema, fieldOptions = {}, initValues) {
super({
oid: null,
privilege: [],
...initValues
});
this.privilegeRoleSchema = getPrivilegeRoleSchema([]);
this.fieldOptions = {
...fieldOptions,
};
}
get idAttribute() {
return 'oid';
}
get baseFields() {
return [
{
id: 'privilege', label: gettext('Privileges'), type: 'collection',
schema: this.privilegeRoleSchema,
uniqueCol: ['grantee'],
editable: false, mode: ['create'],
canAdd: true, canDelete: true,
}
];
}
}

View File

@ -130,3 +130,17 @@
width: 100%;
height: 100%;
}
#grantWizardDlg {
padding-top: 0em;
}
.grant-wizard-objcol {
min-width: 8em !important;
width: 8em !important;
}
.grant-wizard-panel-content {
padding-top: 0.9em !important;
}

View File

@ -0,0 +1,58 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import jasmineEnzyme from 'jasmine-enzyme';
import React from 'react';
import '../helper/enzyme.helper';
import { createMount } from '@material-ui/core/test-utils';
import pgAdmin from 'sources/pgadmin';
import { messages } from '../fake_messages';
// import GrantWizard from '../../../pgadmin/tools/grant_wizard/static/js/grant_wizard_view';
import Theme from 'sources/Theme';
import Wizard from '../../../pgadmin/browser/static/js/WizardView';
import WizardStep from '../../../pgadmin/browser/static/js/WizardStep';
describe('Wizard', () => {
let mount;
/* Use createMount so that material ui components gets the required context */
/* https://material-ui.com/guides/testing/#api */
beforeAll(() => {
mount = createMount();
});
afterAll(() => {
mount.cleanUp();
});
beforeEach(() => {
jasmineEnzyme();
/* messages used by validators */
pgAdmin.Browser = pgAdmin.Browser || {};
pgAdmin.Browser.messages = pgAdmin.Browser.messages || messages;
pgAdmin.Browser.utils = pgAdmin.Browser.utils || {};
});
it('WizardPanel', () => {
mount(
<Theme>
<Wizard
stepList={['Test']}
onStepChange={()=> {}}
onSave={()=>{}}
className={''}
disableNextStep={()=>{return false;}}
>
<WizardStep stepId={0} className={''}>
</WizardStep>
</Wizard>
</Theme>);
});
});

View File

@ -0,0 +1,64 @@
/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2021, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import jasmineEnzyme from 'jasmine-enzyme';
import React from 'react';
import '../helper/enzyme.helper';
import { createMount } from '@material-ui/core/test-utils';
import pgAdmin from 'sources/pgadmin';
import { messages } from '../fake_messages';
import SchemaView from '../../../pgadmin/static/js/SchemaView';
import BaseUISchema from 'sources/SchemaView/base_schema.ui';
import GrantWizardPrivilegeSchema from '../../../pgadmin/tools/grant_wizard/static/js/privilege_schema.ui';
class MockSchema extends BaseUISchema {
get baseFields() {
return [];
}
}
describe('GrantWizard', () => {
let mount;
let schemaObj = new GrantWizardPrivilegeSchema(
() => new MockSchema(),
);
/* Use createMount so that material ui components gets the required context */
/* https://material-ui.com/guides/testing/#api */
beforeAll(() => {
mount = createMount();
});
afterAll(() => {
mount.cleanUp();
});
beforeEach(() => {
jasmineEnzyme();
/* messages used by validators */
pgAdmin.Browser = pgAdmin.Browser || {};
pgAdmin.Browser.messages = pgAdmin.Browser.messages || messages;
pgAdmin.Browser.utils = pgAdmin.Browser.utils || {};
});
it('create', () => {
mount(<SchemaView
formType='dialog'
schema={schemaObj}
viewHelperProps={{
mode: 'create',
}}
onDataChange={() => { }}
showFooter={false}
isTabView={false}
/>);
});
});

View File

@ -870,6 +870,13 @@
"@babel/plugin-transform-react-jsx-development" "^7.12.17"
"@babel/plugin-transform-react-pure-annotations" "^7.12.1"
"@babel/runtime@^7.0.0":
version "7.15.4"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.15.4.tgz#fd17d16bfdf878e6dd02d19753a39fa8a8d9c84a"
integrity sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw==
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.12.0", "@babel/runtime@^7.13.10":
version "7.14.0"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.0.tgz#46794bc20b612c5f75e62dd071e24dfd95f1cbe6"
@ -6425,7 +6432,7 @@ media-typer@0.3.0:
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
memoize-one@^5.0.0:
"memoize-one@>=3.1.1 <6", memoize-one@^5.0.0:
version "5.2.1"
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e"
integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==
@ -7857,6 +7864,19 @@ react-transition-group@^4.0.0, react-transition-group@^4.3.0, react-transition-g
loose-envify "^1.4.0"
prop-types "^15.6.2"
react-virtualized-auto-sizer@^1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.6.tgz#66c5b1c9278064c5ef1699ed40a29c11518f97ca"
integrity sha512-7tQ0BmZqfVF6YYEWcIGuoR3OdYe8I/ZFbNclFlGOC3pMqunkYF/oL30NCjSGl9sMEb17AnzixDz98Kqc3N76HQ==
react-window@^1.8.5:
version "1.8.6"
resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.6.tgz#d011950ac643a994118632665aad0c6382e2a112"
integrity sha512-8VwEEYyjz6DCnGBsd+MgkD0KJ2/OXFULyDtorIiTz+QzwoP94tBoA7CnbtyXMm+cCeAUER5KJcPtWl9cpKbOBg==
dependencies:
"@babel/runtime" "^7.0.0"
memoize-one ">=3.1.1 <6"
react@^17.0.1:
version "17.0.2"
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"