console: add the ability to delete a role in permissions summary page (close #3353) (#4987)

This commit is contained in:
Sameer Kolhar 2020-06-19 14:36:40 +05:30 committed by GitHub
parent a7a60c2dfe
commit 4293714519
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 432 additions and 258 deletions

View File

@ -8,6 +8,7 @@
(Add entries here in the order of: server, console, cli, docs, others)
- console: allow manual edit of column types and handle array data types (close #2544, #3335, #2583) (#4546)
- console: add the ability to delete a role in permissions summary page (close #3353) (#4987)
## `v1.3.0-beta.2`

View File

@ -532,6 +532,7 @@ const makeMigrationCall = (
successMsg,
errorMsg,
shouldSkipSchemaReload,
skipExecution = false,
isRetry
) => {
const upQuery = {
@ -548,6 +549,7 @@ const makeMigrationCall = (
name: sanitize(migrationName),
up: upQuery.args,
down: downQuery.args,
skip_execution: skipExecution,
};
const currMigrationMode = getState().main.migrationMode;

View File

@ -0,0 +1,60 @@
import React, { ReactElement } from 'react';
import styles from './PermissionsSummary.scss';
type HeaderContentProps = {
content: string;
actionButtons: Array<ReactElement>;
};
const HeaderContent: React.FC<HeaderContentProps> = ({
content,
actionButtons,
}) => {
return actionButtons.length ? (
<div
className={`${styles.actionCell} ${styles.display_flex} ${styles.flex_space_between}`}
>
<div>{content}</div>
<div className={`${styles.tableHeaderActions} ${styles.display_flex}`}>
{actionButtons.map((actionButton, i) => (
<div key={`${content}-action-btn-${i}`}>{actionButton}</div>
))}
</div>
</div>
) : (
<>{content}</>
);
};
type HeaderProps = {
content: string;
selectable: boolean;
isSelected?: boolean;
onClick?: () => void;
actionButtons?: Array<ReactElement>;
key?: string | null;
};
const Header: React.FC<HeaderProps> = ({
content,
selectable,
isSelected,
onClick,
actionButtons = [],
key,
}) => {
const selectableClassName = selectable ? styles.cursorPointer : '';
const isSelectedClassName = isSelected ? styles.selected : '';
return (
<th
key={key || content}
onClick={selectable ? onClick : undefined}
className={`${selectableClassName} ${isSelectedClassName}`}
>
<HeaderContent content={content} actionButtons={actionButtons} />
</th>
);
};
export default Header;

View File

@ -23,7 +23,11 @@ import {
import { getConfirmation } from '../../../Common/utils/jsUtils';
import { updateSchemaInfo } from '../DataActions';
import { copyRolePermissions, permOpenEdit } from '../TablePermissions/Actions';
import {
copyRolePermissions,
permOpenEdit,
deleteRoleGlobally,
} from '../TablePermissions/Actions';
import {
getAllRoles,
@ -33,6 +37,9 @@ import {
getPermissionRowAccessSummary,
} from './utils';
import Header from './Header';
import RolesHeader from './RolesHeader';
class PermissionsSummary extends Component {
initState = {
currRole: null,
@ -182,52 +189,6 @@ class PermissionsSummary extends Component {
);
};
const getHeader = (
content,
selectable,
isSelected,
onClick,
actionBtn = null,
key = null
) => {
const getContents = () => {
let headerContent;
if (!actionBtn) {
headerContent = content;
} else {
headerContent = (
<div
className={
styles.actionCell +
' ' +
styles.display_flex +
' ' +
styles.flex_space_between
}
>
<div>{content}</div>
<div>{actionBtn}</div>
</div>
);
}
return headerContent;
};
return (
<th
key={key || content}
onClick={selectable ? onClick : null}
className={`${selectable ? styles.cursorPointer : ''} ${
isSelected ? styles.selected : ''
}`}
>
{getContents()}
</th>
);
};
const getCellOnClick = (table, role, action) => {
return () => {
dispatch(
@ -247,66 +208,46 @@ class PermissionsSummary extends Component {
};
};
const getRolesHeaders = (selectable = true, selectedFirst = false) => {
const rolesHeaders = [];
const copyOnClick = (e, role) => {
e.preventDefault();
e.stopPropagation();
if (!allRoles.length) {
rolesHeaders.push(getHeader('No roles', false));
} else {
allRoles.forEach(role => {
const isCurrRole = currRole === role;
this.setState({
copyState: {
...copyState,
copyFromRole: role,
copyFromTable: currTable ? getTableNameWithSchema(currTable) : 'all',
copyFromAction: currRole ? 'all' : currAction,
},
});
};
const setRole = () => {
this.setState({ currRole: isCurrRole ? null : role });
window.scrollTo(0, 0);
};
const deleteOnClick = (e, role) => {
e.preventDefault();
e.stopPropagation();
const getCopyBtn = () => {
const copyOnClick = e => {
e.preventDefault();
e.stopPropagation();
const deleteConfirmed = getConfirmation(
`This will delete all permissions for the role: "${role}" for all entities in the current Postgres schema`,
true,
role
);
this.setState({
copyState: {
...copyState,
copyFromRole: role,
copyFromTable: currTable
? getTableNameWithSchema(currTable)
: 'all',
copyFromAction: currRole ? 'all' : currAction,
},
});
};
return (
<Button
color="white"
size="xs"
onClick={copyOnClick}
title="Copy permissions"
>
{getActionIcon('fa-copy')}
</Button>
);
};
const roleHeader = getHeader(
role,
selectable,
isCurrRole,
setRole,
getCopyBtn()
);
if (selectedFirst && isCurrRole) {
rolesHeaders.unshift(roleHeader);
} else {
rolesHeaders.push(roleHeader);
}
});
if (deleteConfirmed) {
dispatch(deleteRoleGlobally(role));
}
};
return rolesHeaders;
const setRole = (role, isCurrRole) => {
this.setState({ currRole: isCurrRole ? null : role });
window.scrollTo(0, 0);
};
const defaultRolesHeaderProps = {
allRoles,
currentRole: currRole,
onCopyClick: copyOnClick,
onDeleteClick: deleteOnClick,
setRole,
};
const getRolesCells = (table, roleCellRenderer) => {
@ -366,7 +307,9 @@ class PermissionsSummary extends Component {
if (!currSchemaTrackedTables.length) {
tablesRows.push(
<tr key={'No tables'}>{getHeader('No tables', false)}</tr>
<tr key={'No tables'}>
<Header content="No tables" selectable={false} />
</tr>
);
} else {
currSchemaTrackedTables.forEach((table, i) => {
@ -385,13 +328,15 @@ class PermissionsSummary extends Component {
});
};
return getHeader(
displayTableName(table),
selectable,
isCurrTable,
setTable,
null,
tableName
return (
<Header
content={displayTableName(table)}
selectable={selectable}
isSelected={isCurrTable}
onClick={setTable}
actionButtons={[]}
key={tableName}
/>
);
};
@ -631,7 +576,7 @@ class PermissionsSummary extends Component {
<thead>
<tr>
{getActionSelector()}
{getRolesHeaders(false)}
<RolesHeader selectable={false} {...defaultRolesHeaderProps} />
</tr>
</thead>
<tbody>
@ -656,7 +601,11 @@ class PermissionsSummary extends Component {
return (
<tr>
{getBackBtn('currRole')}
{getRolesHeaders(true, true)}
<RolesHeader
selectable
selectedFirst
{...defaultRolesHeaderProps}
/>
</tr>
);
};
@ -715,7 +664,7 @@ class PermissionsSummary extends Component {
return (
<tr>
{getActionSelector()}
{getRolesHeaders()}
<RolesHeader {...defaultRolesHeaderProps} />
</tr>
);
};

View File

@ -1,4 +1,4 @@
@import "../../../Common/Common";
@import '../../../Common/Common';
//.rolesTableWrapper {
//}
@ -6,7 +6,8 @@
.rolesTable {
width: fit-content;
th, td {
th,
td {
vertical-align: middle !important;
}
@ -25,7 +26,7 @@
.selected,
.selected th {
background-color: #FFF3D5 !important;
background-color: #fff3d5 !important;
}
.permissionSymbolNA {
@ -71,3 +72,8 @@
width: calc(100% - 20px);
}
}
.tableHeaderActions {
width: 60px;
justify-content: space-evenly;
}

View File

@ -0,0 +1,75 @@
import React from 'react';
import Button from '../../../Common/Button/Button';
import Header from './Header';
import styles from './PermissionsSummary.scss';
type IconButtonProps = {
onClick: (e: React.MouseEvent<HTMLButtonElement>) => void;
icon: string;
title: string;
};
const IconButton: React.FC<IconButtonProps> = ({ onClick, icon, title }) => (
<Button color="white" size="xs" onClick={onClick} title={title}>
<i className={`fa ${icon} ${styles.actionIcon}`} aria-hidden="true" />
</Button>
);
type RolesHeaderProps = {
selectable?: boolean;
selectedFirst?: boolean;
allRoles: Array<string>;
currentRole: string;
onCopyClick: (e: React.MouseEvent<HTMLButtonElement>, role: string) => void;
onDeleteClick: (e: React.MouseEvent<HTMLButtonElement>, role: string) => void;
setRole: (role: string, isCurrRole: boolean) => void;
};
const RolesHeader: React.FC<RolesHeaderProps> = ({
selectable = true,
selectedFirst = false,
allRoles,
currentRole,
onCopyClick,
onDeleteClick,
setRole,
}) => {
let roles = [...allRoles];
if (selectedFirst) {
roles = allRoles.reduce((acc, role) => {
if (role === currentRole) return [role, ...acc];
return [...acc, role];
}, [] as string[]);
}
return (
<>
{roles.length ? (
roles.map(role => (
<Header
content={role}
selectable={selectable}
isSelected={currentRole === role}
onClick={() => setRole(role, currentRole === role)}
actionButtons={[
<IconButton
icon="fa-copy"
onClick={e => onCopyClick(e, role)}
title="Copy Permissions"
/>,
<IconButton
icon="fa-trash"
onClick={e => onDeleteClick(e, role)}
title="Delete Role Permissions"
/>,
]}
/>
))
) : (
<Header content="No roles" selectable={false} />
)}
</>
);
};
export default RolesHeader;

View File

@ -39,6 +39,107 @@ import { getConfirmation } from '../../../Common/utils/jsUtils';
import ToolTip from '../../../Common/Tooltip/Tooltip';
import KnowMoreLink from '../../../Common/KnowMoreLink/KnowMoreLink';
import RawSqlButton from '../Common/Components/RawSqlButton';
import styles from '../../../Common/Common.scss';
const SchemaPermissionsButton = ({ schema }) => (
<Link to={getSchemaPermissionsRoute(schema)} style={{ marginLeft: '20px' }}>
<Button color="white" size="xs" className={styles.add_mar_left_mid}>
Show Permissions Summary
</Button>
</Link>
);
const OpenCreateSection = React.forwardRef(
({ ref, value, handleInputChange, handleCreate, handleCancelCreate }) => (
<div className={styles.display_inline + ' ' + styles.add_mar_left}>
<div className={styles.display_inline}>
<input
type="text"
value={value}
onChange={handleInputChange}
placeholder="schema_name"
className={`form-control input-sm ${styles.display_inline}`}
ref={ref}
/>
</div>
<Button
color="white"
size="xs"
onClick={handleCreate}
className={styles.add_mar_left_mid}
>
Create
</Button>
<Button
color="white"
size="xs"
onClick={handleCancelCreate}
className={styles.add_mar_left_mid}
>
Cancel
</Button>
</div>
)
);
const ClosedCreateSection = ({ onClick }) => (
<Button color="white" size="xs" onClick={onClick} title="Create new schema">
Create
</Button>
);
const CreateSchemaSection = React.forwardRef(
({
ref,
schema,
migrationMode,
createSchemaOpen,
schemaNameEdit,
handleCancelCreateNewSchema,
handleCreateNewClick,
handleSchemaNameChange,
handleCreateClick,
}) =>
migrationMode && (
<div className={`${styles.display_flex}`}>
{createSchemaOpen ? (
<OpenCreateSection
ref={ref}
value={schemaNameEdit}
handleInputChange={handleSchemaNameChange}
handleCreate={handleCreateClick}
handleCancelCreate={handleCancelCreateNewSchema}
/>
) : (
<ClosedCreateSection onClick={handleCreateNewClick} />
)}
<SchemaPermissionsButton schema={schema} />
</div>
)
);
const DeleteSchemaButton = ({ dispatch, migrationMode }) => {
const successCb = () => {
dispatch(updateCurrentSchema('public'));
};
const handleDelete = () => {
dispatch(deleteCurrentSchema(successCb));
};
return (
migrationMode && (
<Button
color="white"
size="xs"
onClick={handleDelete}
title="Delete current schema"
>
Delete
</Button>
)
);
};
class Schema extends Component {
constructor(props) {
@ -54,8 +155,44 @@ class Schema extends Component {
this.props.dispatch(
updateSchemaInfo({ schemas: [this.props.currentSchema] })
);
this.schemaNameInputRef = React.createRef(null);
}
cancelCreateNewSchema = () => {
this.setState({
createSchemaOpen: false,
});
};
onCreateNewClick = () => {
this.setState({ createSchemaOpen: true });
};
onChangeSchemaName = e => {
this.setState({ schemaNameEdit: e.target.value });
};
handleCreateClick = () => {
const schemaName = this.state.schemaNameEdit.trim();
if (!schemaName) {
this.schemaNameInputRef.current.focus();
return;
}
const successCb = () => {
this.props.dispatch(updateCurrentSchema(schemaName));
this.setState({
schemaNameEdit: '',
createSchemaOpen: false,
});
};
this.props.dispatch(createNewSchema(schemaName, successCb));
};
render() {
const {
schema,
@ -70,14 +207,10 @@ class Schema extends Component {
trackedFunctions,
} = this.props;
const styles = require('../../../Common/Common.scss');
const handleSchemaChange = e => {
dispatch(updateCurrentSchema(e.target.value));
};
/***********/
const _getTrackableFunctions = () => {
const trackedFuncNames = trackedFunctions.map(fn => getFunctionName(fn));
@ -104,8 +237,6 @@ class Schema extends Component {
);
};
/***********/
const allUntrackedTables = getUntrackedTables(
getSchemaTables(schema, currentSchema)
);
@ -144,146 +275,39 @@ class Schema extends Component {
));
};
const getCreateSchemaSection = () => {
let createSchemaSection = null;
if (migrationMode) {
const { createSchemaOpen, schemaNameEdit } = this.state;
const handleCreateNewClick = () => {
this.setState({ createSchemaOpen: true });
};
const handleSchemaNameChange = e => {
this.setState({ schemaNameEdit: e.target.value });
};
const handleCreateClick = () => {
const schemaName = schemaNameEdit.trim();
if (!schemaName) {
document.getElementById('schema-name-input').focus();
return;
}
const successCb = () => {
dispatch(updateCurrentSchema(schemaName));
this.setState({
schemaNameEdit: '',
createSchemaOpen: false,
});
};
dispatch(createNewSchema(schemaName, successCb));
};
const handleCancelCreateNewSchema = () => {
this.setState({
createSchemaOpen: false,
});
};
const closedCreateSection = (
<Button
color="white"
size="xs"
onClick={handleCreateNewClick}
title="Create new schema"
>
<i className="fa fa-plus" aria-hidden="true" />
</Button>
);
const openCreateSection = (
<div className={styles.display_inline + ' ' + styles.add_mar_left}>
<div className={styles.display_inline}>
<input
id="schema-name-input"
type="text"
value={schemaNameEdit}
onChange={handleSchemaNameChange}
placeholder="schema_name"
className={'form-control input-sm ' + styles.display_inline}
/>
</div>
<Button
color="white"
size="xs"
onClick={handleCreateClick}
className={styles.add_mar_left_mid}
>
Create
</Button>
<Button
color="white"
size="xs"
onClick={handleCancelCreateNewSchema}
className={styles.add_mar_left_mid}
>
Cancel
</Button>
</div>
);
createSchemaSection = createSchemaOpen
? openCreateSection
: closedCreateSection;
}
return createSchemaSection;
};
const getDeleteSchemaBtn = () => {
let deleteSchemaBtn = null;
if (migrationMode) {
const handleDelete = () => {
const successCb = () => {
dispatch(updateCurrentSchema('public'));
};
dispatch(deleteCurrentSchema(successCb));
};
deleteSchemaBtn = (
<Button
color="white"
size="xs"
onClick={handleDelete}
title="Delete current schema"
>
<i className="fa fa-trash" aria-hidden="true" />
</Button>
);
}
return deleteSchemaBtn;
};
return (
<div className={styles.add_mar_top}>
<div className={styles.display_inline}>Current Postgres schema</div>
<div className={styles.display_inline}>
<select
onChange={handleSchemaChange}
className={
styles.add_mar_left_mid +
' ' +
styles.width_auto +
' form-control'
}
className={`${styles.add_mar_left_mid} ${styles.width_auto} form-control`}
value={currentSchema}
>
{getSchemaOptions()}
</select>
</div>
<div className={styles.display_inline + ' ' + styles.add_mar_left}>
<div className={styles.display_inline}>{getDeleteSchemaBtn()}</div>
<div className={`${styles.display_inline} ${styles.add_mar_left}`}>
<div className={styles.display_inline}>
<DeleteSchemaButton
dispatch={dispatch}
migrationMode={migrationMode}
/>
</div>
<div
className={`${styles.display_inline} ${styles.add_mar_left_mid}`}
>
{getCreateSchemaSection()}
<CreateSchemaSection
ref={this.schemaNameInputRef}
schema={currentSchema}
migrationMode={migrationMode}
schemaNameEdit={this.state.schemaNameEdit}
createSchemaOpen={this.state.createSchemaOpen}
handleCancelCreateNewSchema={this.cancelCreateNewSchema}
handleCreateNewClick={this.onCreateNewClick}
handleSchemaNameChange={this.onChangeSchemaName}
handleCreateClick={this.handleCreateClick}
/>
</div>
</div>
</div>
@ -675,16 +699,6 @@ class Schema extends Component {
);
};
const getPermissionsSummaryLink = () => {
return (
<div className={styles.add_mar_top}>
<Link to={getSchemaPermissionsRoute(currentSchema)}>
Schema permissions summary
</Link>
</div>
);
};
return (
<div
className={`container-fluid ${styles.padd_left_remove} ${styles.padd_top}`}
@ -703,7 +717,6 @@ class Schema extends Component {
{getUntrackedFunctionsSection()}
{getNonTrackableFunctionsSection()}
<hr />
{getPermissionsSummaryLink()}
</div>
</div>
);

View File

@ -643,6 +643,73 @@ const copyRolePermissions = (
};
};
const deleteRoleGlobally = roleName => {
return (dispatch, getState) => {
const permissionsUpQueries = [];
const permissionsDownQueries = [];
const allSchemas = getState().tables.allSchemas;
const currentSchema = getState().tables.currentSchema;
const tables = getSchemaTables(allSchemas, currentSchema);
tables.forEach(table => {
const tableDef = getTableDef(table);
const actions = ['select', 'insert', 'update', 'delete'];
actions.forEach(_action => {
const currPermissions = getTablePermissions(table, roleName, _action);
if (currPermissions) {
// existing permission is there
const deleteQuery = getDropPermissionQuery(
_action,
tableDef,
roleName
);
// since the actions must be revertible
const createQuery = getCreatePermissionQuery(
_action,
tableDef,
roleName,
currPermissions
);
permissionsUpQueries.push(deleteQuery);
permissionsDownQueries.push(createQuery);
}
});
});
// Apply migration
const migrationName = `delete_role_${roleName}`;
const requestMsg = 'Deleting role';
const successMsg = 'Role Deleted';
const errorMsg = 'Role deletion failed';
const customOnSuccess = () => {
// fetch all roles
dispatch(fetchRoleList());
};
const customOnError = () => {};
makeMigrationCall(
dispatch,
getState,
permissionsUpQueries,
permissionsDownQueries,
migrationName,
customOnSuccess,
customOnError,
requestMsg,
successMsg,
errorMsg
);
};
};
const permChangePermissions = changeType => {
return (dispatch, getState) => {
const allSchemas = getState().tables.allSchemas;
@ -790,4 +857,5 @@ export {
permDelApplySamePerm,
applySamePermissionsBulk,
copyRolePermissions,
deleteRoleGlobally,
};