mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-15 01:12:56 +03:00
add schema permissions summary page + update table permissions page (#2693)
Schema permissions summary page * show summary of access control for all roles across tables and actions * allow copy of permissions from one role to others Table permissions page * highlight actions and roles as headers in table * move bulk action selector to end of row * scroll to edit/bulk section when opened * pre-select table and action in clone section
This commit is contained in:
parent
2c108daef8
commit
03ea997c3b
@ -43,12 +43,22 @@ table {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
table thead tr th {
|
||||
table thead tr th,
|
||||
table tbody tr th {
|
||||
background-color: #F2F2F2 !important;
|
||||
color: #4D4D4D;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
.full_container {
|
||||
margin: 20px;
|
||||
min-width: fit-content;
|
||||
}
|
||||
|
||||
.fit_content {
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.pageSidebar {
|
||||
height: calc(100vh - 50px);
|
||||
overflow: auto;
|
||||
@ -167,8 +177,13 @@ table thead tr th {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.line_mar {
|
||||
margin: 15px 20px;
|
||||
.flex_space_between {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.flex_0 {
|
||||
flex: 0;
|
||||
}
|
||||
|
||||
.wd5 {
|
||||
@ -198,6 +213,10 @@ table thead tr th {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.wd100Percent {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.wd90 {
|
||||
width: 90%;
|
||||
}
|
||||
@ -754,6 +773,10 @@ code {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.text_center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.block_wrapper {
|
||||
padding: 20px;
|
||||
background-color: #fff;
|
||||
@ -861,6 +884,14 @@ code {
|
||||
color: #767E96
|
||||
}
|
||||
|
||||
.text_link {
|
||||
color: #337ab7;
|
||||
}
|
||||
|
||||
.text_link:hover, .text_link:focus {
|
||||
color: #23527c;
|
||||
}
|
||||
|
||||
.docsButton {
|
||||
background-color: #fff;
|
||||
border-radius: 5px;
|
||||
@ -1325,10 +1356,6 @@ code {
|
||||
width: 325px;
|
||||
}
|
||||
|
||||
.wd100Percent {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
/* container height subtracting top header and bottom scroll bar */
|
||||
$mainContainerHeight: calc(100vh - 50px - 25px);
|
||||
|
||||
|
@ -1,30 +1,12 @@
|
||||
import React from 'react';
|
||||
import Tooltip from 'react-bootstrap/lib/Tooltip';
|
||||
import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger';
|
||||
|
||||
import styles from './GqlCompatibilityWarning.scss';
|
||||
import WarningSymbol from '../WarningSymbol/WarningSymbol';
|
||||
|
||||
const GqlCompatibilityWarning = () => {
|
||||
const gqlCompatibilityTip = (
|
||||
<Tooltip id="tooltip-scheme-warning">
|
||||
This identifier name does not conform to the GraphQL naming standard.
|
||||
Names in GraphQL should be limited to this ASCII subset:
|
||||
/[_A-Za-z][_0-9A-Za-z]*/.
|
||||
</Tooltip>
|
||||
);
|
||||
const gqlCompatibilityTip =
|
||||
'This identifier name does not conform to the GraphQL naming standard. Names in GraphQL should be limited to this ASCII subset: /[_A-Za-z][_0-9A-Za-z]*/.';
|
||||
|
||||
return (
|
||||
<div className={styles.display_inline}>
|
||||
<OverlayTrigger placement="right" overlay={gqlCompatibilityTip}>
|
||||
<i
|
||||
className={`fa fa-exclamation-triangle ${
|
||||
styles.gqlCompatibilityWarning
|
||||
}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</OverlayTrigger>
|
||||
</div>
|
||||
);
|
||||
return <WarningSymbol tooltipText={gqlCompatibilityTip} />;
|
||||
};
|
||||
|
||||
export default GqlCompatibilityWarning;
|
||||
|
@ -13,12 +13,12 @@
|
||||
C74.03,0,68.038,20.458,65.159,30.292l-0.49,1.659c-0.585,1.946-2.12,7.942-4.122,15.962H39.239c-3.364,0-6.09,2.726-6.09,6.09
|
||||
c0,3.364,2.726,6.09,6.09,6.09H57.53c-6.253,25.362-14.334,58.815-15.223,62.498c-0.332,0.965-2.829,7.742-7.937,7.742
|
||||
c-7.8,0-11.177-10.948-11.204-11.03c-0.936-3.229-4.305-5.098-7.544-4.156c-3.23,0.937-5.092,4.314-4.156,7.545
|
||||
C13.597,130.053,20.816,142.514,34.367,142.514z"/>
|
||||
C13.597,130.053,20.816,142.514,34.367,142.514z" fill="#767E93"/>
|
||||
<path d="M124.685,126.809c3.589,0,6.605-2.549,6.605-6.607c0-1.885-0.754-3.586-2.359-5.474l-12.646-14.534l12.271-14.346
|
||||
c1.132-1.416,1.98-2.926,1.98-4.908c0-3.59-2.927-6.231-6.703-6.231c-2.547,0-4.527,1.604-6.229,3.684l-9.531,12.454L98.73,78.391
|
||||
c-1.89-2.357-3.869-3.682-6.7-3.682c-3.59,0-6.607,2.551-6.607,6.609c0,1.885,0.756,3.586,2.357,5.471l11.799,13.592
|
||||
L86.647,115.67c-1.227,1.416-1.98,2.926-1.98,4.908c0,3.589,2.926,6.229,6.699,6.229c2.549,0,4.53-1.604,6.229-3.682l10.19-13.4
|
||||
l10.193,13.4C119.872,125.488,121.854,126.809,124.685,126.809z"/>
|
||||
l10.193,13.4C119.872,125.488,121.854,126.809,124.685,126.809z" fill="#767E93"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
@ -0,0 +1,54 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="142.514px" height="142.514px" viewBox="0 0 142.514 142.514" style="enable-background:new 0 0 142.514 142.514;"
|
||||
xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<path d="M34.367,142.514c11.645,0,17.827-10.4,19.645-16.544c0.029-0.097,0.056-0.196,0.081-0.297
|
||||
c4.236-17.545,10.984-45.353,15.983-65.58h17.886c3.363,0,6.09-2.726,6.09-6.09c0-3.364-2.727-6.09-6.09-6.09H73.103
|
||||
c1.6-6.373,2.771-10.912,3.232-12.461l0.512-1.734c1.888-6.443,6.309-21.535,13.146-21.535c6.34,0,7.285,9.764,7.328,10.236
|
||||
c0.27,3.343,3.186,5.868,6.537,5.579c3.354-0.256,5.864-3.187,5.605-6.539C108.894,14.036,104.087,0,89.991,0
|
||||
C74.03,0,68.038,20.458,65.159,30.292l-0.49,1.659c-0.585,1.946-2.12,7.942-4.122,15.962H39.239c-3.364,0-6.09,2.726-6.09,6.09
|
||||
c0,3.364,2.726,6.09,6.09,6.09H57.53c-6.253,25.362-14.334,58.815-15.223,62.498c-0.332,0.965-2.829,7.742-7.937,7.742
|
||||
c-7.8,0-11.177-10.948-11.204-11.03c-0.936-3.229-4.305-5.098-7.544-4.156c-3.23,0.937-5.092,4.314-4.156,7.545
|
||||
C13.597,130.053,20.816,142.514,34.367,142.514z" fill="#fd9540"/>
|
||||
<path d="M124.685,126.809c3.589,0,6.605-2.549,6.605-6.607c0-1.885-0.754-3.586-2.359-5.474l-12.646-14.534l12.271-14.346
|
||||
c1.132-1.416,1.98-2.926,1.98-4.908c0-3.59-2.927-6.231-6.703-6.231c-2.547,0-4.527,1.604-6.229,3.684l-9.531,12.454L98.73,78.391
|
||||
c-1.89-2.357-3.869-3.682-6.7-3.682c-3.59,0-6.607,2.551-6.607,6.609c0,1.885,0.756,3.586,2.357,5.471l11.799,13.592
|
||||
L86.647,115.67c-1.227,1.416-1.98,2.926-1.98,4.908c0,3.589,2.926,6.229,6.699,6.229c2.549,0,4.53-1.604,6.229-3.682l10.19-13.4
|
||||
l10.193,13.4C119.872,125.488,121.854,126.809,124.685,126.809z" fill="#fd9540"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.1 KiB |
@ -1,41 +0,0 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" width="512px" height="512px" viewBox="0 0 142.514 142.514" style="enable-background:new 0 0 142.514 142.514;" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<path d="M34.367,142.514c11.645,0,17.827-10.4,19.645-16.544c0.029-0.097,0.056-0.196,0.081-0.297 c4.236-17.545,10.984-45.353,15.983-65.58h17.886c3.363,0,6.09-2.726,6.09-6.09c0-3.364-2.727-6.09-6.09-6.09H73.103 c1.6-6.373,2.771-10.912,3.232-12.461l0.512-1.734c1.888-6.443,6.309-21.535,13.146-21.535c6.34,0,7.285,9.764,7.328,10.236 c0.27,3.343,3.186,5.868,6.537,5.579c3.354-0.256,5.864-3.187,5.605-6.539C108.894,14.036,104.087,0,89.991,0 C74.03,0,68.038,20.458,65.159,30.292l-0.49,1.659c-0.585,1.946-2.12,7.942-4.122,15.962H39.239c-3.364,0-6.09,2.726-6.09,6.09 c0,3.364,2.726,6.09,6.09,6.09H57.53c-6.253,25.362-14.334,58.815-15.223,62.498c-0.332,0.965-2.829,7.742-7.937,7.742 c-7.8,0-11.177-10.948-11.204-11.03c-0.936-3.229-4.305-5.098-7.544-4.156c-3.23,0.937-5.092,4.314-4.156,7.545 C13.597,130.053,20.816,142.514,34.367,142.514z" fill="#fd9540"/>
|
||||
<path d="M124.685,126.809c3.589,0,6.605-2.549,6.605-6.607c0-1.885-0.754-3.586-2.359-5.474l-12.646-14.534l12.271-14.346 c1.132-1.416,1.98-2.926,1.98-4.908c0-3.59-2.927-6.231-6.703-6.231c-2.547,0-4.527,1.604-6.229,3.684l-9.531,12.454L98.73,78.391 c-1.89-2.357-3.869-3.682-6.7-3.682c-3.59,0-6.607,2.551-6.607,6.609c0,1.885,0.756,3.586,2.357,5.471l11.799,13.592 L86.647,115.67c-1.227,1.416-1.98,2.926-1.98,4.908c0,3.589,2.926,6.229,6.699,6.229c2.549,0,4.53-1.604,6.229-3.682l10.19-13.4 l10.193,13.4C119.872,125.488,121.854,126.809,124.685,126.809z" fill="#fd9540"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 2.1 KiB |
37
console/src/components/Common/Modal/Modal.js
Normal file
37
console/src/components/Common/Modal/Modal.js
Normal file
@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import BootstrapModal from 'react-bootstrap/lib/Modal';
|
||||
import BootstrapModalButton from 'react-bootstrap/lib/Button';
|
||||
|
||||
const Modal = ({
|
||||
show = true,
|
||||
title,
|
||||
onClose,
|
||||
onSubmit,
|
||||
onCancel = null,
|
||||
submitText = null,
|
||||
submitTestId = null,
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<BootstrapModal show={show} onHide={onClose}>
|
||||
<BootstrapModal.Header closeButton>
|
||||
<BootstrapModal.Title>{title}</BootstrapModal.Title>
|
||||
</BootstrapModal.Header>
|
||||
<BootstrapModal.Body>{children}</BootstrapModal.Body>
|
||||
<BootstrapModal.Footer>
|
||||
<BootstrapModalButton onClick={onCancel || onClose}>
|
||||
Cancel
|
||||
</BootstrapModalButton>
|
||||
<BootstrapModalButton
|
||||
onClick={onSubmit}
|
||||
bsStyle="primary"
|
||||
data-test={submitTestId}
|
||||
>
|
||||
{submitText || 'Submit'}
|
||||
</BootstrapModalButton>
|
||||
</BootstrapModal.Footer>
|
||||
</BootstrapModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default Modal;
|
@ -140,15 +140,6 @@ a.expanded {
|
||||
}
|
||||
}
|
||||
|
||||
.iClickable {
|
||||
}
|
||||
|
||||
.iClickable:hover {
|
||||
cursor: pointer;
|
||||
color: #b85c27;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.insertBox {
|
||||
min-width: 270px;
|
||||
}
|
||||
|
28
console/src/components/Common/WarningSymbol/WarningSymbol.js
Normal file
28
console/src/components/Common/WarningSymbol/WarningSymbol.js
Normal file
@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import Tooltip from 'react-bootstrap/lib/Tooltip';
|
||||
import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger';
|
||||
|
||||
import styles from './WarningSymbol.scss';
|
||||
|
||||
const WarningSymbol = ({
|
||||
tooltipText,
|
||||
tooltipPlacement = 'right',
|
||||
customStyle = null,
|
||||
}) => {
|
||||
const tooltip = <Tooltip>{tooltipText}</Tooltip>;
|
||||
|
||||
return (
|
||||
<div className={styles.display_inline}>
|
||||
<OverlayTrigger placement={tooltipPlacement} overlay={tooltip}>
|
||||
<i
|
||||
className={`fa fa-exclamation-triangle ${styles.warningSymbol} ${
|
||||
customStyle ? customStyle : ''
|
||||
}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</OverlayTrigger>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WarningSymbol;
|
@ -1,5 +1,5 @@
|
||||
@import "../Common.scss";
|
||||
|
||||
.gqlCompatibilityWarning {
|
||||
.warningSymbol {
|
||||
color: #d9534f;
|
||||
}
|
49
console/src/components/Common/utils/jsUtils.js
Normal file
49
console/src/components/Common/utils/jsUtils.js
Normal file
@ -0,0 +1,49 @@
|
||||
// TODO: make functions from this file available without imports
|
||||
|
||||
export const exists = value => {
|
||||
return value !== null && value !== undefined;
|
||||
};
|
||||
|
||||
export const isArray = value => {
|
||||
return Array.isArray(value);
|
||||
};
|
||||
|
||||
export const isObject = value => {
|
||||
return typeof value === 'object';
|
||||
};
|
||||
|
||||
export const isString = value => {
|
||||
return typeof value === 'string';
|
||||
};
|
||||
|
||||
export const isEmpty = value => {
|
||||
let _isEmpty = false;
|
||||
|
||||
if (!exists(value)) {
|
||||
_isEmpty = true;
|
||||
} else if (isArray(value)) {
|
||||
_isEmpty = value.length === 0;
|
||||
} else if (isObject(value)) {
|
||||
_isEmpty = JSON.stringify(value) === JSON.stringify({});
|
||||
} else if (isString(value)) {
|
||||
_isEmpty = value === '';
|
||||
}
|
||||
|
||||
return _isEmpty;
|
||||
};
|
||||
|
||||
export const isEqual = (value1, value2) => {
|
||||
let _isEqual = false;
|
||||
|
||||
if (typeof value1 === typeof value2) {
|
||||
if (isArray(value1)) {
|
||||
// TODO
|
||||
} else if (isObject(value2)) {
|
||||
_isEqual = JSON.stringify(value1) === JSON.stringify(value2);
|
||||
} else {
|
||||
_isEqual = value1 === value2;
|
||||
}
|
||||
}
|
||||
|
||||
return _isEqual;
|
||||
};
|
103
console/src/components/Common/utils/pgUtils.js
Normal file
103
console/src/components/Common/utils/pgUtils.js
Normal file
@ -0,0 +1,103 @@
|
||||
import React from 'react';
|
||||
import { isEqual } from './jsUtils';
|
||||
|
||||
/*** Table/View utils ***/
|
||||
|
||||
// TODO: figure out better pattern for overloading fns
|
||||
|
||||
export const getTableName = table => {
|
||||
return table.table_name;
|
||||
};
|
||||
|
||||
export const getTableSchema = table => {
|
||||
return table.table_schema;
|
||||
};
|
||||
|
||||
// tableName and tableNameWithSchema are either/or arguments
|
||||
export const generateTableDef = (
|
||||
tableName,
|
||||
tableSchema = 'public',
|
||||
tableNameWithSchema = null
|
||||
) => {
|
||||
if (tableNameWithSchema) {
|
||||
tableSchema = tableNameWithSchema.split('.')[0];
|
||||
tableName = tableNameWithSchema.split('.')[1];
|
||||
}
|
||||
|
||||
return {
|
||||
schema: tableSchema,
|
||||
name: tableName,
|
||||
};
|
||||
};
|
||||
|
||||
export const getTableDef = table => {
|
||||
return generateTableDef(getTableName(table), getTableSchema(table));
|
||||
};
|
||||
|
||||
// table and tableDef are either/or arguments
|
||||
export const getTableNameWithSchema = (
|
||||
table,
|
||||
wrapDoubleQuotes = true,
|
||||
tableDef = null
|
||||
) => {
|
||||
let _fullTableName;
|
||||
|
||||
tableDef = tableDef || getTableDef(table);
|
||||
|
||||
if (wrapDoubleQuotes) {
|
||||
_fullTableName =
|
||||
'"' + tableDef.schema + '"' + '.' + '"' + tableDef.name + '"';
|
||||
} else {
|
||||
_fullTableName = tableDef.schema + '.' + tableDef.name;
|
||||
}
|
||||
|
||||
return _fullTableName;
|
||||
};
|
||||
|
||||
export const checkIfTable = table => {
|
||||
return table.table_type === 'BASE TABLE';
|
||||
};
|
||||
|
||||
export const displayTableName = table => {
|
||||
const tableName = getTableName(table);
|
||||
const isTable = checkIfTable(table);
|
||||
|
||||
return isTable ? <span>{tableName}</span> : <i>{tableName}</i>;
|
||||
};
|
||||
|
||||
export const findTable = (allTables, tableDef) => {
|
||||
return allTables.find(t => isEqual(getTableDef(t), tableDef));
|
||||
};
|
||||
|
||||
export const getSchemaTables = (allTables, tableSchema) => {
|
||||
return allTables.filter(t => getTableSchema(t) === tableSchema);
|
||||
};
|
||||
|
||||
export const getTrackedTables = tables => {
|
||||
return tables.filter(t => t.is_table_tracked);
|
||||
};
|
||||
|
||||
/*** Table/View permissions utils ***/
|
||||
export const getTablePermissions = (table, role = null, action = null) => {
|
||||
let tablePermissions = table.permissions;
|
||||
|
||||
if (role) {
|
||||
tablePermissions = tablePermissions.find(p => p.role_name === role);
|
||||
|
||||
if (tablePermissions && action) {
|
||||
tablePermissions = tablePermissions.permissions[action];
|
||||
}
|
||||
}
|
||||
|
||||
return tablePermissions;
|
||||
};
|
||||
|
||||
/*** Function utils ***/
|
||||
|
||||
export const getFunctionSchema = pgFunction => {
|
||||
return pgFunction.function_schema;
|
||||
};
|
||||
|
||||
export const getFunctionName = pgFunction => {
|
||||
return pgFunction.function_name;
|
||||
};
|
59
console/src/components/Common/utils/routesUtils.js
Normal file
59
console/src/components/Common/utils/routesUtils.js
Normal file
@ -0,0 +1,59 @@
|
||||
// import globals from '../../../Globals';
|
||||
import {
|
||||
getTableSchema,
|
||||
getTableName,
|
||||
checkIfTable,
|
||||
getFunctionSchema,
|
||||
getFunctionName,
|
||||
} from './pgUtils';
|
||||
|
||||
/*** DATA ROUTES ***/
|
||||
|
||||
export const getSchemaBaseRoute = schemaName => {
|
||||
// return `${globals.urlPrefix}/data/schema/${schemaName}`;
|
||||
return `/data/schema/${schemaName}`;
|
||||
};
|
||||
|
||||
export const getSchemaAddTableRoute = schemaName => {
|
||||
return `${getSchemaBaseRoute(schemaName)}/table/add`;
|
||||
};
|
||||
|
||||
export const getSchemaPermissionsRoute = schemaName => {
|
||||
return `${getSchemaBaseRoute(schemaName)}/permissions`;
|
||||
};
|
||||
|
||||
export const getTableBaseRoute = table => {
|
||||
return `${getSchemaBaseRoute(getTableSchema(table))}/${
|
||||
checkIfTable(table) ? 'tables' : 'views'
|
||||
}/${getTableName(table)}`;
|
||||
};
|
||||
|
||||
export const getTableBrowseRoute = table => {
|
||||
return `${getTableBaseRoute(table)}/browse`;
|
||||
};
|
||||
|
||||
export const getTableModifyRoute = table => {
|
||||
return `${getTableBaseRoute(table)}/modify`;
|
||||
};
|
||||
|
||||
export const getTableRelationshipsRoute = table => {
|
||||
return `${getTableBaseRoute(table)}/relationships`;
|
||||
};
|
||||
|
||||
export const getTablePermissionsRoute = table => {
|
||||
return `${getTableBaseRoute(table)}/permissions`;
|
||||
};
|
||||
|
||||
export const getFunctionBaseRoute = pgFunction => {
|
||||
return `${getSchemaBaseRoute(
|
||||
getFunctionSchema(pgFunction)
|
||||
)}/functions/${getFunctionName(pgFunction)}`;
|
||||
};
|
||||
|
||||
export const getFunctionModifyRoute = pgFunction => {
|
||||
return `${getFunctionBaseRoute(pgFunction)}/modify`;
|
||||
};
|
||||
|
||||
export const getFunctionPermissionsRoute = pgFunction => {
|
||||
return `${getFunctionBaseRoute(pgFunction)}/permissions`;
|
||||
};
|
3
console/src/components/Common/utils/urlUtils.js
Normal file
3
console/src/components/Common/utils/urlUtils.js
Normal file
@ -0,0 +1,3 @@
|
||||
export const getPathRoot = path => {
|
||||
return path.split('/')[1];
|
||||
};
|
25
console/src/components/Common/utils/v1QueryUtils.js
Normal file
25
console/src/components/Common/utils/v1QueryUtils.js
Normal file
@ -0,0 +1,25 @@
|
||||
export const getCreatePermissionQuery = (
|
||||
action,
|
||||
tableDef,
|
||||
role,
|
||||
permission
|
||||
) => {
|
||||
return {
|
||||
type: 'create_' + action + '_permission',
|
||||
args: {
|
||||
table: tableDef,
|
||||
role: role,
|
||||
permission: permission,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getDropPermissionQuery = (action, tableDef, role) => {
|
||||
return {
|
||||
type: 'drop_' + action + '_permission',
|
||||
args: {
|
||||
table: tableDef,
|
||||
role: role,
|
||||
},
|
||||
};
|
||||
};
|
@ -2,16 +2,24 @@ import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Link } from 'react-router';
|
||||
import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger';
|
||||
import Tooltip from 'react-bootstrap/lib/Tooltip';
|
||||
|
||||
import * as tooltips from './Tooltips';
|
||||
import globals from '../../Globals';
|
||||
import * as tooltip from './Tooltips';
|
||||
import { getPathRoot } from '../Common/utils/urlUtils';
|
||||
|
||||
import Spinner from '../Common/Spinner/Spinner';
|
||||
import WarningSymbol from '../Common/WarningSymbol/WarningSymbol';
|
||||
|
||||
import {
|
||||
loadServerVersion,
|
||||
fetchServerConfig,
|
||||
loadLatestServerVersion,
|
||||
featureCompatibilityInit,
|
||||
} from './Actions';
|
||||
|
||||
import { loadConsoleTelemetryOpts } from '../../telemetry/Actions.js';
|
||||
|
||||
import {
|
||||
loadInconsistentObjects,
|
||||
redirectToMetadataStatus,
|
||||
@ -167,7 +175,7 @@ class Main extends React.Component {
|
||||
const pixHeart = require('./images/pix-heart.svg');
|
||||
|
||||
const currentLocation = location.pathname;
|
||||
const currentActiveBlock = currentLocation.split('/')[1];
|
||||
const currentActiveBlock = getPathRoot(currentLocation);
|
||||
|
||||
const getMainContent = () => {
|
||||
let mainContent = null;
|
||||
@ -221,20 +229,18 @@ class Main extends React.Component {
|
||||
) {
|
||||
adminSecretHtml = (
|
||||
<div className={styles.secureSection}>
|
||||
<OverlayTrigger placement="left" overlay={tooltip.secureEndpoint}>
|
||||
<a
|
||||
href="https://docs.hasura.io/1.0/graphql/manual/deployment/securing-graphql-endpoint.html"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<i
|
||||
className={
|
||||
styles.padd_small_right + ' fa fa-exclamation-triangle'
|
||||
}
|
||||
/>
|
||||
Secure your endpoint
|
||||
</a>
|
||||
</OverlayTrigger>
|
||||
<a
|
||||
href="https://docs.hasura.io/1.0/graphql/manual/deployment/securing-graphql-endpoint.html"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<WarningSymbol
|
||||
tooltipText={tooltips.secureEndpoint}
|
||||
tooltipPlacement={'left'}
|
||||
customStyle={styles.secureSectionSymbol}
|
||||
/>
|
||||
Secure your endpoint
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -417,6 +423,39 @@ class Main extends React.Component {
|
||||
return helpDropdownPosStyle;
|
||||
};
|
||||
|
||||
const getSidebarItem = (
|
||||
title,
|
||||
icon,
|
||||
tooltipText,
|
||||
path,
|
||||
isDefault = false
|
||||
) => {
|
||||
const itemTooltip = <Tooltip>{tooltipText}</Tooltip>;
|
||||
|
||||
const block = getPathRoot(path);
|
||||
|
||||
return (
|
||||
<OverlayTrigger placement="right" overlay={itemTooltip}>
|
||||
<li>
|
||||
<Link
|
||||
className={
|
||||
currentActiveBlock === block ||
|
||||
(isDefault && currentActiveBlock === '')
|
||||
? styles.navSideBarActive
|
||||
: ''
|
||||
}
|
||||
to={appPrefix + path}
|
||||
>
|
||||
<div className={styles.iconCenter} data-test={block}>
|
||||
<i className={`fa ${icon}`} aria-hidden="true" />
|
||||
</div>
|
||||
<p>{title}</p>
|
||||
</Link>
|
||||
</li>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.flexRow}>
|
||||
@ -435,97 +474,31 @@ class Main extends React.Component {
|
||||
</div>
|
||||
<div className={styles.header_items}>
|
||||
<ul className={styles.sidebarItems}>
|
||||
<OverlayTrigger placement="right" overlay={tooltip.apiexplorer}>
|
||||
<li>
|
||||
<Link
|
||||
className={
|
||||
currentActiveBlock === 'api-explorer' ||
|
||||
currentActiveBlock === ''
|
||||
? styles.navSideBarActive
|
||||
: ''
|
||||
}
|
||||
to={appPrefix + '/api-explorer'}
|
||||
>
|
||||
<div
|
||||
className={styles.iconCenter}
|
||||
data-test="api-explorer"
|
||||
>
|
||||
<i
|
||||
title="API Explorer"
|
||||
className="fa fa-flask"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<p>GraphiQL</p>
|
||||
</Link>
|
||||
</li>
|
||||
</OverlayTrigger>
|
||||
<OverlayTrigger placement="right" overlay={tooltip.data}>
|
||||
<li>
|
||||
<Link
|
||||
className={
|
||||
currentActiveBlock === 'data'
|
||||
? styles.navSideBarActive
|
||||
: ''
|
||||
}
|
||||
to={appPrefix + '/data/schema/' + currentSchema}
|
||||
>
|
||||
<div className={styles.iconCenter}>
|
||||
<i
|
||||
title="Data Service"
|
||||
className="fa fa-database"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<p>Data</p>
|
||||
</Link>
|
||||
</li>
|
||||
</OverlayTrigger>
|
||||
<OverlayTrigger
|
||||
placement="right"
|
||||
overlay={tooltip.remoteSchema}
|
||||
>
|
||||
<li>
|
||||
<Link
|
||||
className={
|
||||
currentActiveBlock === 'remote-schemas'
|
||||
? styles.navSideBarActive
|
||||
: ''
|
||||
}
|
||||
to={appPrefix + '/remote-schemas/manage/schemas'}
|
||||
>
|
||||
<div className={styles.iconCenter}>
|
||||
<i
|
||||
title="Remote Schemas"
|
||||
className="fa fa-plug"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<p>Remote Schemas</p>
|
||||
</Link>
|
||||
</li>
|
||||
</OverlayTrigger>
|
||||
<OverlayTrigger placement="right" overlay={tooltip.events}>
|
||||
<li>
|
||||
<Link
|
||||
className={
|
||||
currentActiveBlock === 'events'
|
||||
? styles.navSideBarActive
|
||||
: ''
|
||||
}
|
||||
to={appPrefix + '/events/manage/triggers'}
|
||||
>
|
||||
<div className={styles.iconCenter}>
|
||||
<i
|
||||
title="Events"
|
||||
className="fa fa-cloud"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<p>Events</p>
|
||||
</Link>
|
||||
</li>
|
||||
</OverlayTrigger>
|
||||
{getSidebarItem(
|
||||
'GraphiQL',
|
||||
'fa-flask',
|
||||
tooltips.apiExplorer,
|
||||
'/api-explorer',
|
||||
true
|
||||
)}
|
||||
{getSidebarItem(
|
||||
'Data',
|
||||
'fa-database',
|
||||
tooltips.data,
|
||||
'/data/schema/' + currentSchema
|
||||
)}
|
||||
{getSidebarItem(
|
||||
'Remote Schemas',
|
||||
'fa-plug',
|
||||
tooltips.remoteSchema,
|
||||
'/remote-schemas/manage/schemas'
|
||||
)}
|
||||
{getSidebarItem(
|
||||
'Events',
|
||||
'fa-cloud',
|
||||
tooltips.events,
|
||||
'/events/manage/triggers'
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
<div id="dropdown_wrapper" className={styles.clusterInfoWrapper}>
|
||||
|
@ -953,14 +953,18 @@
|
||||
}
|
||||
|
||||
.secureSection {
|
||||
// background-color: #545a6c;
|
||||
|
||||
padding: 15px;
|
||||
|
||||
a {
|
||||
color: white;
|
||||
//color: #FFC627;
|
||||
color: #FFFFFF;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.secureSectionSymbol {
|
||||
//color: #FFC627 !important;
|
||||
color: inherit !important;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes heartbeat {
|
||||
|
@ -1,26 +1,15 @@
|
||||
import React from 'react';
|
||||
import Tooltip from 'react-bootstrap/lib/Tooltip';
|
||||
import globals from '../../Globals';
|
||||
|
||||
export const data = (
|
||||
<Tooltip id="tooltip-data-service">Data & Schema management</Tooltip>
|
||||
);
|
||||
export const data = 'Data & Schema management';
|
||||
|
||||
export const apiexplorer = (
|
||||
<Tooltip id="tooltip-api-explorer">Test the GraphQL APIs</Tooltip>
|
||||
);
|
||||
export const apiExplorer = 'Test the GraphQL APIs';
|
||||
|
||||
export const events = (
|
||||
<Tooltip id="tooltip-events">Manage Event Triggers</Tooltip>
|
||||
);
|
||||
export const events = 'Manage Event Triggers';
|
||||
|
||||
export const remoteSchema = (
|
||||
<Tooltip id="tooltip-remoteschema">Manage Remote Schemas</Tooltip>
|
||||
);
|
||||
export const remoteSchema = 'Manage Remote Schemas';
|
||||
|
||||
export const secureEndpoint = (
|
||||
<Tooltip id="tooltip-secure-endpoint">
|
||||
This graphql endpoint is public and you should add an{' '}
|
||||
{globals.adminSecretLabel}
|
||||
</Tooltip>
|
||||
);
|
||||
export const roles = 'User Roles Summary';
|
||||
|
||||
export const secureEndpoint = `This GraphQL endpoint is public. You should add an ${
|
||||
globals.adminSecretLabel
|
||||
}`;
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React, { Component } from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
|
||||
import Endpoints from '../../../Endpoints';
|
||||
|
||||
@ -110,13 +111,10 @@ class About extends Component {
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`container-fluid ${styles.add_mar_top} ${styles.add_mar_left}`}
|
||||
>
|
||||
<div className={`container-fluid ${styles.full_container}`}>
|
||||
<div className={styles.subHeader}>
|
||||
<h2 className={`${styles.heading_text} ${styles.remove_pad_bottom}`}>
|
||||
About
|
||||
</h2>
|
||||
<Helmet title={'About | Hasura'} />
|
||||
<h2 className={styles.headerText}>About</h2>
|
||||
<div className={styles.wd60}>
|
||||
<div className={styles.add_mar_top}>
|
||||
{getServerVersionSection()}
|
||||
|
@ -6,7 +6,7 @@ import jwt from 'jsonwebtoken';
|
||||
import TextAreaWithCopy from '../../../Common/TextAreaWithCopy/TextAreaWithCopy';
|
||||
import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger';
|
||||
import Tooltip from 'react-bootstrap/lib/Tooltip';
|
||||
import ModalWrapper from '../../../Common/ModalWrapper';
|
||||
import ModalWrapper from '../../../Common/Modal/ModalWrapper';
|
||||
|
||||
import { parseAuthHeader } from './utils';
|
||||
|
||||
@ -613,8 +613,8 @@ class ApiRequest extends Component {
|
||||
claimData =
|
||||
claimFormat === 'stringified_json'
|
||||
? generateValidNameSpaceData(
|
||||
JSON.parse(payload[claimNameSpace])
|
||||
)
|
||||
JSON.parse(payload[claimNameSpace])
|
||||
)
|
||||
: generateValidNameSpaceData(payload[claimNameSpace]);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
@ -462,10 +462,7 @@ class AddTable extends Component {
|
||||
placement="right"
|
||||
overlay={tooltip.primaryKeyDescription}
|
||||
>
|
||||
<i
|
||||
className={`fa fa-question-circle ${styles.iClickable}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<i className={'fa fa-question-circle'} aria-hidden="true" />
|
||||
</OverlayTrigger>{' '}
|
||||
|
||||
</h4>
|
||||
@ -482,10 +479,7 @@ class AddTable extends Component {
|
||||
placement="right"
|
||||
overlay={tooltip.foreignKeyDescription}
|
||||
>
|
||||
<i
|
||||
className={`fa fa-question-circle ${styles.iClickable}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<i className={'fa fa-question-circle'} aria-hidden="true" />
|
||||
</OverlayTrigger>{' '}
|
||||
|
||||
</h4>
|
||||
@ -507,10 +501,7 @@ class AddTable extends Component {
|
||||
placement="right"
|
||||
overlay={tooltip.uniqueKeyDescription}
|
||||
>
|
||||
<i
|
||||
className={`fa fa-question-circle ${styles.iClickable}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<i className={'fa fa-question-circle'} aria-hidden="true" />
|
||||
</OverlayTrigger>{' '}
|
||||
|
||||
</h4>
|
||||
|
@ -50,17 +50,17 @@ const ForeignKeySelector = ({
|
||||
onChange={dispatchSetRefSchema}
|
||||
>
|
||||
{// default unselected option
|
||||
refSchemaName === '' && (
|
||||
<option value={''} disabled>
|
||||
{'-- reference schema --'}
|
||||
</option>
|
||||
)}
|
||||
refSchemaName === '' && (
|
||||
<option value={''} disabled>
|
||||
{'-- reference schema --'}
|
||||
</option>
|
||||
)}
|
||||
{// all reference schema options
|
||||
schemaList.map((rs, j) => (
|
||||
<option key={j} value={rs}>
|
||||
{rs}
|
||||
</option>
|
||||
))}
|
||||
schemaList.map((rs, j) => (
|
||||
<option key={j} value={rs}>
|
||||
{rs}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
@ -105,19 +105,19 @@ const ForeignKeySelector = ({
|
||||
disabled={!refSchemaName}
|
||||
>
|
||||
{// default unselected option
|
||||
refTableName === '' && (
|
||||
<option value={''} disabled>
|
||||
{'-- reference table --'}
|
||||
</option>
|
||||
)}
|
||||
refTableName === '' && (
|
||||
<option value={''} disabled>
|
||||
{'-- reference table --'}
|
||||
</option>
|
||||
)}
|
||||
{// all reference table options
|
||||
Object.keys(refTables)
|
||||
.sort()
|
||||
.map((rt, j) => (
|
||||
<option key={j} value={rt}>
|
||||
{rt}
|
||||
</option>
|
||||
))}
|
||||
Object.keys(refTables)
|
||||
.sort()
|
||||
.map((rt, j) => (
|
||||
<option key={j} value={rt}>
|
||||
{rt}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
@ -304,10 +304,7 @@ const ForeignKeySelector = ({
|
||||
<div className={`${styles.add_mar_bottom_mid}`}>
|
||||
<b>On Update Violation:</b>
|
||||
<OverlayTrigger placement="right" overlay={fkViolationOnUpdate}>
|
||||
<i
|
||||
className={`fa fa-question-circle ${styles.iClickable}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<i className={'fa fa-question-circle'} aria-hidden="true" />
|
||||
</OverlayTrigger>{' '}
|
||||
|
||||
</div>
|
||||
@ -317,10 +314,7 @@ const ForeignKeySelector = ({
|
||||
<div className={`${styles.add_mar_bottom_mid}`}>
|
||||
<b>On Delete Violation:</b>
|
||||
<OverlayTrigger placement="right" overlay={fkViolationOnDelete}>
|
||||
<i
|
||||
className={`fa fa-question-circle ${styles.iClickable}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<i className={'fa fa-question-circle'} aria-hidden="true" />
|
||||
</OverlayTrigger>{' '}
|
||||
|
||||
</div>
|
||||
|
@ -21,6 +21,7 @@ import {
|
||||
dataPageConnector,
|
||||
migrationsConnector,
|
||||
functionWrapperConnector,
|
||||
permissionsSummaryConnector,
|
||||
ModifyCustomFunction,
|
||||
PermissionCustomFunction,
|
||||
// metadataConnector,
|
||||
@ -108,6 +109,10 @@ const makeDataRouter = (
|
||||
component={permissionsConnector(connect)}
|
||||
tableType={'view'}
|
||||
/>
|
||||
<Route
|
||||
path=":schema/permissions"
|
||||
component={permissionsSummaryConnector(connect)}
|
||||
/>
|
||||
</Route>
|
||||
<Route
|
||||
path="schema/:schema/table/add"
|
||||
|
@ -5,8 +5,16 @@ import { Link } from 'react-router';
|
||||
import LeftSubSidebar from '../../Common/Layout/LeftSubSidebar/LeftSubSidebar';
|
||||
import gqlPattern from './Common/GraphQLValidation';
|
||||
import GqlCompatibilityWarning from '../../Common/GqlCompatibilityWarning/GqlCompatibilityWarning';
|
||||
|
||||
const appPrefix = '/data';
|
||||
import {
|
||||
displayTableName,
|
||||
getFunctionName,
|
||||
getTableName,
|
||||
} from '../../Common/utils/pgUtils';
|
||||
import {
|
||||
getFunctionModifyRoute,
|
||||
getSchemaAddTableRoute,
|
||||
getTableBrowseRoute,
|
||||
} from '../../Common/utils/routesUtils';
|
||||
|
||||
class DataSubSidebar extends React.Component {
|
||||
constructor() {
|
||||
@ -35,6 +43,7 @@ class DataSubSidebar extends React.Component {
|
||||
if (nextProps.metadata.ongoingRequest) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -49,7 +58,7 @@ class DataSubSidebar extends React.Component {
|
||||
render() {
|
||||
const styles = require('../../Common/Layout/LeftSubSidebar/LeftSubSidebar.scss');
|
||||
const functionSymbol = require('../../Common/Layout/LeftSubSidebar/function.svg');
|
||||
const functionSymbolActive = require('../../Common/Layout/LeftSubSidebar/function_high.svg');
|
||||
const functionSymbolActive = require('../../Common/Layout/LeftSubSidebar/function_active.svg');
|
||||
const {
|
||||
functionsList,
|
||||
currentTable,
|
||||
@ -64,11 +73,11 @@ class DataSubSidebar extends React.Component {
|
||||
const trackedTablesLength = trackedTables.length;
|
||||
|
||||
const tableList = trackedTables.filter(t =>
|
||||
t.table_name.includes(searchInput)
|
||||
getTableName(t).includes(searchInput)
|
||||
);
|
||||
|
||||
const listedFunctions = functionsList.filter(f =>
|
||||
f.function_name.includes(searchInput)
|
||||
getFunctionName(f).includes(searchInput)
|
||||
);
|
||||
|
||||
const getSearchInput = () => {
|
||||
@ -93,7 +102,7 @@ class DataSubSidebar extends React.Component {
|
||||
const tables = {};
|
||||
tableList.map(t => {
|
||||
if (t.is_table_tracked) {
|
||||
tables[t.table_name] = t;
|
||||
tables[getTableName(t)] = t;
|
||||
}
|
||||
});
|
||||
|
||||
@ -103,7 +112,7 @@ class DataSubSidebar extends React.Component {
|
||||
tableLinks = Object.keys(tables)
|
||||
.sort()
|
||||
.map((tableName, i) => {
|
||||
let _childLink;
|
||||
const table = tables[tableName];
|
||||
|
||||
let activeTableClass = '';
|
||||
if (
|
||||
@ -113,61 +122,27 @@ class DataSubSidebar extends React.Component {
|
||||
activeTableClass = styles.activeTable;
|
||||
}
|
||||
|
||||
const gqlCompatibilityWarning = !gqlPattern.test(tableName) ? (
|
||||
<span className={styles.add_mar_left_mid}>
|
||||
<GqlCompatibilityWarning />
|
||||
</span>
|
||||
) : null;
|
||||
|
||||
if (tables[tableName].table_type === 'BASE TABLE') {
|
||||
_childLink = (
|
||||
<li className={activeTableClass} key={i}>
|
||||
<Link
|
||||
to={
|
||||
appPrefix +
|
||||
'/schema/' +
|
||||
currentSchema +
|
||||
'/tables/' +
|
||||
tableName +
|
||||
'/browse'
|
||||
}
|
||||
data-test={tableName}
|
||||
>
|
||||
<i
|
||||
className={styles.tableIcon + ' fa fa-table'}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{tableName}
|
||||
</Link>
|
||||
{gqlCompatibilityWarning}
|
||||
</li>
|
||||
);
|
||||
} else {
|
||||
_childLink = (
|
||||
<li className={activeTableClass} key={i}>
|
||||
<Link
|
||||
to={
|
||||
appPrefix +
|
||||
'/schema/' +
|
||||
currentSchema +
|
||||
'/views/' +
|
||||
tableName +
|
||||
'/browse'
|
||||
}
|
||||
data-test={tableName}
|
||||
>
|
||||
<i
|
||||
className={styles.tableIcon + ' fa fa-table'}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<i>{tableName}</i>
|
||||
</Link>
|
||||
{gqlCompatibilityWarning}
|
||||
</li>
|
||||
let gqlCompatibilityWarning = null;
|
||||
if (!gqlPattern.test(tableName)) {
|
||||
gqlCompatibilityWarning = (
|
||||
<span className={styles.add_mar_left_mid}>
|
||||
<GqlCompatibilityWarning />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return _childLink;
|
||||
return (
|
||||
<li className={activeTableClass} key={i}>
|
||||
<Link to={getTableBrowseRoute(table)} data-test={tableName}>
|
||||
<i
|
||||
className={styles.tableIcon + ' fa fa-table'}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{displayTableName(table)}
|
||||
</Link>
|
||||
{gqlCompatibilityWarning}
|
||||
</li>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@ -179,38 +154,23 @@ class DataSubSidebar extends React.Component {
|
||||
|
||||
// If the listedFunctions is non empty
|
||||
if (listedFunctions.length > 0) {
|
||||
const functionHtml = listedFunctions.map((f, i) => (
|
||||
<li
|
||||
className={
|
||||
f.function_name === currentFunction ? styles.activeTable : ''
|
||||
}
|
||||
key={'fn ' + i}
|
||||
>
|
||||
<Link
|
||||
to={
|
||||
appPrefix +
|
||||
'/schema/' +
|
||||
currentSchema +
|
||||
'/functions/' +
|
||||
f.function_name
|
||||
}
|
||||
data-test={f.function_name}
|
||||
>
|
||||
<div
|
||||
className={styles.display_inline + ' ' + styles.functionIcon}
|
||||
>
|
||||
<img
|
||||
src={
|
||||
f.function_name === currentFunction
|
||||
? functionSymbolActive
|
||||
: functionSymbol
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{f.function_name}
|
||||
</Link>
|
||||
</li>
|
||||
));
|
||||
const functionHtml = listedFunctions.map((func, i) => {
|
||||
const funcName = getFunctionName(func);
|
||||
const isActive = funcName === currentFunction;
|
||||
|
||||
return (
|
||||
<li className={isActive ? styles.activeTable : ''} key={'fn ' + i}>
|
||||
<Link to={getFunctionModifyRoute(func)} data-test={funcName}>
|
||||
<div
|
||||
className={styles.display_inline + ' ' + styles.functionIcon}
|
||||
>
|
||||
<img src={isActive ? functionSymbolActive : functionSymbol} />
|
||||
</div>
|
||||
{getFunctionName(func)}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
|
||||
tableLinks = [...tableLinks, ...dividerHr, ...functionHtml];
|
||||
} else if (
|
||||
@ -234,7 +194,7 @@ class DataSubSidebar extends React.Component {
|
||||
showAddBtn={migrationMode}
|
||||
searchInput={getSearchInput()}
|
||||
heading={`Tables (${trackedTablesLength})`}
|
||||
addLink={'/data/schema/' + currentSchema + '/table/add'}
|
||||
addLink={getSchemaAddTableRoute(currentSchema)}
|
||||
addLabel={'Add Table'}
|
||||
addTestString={'sidebar-add-table'}
|
||||
childListTestString={'table-links'}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,73 @@
|
||||
@import "../../../Common/Common";
|
||||
|
||||
//.rolesTableWrapper {
|
||||
//}
|
||||
|
||||
.rolesTable {
|
||||
width: fit-content;
|
||||
|
||||
th, td {
|
||||
vertical-align: middle !important;
|
||||
}
|
||||
|
||||
th {
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
td {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
td pre {
|
||||
max-height: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
.selected,
|
||||
.selected th {
|
||||
background-color: #FFF3D5 !important;
|
||||
}
|
||||
|
||||
.permissionSymbolNA {
|
||||
color: firebrick;
|
||||
}
|
||||
|
||||
.permissionSymbolFA {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.permissionSymbolNA,
|
||||
.permissionSymbolFA,
|
||||
.permissionSymbolPA {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
// TODO: make common with Permissions page
|
||||
.actionIcon {
|
||||
font-size: 12px;
|
||||
color: #337ab7;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.clickableCell {
|
||||
cursor: pointer;
|
||||
|
||||
.actionIcon {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.clickableCell:hover {
|
||||
background-color: #ebf7de;
|
||||
|
||||
.actionIcon {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.clickableCell,
|
||||
.actionCell {
|
||||
.flex_space_between > div:first-child {
|
||||
width: calc(100% - 20px);
|
||||
}
|
||||
}
|
106
console/src/components/Services/Data/PermissionsSummary/utils.js
Normal file
106
console/src/components/Services/Data/PermissionsSummary/utils.js
Normal file
@ -0,0 +1,106 @@
|
||||
import React from 'react';
|
||||
import styles from './PermissionsSummary.scss';
|
||||
|
||||
export const permissionsSymbols = {
|
||||
fullAccess: (
|
||||
<i
|
||||
className={'fa fa-check ' + styles.permissionSymbolFA}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
),
|
||||
noAccess: (
|
||||
<i
|
||||
className={'fa fa-times ' + styles.permissionSymbolNA}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
),
|
||||
partialAccess: (
|
||||
<i
|
||||
className={'fa fa-filter ' + styles.permissionSymbolPA}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
export const getAllRoles = allTableSchemas => {
|
||||
const _allRoles = [];
|
||||
|
||||
allTableSchemas.forEach(tableSchema => {
|
||||
if (tableSchema.permissions) {
|
||||
tableSchema.permissions.forEach(p => {
|
||||
if (!_allRoles.includes(p.role_name)) {
|
||||
_allRoles.push(p.role_name);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
_allRoles.sort();
|
||||
|
||||
return _allRoles;
|
||||
};
|
||||
|
||||
export const getTablePermissionsByRoles = tableSchema => {
|
||||
const tablePermissionsByRoles = {};
|
||||
|
||||
tableSchema.permissions.forEach(
|
||||
p => (tablePermissionsByRoles[p.role_name] = p.permissions)
|
||||
);
|
||||
|
||||
return tablePermissionsByRoles;
|
||||
};
|
||||
|
||||
const getQueryFilterKey = query => {
|
||||
return query === 'insert' ? 'check' : 'filter';
|
||||
};
|
||||
|
||||
export const getPermissionFilterString = (
|
||||
permission,
|
||||
query,
|
||||
pretty = false
|
||||
) => {
|
||||
const filterKey = getQueryFilterKey(query);
|
||||
|
||||
let filterString = '';
|
||||
if (permission) {
|
||||
filterString = pretty
|
||||
? JSON.stringify(permission[filterKey], null, 2)
|
||||
: JSON.stringify(permission[filterKey]);
|
||||
}
|
||||
|
||||
return filterString;
|
||||
};
|
||||
|
||||
export const getPermissionColumnAccessSummary = (permission, tableColumns) => {
|
||||
let columnAccessStatus;
|
||||
|
||||
if (!permission || !permission.columns.length) {
|
||||
columnAccessStatus = 'no columns';
|
||||
} else if (
|
||||
permission.columns === '*' ||
|
||||
permission.columns.length === tableColumns.length
|
||||
) {
|
||||
columnAccessStatus = 'all columns';
|
||||
} else {
|
||||
columnAccessStatus = 'partial columns';
|
||||
}
|
||||
|
||||
return columnAccessStatus;
|
||||
};
|
||||
|
||||
export const getPermissionRowAccessSummary = filterString => {
|
||||
let rowAccessStatus;
|
||||
|
||||
const noAccess = filterString === '';
|
||||
const noChecks = filterString === '{}';
|
||||
|
||||
if (noAccess) {
|
||||
rowAccessStatus = 'no access';
|
||||
} else if (noChecks) {
|
||||
rowAccessStatus = 'without any checks';
|
||||
} else {
|
||||
rowAccessStatus = 'with custom check';
|
||||
}
|
||||
|
||||
return rowAccessStatus;
|
||||
};
|
@ -3,8 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import Helmet from 'react-helmet';
|
||||
import AceEditor from 'react-ace';
|
||||
import 'brace/mode/sql';
|
||||
import Modal from 'react-bootstrap/lib/Modal';
|
||||
import ModalButton from 'react-bootstrap/lib/Button';
|
||||
import Modal from '../../../Common/Modal/Modal';
|
||||
import Button from '../../../Common/Button/Button';
|
||||
import { parseCreateSQL } from './utils';
|
||||
|
||||
@ -142,30 +141,22 @@ const RawSQL = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal show={isModalOpen} onHide={onModalClose.bind(this)}>
|
||||
<Modal.Header closeModalButton>
|
||||
<Modal.Title>Run SQL</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<div className="content-fluid">
|
||||
<div className="row">
|
||||
<div className="col-xs-12">
|
||||
Your SQL Statement is most likely modifying the database schema.
|
||||
Are you sure its not a migration?
|
||||
</div>
|
||||
<Modal
|
||||
show={isModalOpen}
|
||||
title={'Run SQL'}
|
||||
onClose={onModalClose}
|
||||
onSubmit={onConfirmNoMigration}
|
||||
submitText={'Yes, i confirm'}
|
||||
submitTestId={'not-migration-confirm'}
|
||||
>
|
||||
<div className="content-fluid">
|
||||
<div className="row">
|
||||
<div className="col-xs-12">
|
||||
Your SQL Statement is most likely modifying the database schema.
|
||||
Are you sure its not a migration?
|
||||
</div>
|
||||
</div>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<ModalButton onClick={onModalClose}>Cancel</ModalButton>
|
||||
<ModalButton
|
||||
onClick={onConfirmNoMigration}
|
||||
bsStyle="primary"
|
||||
data-test="not-migration-confirm"
|
||||
>
|
||||
Yes, i confirm
|
||||
</ModalButton>
|
||||
</Modal.Footer>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
@ -1,12 +1,13 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Helmet from 'react-helmet';
|
||||
import { push } from 'react-router-redux';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger';
|
||||
|
||||
import {
|
||||
untrackedTip,
|
||||
untrackedTablesTip,
|
||||
untrackedRelTip,
|
||||
trackableFunctionsTip,
|
||||
nonTrackableFunctionsTip,
|
||||
@ -27,15 +28,16 @@ import {
|
||||
autoAddRelName,
|
||||
autoTrackRelations,
|
||||
} from '../TableRelationships/Actions';
|
||||
import globals from '../../../../Globals';
|
||||
import { getRelDef } from '../TableRelationships/utils';
|
||||
import {
|
||||
getSchemaAddTableRoute,
|
||||
getSchemaPermissionsRoute,
|
||||
} from '../../../Common/utils/routesUtils';
|
||||
import { createNewSchema, deleteCurrentSchema } from './Actions';
|
||||
import CollapsibleToggle from '../../../Common/CollapsibleToggle/CollapsibleToggle';
|
||||
import gqlPattern from '../Common/GraphQLValidation';
|
||||
import GqlCompatibilityWarning from '../../../Common/GqlCompatibilityWarning/GqlCompatibilityWarning';
|
||||
|
||||
const appPrefix = globals.urlPrefix + '/data';
|
||||
|
||||
class Schema extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@ -108,6 +110,24 @@ class Schema extends Component {
|
||||
return _untrackedTables.sort(tableSortFunc);
|
||||
};
|
||||
|
||||
const getSectionHeading = (headingText, tooltip, actionBtn = null) => {
|
||||
return (
|
||||
<div>
|
||||
<h4
|
||||
className={`${styles.subheading_text} ${styles.display_inline} ${
|
||||
styles.add_mar_right_mid
|
||||
}`}
|
||||
>
|
||||
{headingText}
|
||||
</h4>
|
||||
<OverlayTrigger placement="right" overlay={tooltip}>
|
||||
<i className="fa fa-info-circle" aria-hidden="true" />
|
||||
</OverlayTrigger>
|
||||
{actionBtn}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/***********/
|
||||
|
||||
const allUntrackedTables = getUntrackedTables();
|
||||
@ -120,7 +140,7 @@ class Schema extends Component {
|
||||
const handleClick = e => {
|
||||
e.preventDefault();
|
||||
|
||||
dispatch(push(`${appPrefix}/schema/${currentSchema}/table/add`));
|
||||
dispatch(push(getSchemaAddTableRoute(currentSchema)));
|
||||
};
|
||||
|
||||
createBtn = (
|
||||
@ -360,20 +380,10 @@ class Schema extends Component {
|
||||
return untrackedTablesList;
|
||||
};
|
||||
|
||||
const heading = (
|
||||
<div>
|
||||
<h4
|
||||
className={`${styles.subheading_text} ${styles.display_inline} ${
|
||||
styles.add_mar_right_mid
|
||||
}`}
|
||||
>
|
||||
Untracked tables or views
|
||||
</h4>
|
||||
<OverlayTrigger placement="right" overlay={untrackedTip}>
|
||||
<i className="fa fa-info-circle" aria-hidden="true" />
|
||||
</OverlayTrigger>
|
||||
{getTrackAllBtn()}
|
||||
</div>
|
||||
const heading = getSectionHeading(
|
||||
'Untracked tables or views',
|
||||
untrackedTablesTip,
|
||||
getTrackAllBtn()
|
||||
);
|
||||
|
||||
return (
|
||||
@ -470,20 +480,10 @@ class Schema extends Component {
|
||||
return untrackedRelList;
|
||||
};
|
||||
|
||||
const heading = (
|
||||
<div>
|
||||
<h4
|
||||
className={`${styles.subheading_text} ${styles.display_inline} ${
|
||||
styles.add_mar_right_mid
|
||||
}`}
|
||||
>
|
||||
Untracked foreign-key relations
|
||||
</h4>
|
||||
<OverlayTrigger placement="right" overlay={untrackedRelTip}>
|
||||
<i className="fa fa-info-circle" aria-hidden="true" />
|
||||
</OverlayTrigger>
|
||||
{getTrackAllBtn()}
|
||||
</div>
|
||||
const heading = getSectionHeading(
|
||||
'Untracked foreign-key relations',
|
||||
untrackedRelTip,
|
||||
getTrackAllBtn()
|
||||
);
|
||||
|
||||
return (
|
||||
@ -502,19 +502,9 @@ class Schema extends Component {
|
||||
let trackableFunctionList = null;
|
||||
|
||||
if (trackableFuncs.length > 0) {
|
||||
const heading = (
|
||||
<div>
|
||||
<h4
|
||||
className={`${styles.subheading_text} ${styles.display_inline} ${
|
||||
styles.add_mar_right_mid
|
||||
}`}
|
||||
>
|
||||
Untracked custom functions
|
||||
</h4>
|
||||
<OverlayTrigger placement="right" overlay={trackableFunctionsTip}>
|
||||
<i className="fa fa-info-circle" aria-hidden="true" />
|
||||
</OverlayTrigger>
|
||||
</div>
|
||||
const heading = getSectionHeading(
|
||||
'Untracked custom functions',
|
||||
trackableFunctionsTip
|
||||
);
|
||||
|
||||
trackableFunctionList = (
|
||||
@ -564,22 +554,9 @@ class Schema extends Component {
|
||||
let nonTrackableFuncList = null;
|
||||
|
||||
if (nonTrackableFunctions.length > 0) {
|
||||
const heading = (
|
||||
<div>
|
||||
<h4
|
||||
className={`${styles.subheading_text} ${styles.display_inline} ${
|
||||
styles.add_mar_right_mid
|
||||
}`}
|
||||
>
|
||||
Non trackable custom functions
|
||||
</h4>
|
||||
<OverlayTrigger
|
||||
placement="right"
|
||||
overlay={nonTrackableFunctionsTip}
|
||||
>
|
||||
<i className="fa fa-info-circle" aria-hidden="true" />
|
||||
</OverlayTrigger>
|
||||
</div>
|
||||
const heading = getSectionHeading(
|
||||
'Non trackable custom functions',
|
||||
nonTrackableFunctionsTip
|
||||
);
|
||||
|
||||
nonTrackableFuncList = (
|
||||
@ -613,6 +590,16 @@ class Schema extends Component {
|
||||
return nonTrackableFuncList;
|
||||
};
|
||||
|
||||
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} ${
|
||||
@ -632,6 +619,8 @@ class Schema extends Component {
|
||||
{getUntrackedRelationsSection()}
|
||||
{getUntrackedFunctionsSection()}
|
||||
{false && getNonTrackableFunctionsSection()}
|
||||
<hr />
|
||||
{getPermissionsSummaryLink()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,27 +1,27 @@
|
||||
import React from 'react';
|
||||
import Tooltip from 'react-bootstrap/lib/Tooltip';
|
||||
|
||||
export const untrackedTip = (
|
||||
<Tooltip id="tooltip-data-service">
|
||||
export const untrackedTablesTip = (
|
||||
<Tooltip id="tooltip-tables-untracked">
|
||||
Tables or views that are not exposed over the GraphQL API
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
export const untrackedRelTip = (
|
||||
<Tooltip id="tooltip-data-rel-service">
|
||||
<Tooltip id="tooltip-relationships-untracked">
|
||||
Relationships inferred via foreign-keys that are not exposed over the
|
||||
GraphQL API
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
export const trackableFunctionsTip = (
|
||||
<Tooltip id="tooltip-permission-read">
|
||||
<Tooltip id="tooltip-functions-untracked">
|
||||
Custom functions that are not exposed over the GraphQL API
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
export const nonTrackableFunctionsTip = (
|
||||
<Tooltip id="tooltip-permission-read">
|
||||
<Tooltip id="tooltip-functions-untrackable">
|
||||
Custom functions that do not conform to Hasura requirements
|
||||
</Tooltip>
|
||||
);
|
||||
|
@ -15,13 +15,6 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.limitInput {
|
||||
display: inline-block;
|
||||
width: 100px;
|
||||
margin-left: 15px;
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
.colEditor {
|
||||
:global(.form-control):focus {
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 0 rgba(102, 175, 233, 0.6);
|
||||
@ -33,12 +26,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.newRoleInput {
|
||||
margin-left: 20px;
|
||||
width: calc(100% - 40px);
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.input {
|
||||
margin-right: 10px;
|
||||
}
|
||||
@ -47,53 +34,14 @@
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.column_type_select {
|
||||
width: 200px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.modifyMinWidth {
|
||||
min-width: 735px;
|
||||
}
|
||||
|
||||
.leftIndent {
|
||||
padding-left: 0;
|
||||
margin-left: 0 !important;
|
||||
margin-right: 0 !important;
|
||||
|
||||
.insertRatio {
|
||||
.paddRight {
|
||||
select {
|
||||
width: 150px;
|
||||
display: inline-block;
|
||||
margin: 0 10px;
|
||||
height: 28px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.removeBtnPadding {
|
||||
padding-left: 66px;
|
||||
}
|
||||
|
||||
.fkSelect {
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.fkInEdit {
|
||||
display: inline-block;
|
||||
width: 47%;
|
||||
}
|
||||
|
||||
.fkInEditLeft {
|
||||
margin-right: 1%;
|
||||
}
|
||||
|
||||
.nullable {
|
||||
margin: 0 0 0 !important;
|
||||
margin-right: 5px !important;
|
||||
@ -114,223 +62,10 @@ hr {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.relBlockInline {
|
||||
display: inline-block;
|
||||
width: 16%;
|
||||
}
|
||||
|
||||
.relBlockLeft {
|
||||
text-align: right;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.relBlockRight {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
.radioWrapperAccess {
|
||||
background-color: #F2F4F6;
|
||||
padding: 20px;
|
||||
clear: both;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.greenCircle, .redCircle {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.greenCircle {
|
||||
background-color: #73ca45;
|
||||
}
|
||||
|
||||
.redCircle {
|
||||
background-color: #fd886b;
|
||||
}
|
||||
|
||||
.quickDefaultsPadd {
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.quickDefaultsLeft {
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.permissionsTable {
|
||||
width: 85%;
|
||||
|
||||
td, th {
|
||||
text-align: center;
|
||||
vertical-align: middle !important;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
thead {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
td:first-child, th:first-child {
|
||||
border-right: 4px double #ddd;
|
||||
font-weight: bold;
|
||||
width: 10%;
|
||||
}
|
||||
|
||||
td:nth-child(2) {
|
||||
overflow: auto;
|
||||
max-width: 250px;
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
//.permissionDelete {
|
||||
// cursor: pointer;
|
||||
// margin-left: 10px;
|
||||
//}
|
||||
|
||||
.editPermsLink {
|
||||
font-size: 12px;
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
color: #337ab7;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.bulkSelect {
|
||||
margin-right: 10px !important;
|
||||
}
|
||||
|
||||
.clickableCell {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.clickableCell:hover {
|
||||
background-color: #efefef;
|
||||
}
|
||||
|
||||
.currEdit, .currEdit:hover {
|
||||
background-color: #FFF3D5;
|
||||
color: #FD9540;
|
||||
}
|
||||
}
|
||||
|
||||
.permissionsLegend {
|
||||
font-size: 12px;
|
||||
margin-right: 5px;
|
||||
margin-bottom: 20px;
|
||||
//float: right;
|
||||
//clear: both;
|
||||
|
||||
.permissionsLegendValue {
|
||||
margin-right: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.permissionSymbolNA {
|
||||
color: firebrick;
|
||||
}
|
||||
|
||||
.permissionSymbolFA {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.permissionSymbolNA,
|
||||
.permissionSymbolFA,
|
||||
.permissionSymbolPA {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.applyBulkPermissions {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.samePermissionRole {
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.activeEdit {
|
||||
padding: 10px;
|
||||
background: #fff;
|
||||
border: 1px solid #ccc;
|
||||
margin-bottom: 15px;
|
||||
// overflow: auto;
|
||||
|
||||
.editPermsHeading {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
margin-bottom: 20px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.sectionStatus {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.editPermsSection {
|
||||
margin: 5px;
|
||||
padding: 10px;
|
||||
word-wrap: break-word;
|
||||
|
||||
.columnListElement {
|
||||
float: left;
|
||||
margin-right: 50px;
|
||||
}
|
||||
|
||||
.form_permission_insert_set_wrapper {
|
||||
.permission_insert_set_wrapper {
|
||||
.configure_insert_set_checkbox {
|
||||
position: relative;
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
label {
|
||||
cursor: pointer;
|
||||
min-height: 20px;
|
||||
font-weight: normal;
|
||||
|
||||
input:not([disabled]) {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.insertSetConfigRow {
|
||||
margin: 10px 0px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.input_element_wrapper {
|
||||
width: 20%;
|
||||
|
||||
i {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.set_warning {
|
||||
margin-left: 25px;
|
||||
|
||||
.danger_text {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chevron_mar_right {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
@ -9,6 +9,17 @@ import dataHeaders from '../Common/Headers';
|
||||
import { globalCookiePolicy } from '../../../../Endpoints';
|
||||
import requestAction from '../../../../utils/requestAction';
|
||||
import Endpoints from '../../../../Endpoints';
|
||||
import {
|
||||
findTable,
|
||||
generateTableDef,
|
||||
getSchemaTables,
|
||||
getTableDef,
|
||||
getTablePermissions,
|
||||
} from '../../../Common/utils/pgUtils';
|
||||
import {
|
||||
getCreatePermissionQuery,
|
||||
getDropPermissionQuery,
|
||||
} from '../../../Common/utils/v1QueryUtils';
|
||||
|
||||
export const PERM_ADD_TABLE_SCHEMAS = 'ModifyTable/PERM_ADD_TABLE_SCHEMAS';
|
||||
export const PERM_OPEN_EDIT = 'ModifyTable/PERM_OPEN_EDIT';
|
||||
@ -234,7 +245,11 @@ const updateApplySamePerms = (permissionsState, data, isDelete) => {
|
||||
applySamePerms.splice(data, 1);
|
||||
} else {
|
||||
if (data.index === applySamePerms.length) {
|
||||
applySamePerms.push({ table: '', role: '', action: '' });
|
||||
applySamePerms.push({
|
||||
table: permissionsState.table,
|
||||
action: permissionsState.query,
|
||||
role: '',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -415,6 +430,9 @@ const permRemoveMultipleRoles = tableSchema => {
|
||||
|
||||
const applySamePermissionsBulk = tableSchema => {
|
||||
return (dispatch, getState) => {
|
||||
const permissionsUpQueries = [];
|
||||
const permissionsDownQueries = [];
|
||||
|
||||
const allSchemas = getState().tables.allSchemas;
|
||||
const currentSchema = getState().tables.currentSchema;
|
||||
const permissionsState = getState().tables.modify.permissionsState;
|
||||
@ -425,16 +443,13 @@ const applySamePermissionsBulk = tableSchema => {
|
||||
|
||||
const mainApplyTo = {
|
||||
table: table,
|
||||
role: permissionsState.role,
|
||||
action: currentQueryType,
|
||||
role: permissionsState.role,
|
||||
};
|
||||
|
||||
const permApplyToList = permissionsState.applySamePermissions.concat([
|
||||
mainApplyTo,
|
||||
]);
|
||||
|
||||
const permissionsUpQueries = [];
|
||||
const permissionsDownQueries = [];
|
||||
const permApplyToList = permissionsState.applySamePermissions
|
||||
.filter(applyTo => applyTo.table && applyTo.action && applyTo.role)
|
||||
.concat([mainApplyTo]);
|
||||
|
||||
let currentPermissions = [];
|
||||
allSchemas.forEach(tSchema => {
|
||||
@ -511,9 +526,9 @@ const applySamePermissionsBulk = tableSchema => {
|
||||
const migrationName =
|
||||
'apply_same_permissions_' + currentSchema + '_table_' + table;
|
||||
|
||||
const requestMsg = 'Applying Permissions';
|
||||
const successMsg = 'Permission Changes Applied';
|
||||
const errorMsg = 'Permission Changes Failed';
|
||||
const requestMsg = 'Applying permissions';
|
||||
const successMsg = 'Permission changes applied';
|
||||
const errorMsg = 'Permission changes failed';
|
||||
|
||||
const customOnSuccess = () => {
|
||||
// reset new role name
|
||||
@ -540,6 +555,119 @@ const applySamePermissionsBulk = tableSchema => {
|
||||
};
|
||||
};
|
||||
|
||||
const copyRolePermissions = (
|
||||
fromRole,
|
||||
tableNameWithSchema,
|
||||
action,
|
||||
toRoles,
|
||||
onSuccess
|
||||
) => {
|
||||
return (dispatch, getState) => {
|
||||
const permissionsUpQueries = [];
|
||||
const permissionsDownQueries = [];
|
||||
|
||||
const allSchemas = getState().tables.allSchemas;
|
||||
const currentSchema = getState().tables.currentSchema;
|
||||
|
||||
let tables;
|
||||
if (tableNameWithSchema === 'all') {
|
||||
tables = getSchemaTables(allSchemas, currentSchema);
|
||||
} else {
|
||||
const fromTableDef = generateTableDef(null, null, tableNameWithSchema);
|
||||
tables = [findTable(allSchemas, fromTableDef)];
|
||||
}
|
||||
|
||||
tables.forEach(table => {
|
||||
const tableDef = getTableDef(table);
|
||||
|
||||
let actions;
|
||||
if (action === 'all') {
|
||||
actions = ['select', 'insert', 'update', 'delete'];
|
||||
} else {
|
||||
actions = [action];
|
||||
}
|
||||
|
||||
toRoles.forEach(toRole => {
|
||||
actions.forEach(_action => {
|
||||
const currPermissions = getTablePermissions(table, toRole, _action);
|
||||
const toBeAppliedPermissions = getTablePermissions(
|
||||
table,
|
||||
fromRole,
|
||||
_action
|
||||
);
|
||||
|
||||
if (currPermissions) {
|
||||
// existing permission is there. so drop and recreate for down migrations
|
||||
const deleteQuery = getDropPermissionQuery(
|
||||
_action,
|
||||
tableDef,
|
||||
toRole
|
||||
);
|
||||
const createQuery = getCreatePermissionQuery(
|
||||
_action,
|
||||
tableDef,
|
||||
toRole,
|
||||
currPermissions
|
||||
);
|
||||
|
||||
permissionsUpQueries.push(deleteQuery);
|
||||
permissionsDownQueries.push(createQuery);
|
||||
}
|
||||
|
||||
if (toBeAppliedPermissions) {
|
||||
// now add normal create and drop permissions
|
||||
const createQuery = getCreatePermissionQuery(
|
||||
_action,
|
||||
tableDef,
|
||||
toRole,
|
||||
toBeAppliedPermissions
|
||||
);
|
||||
const deleteQuery = getDropPermissionQuery(
|
||||
_action,
|
||||
tableDef,
|
||||
toRole
|
||||
);
|
||||
|
||||
permissionsUpQueries.push(createQuery);
|
||||
permissionsDownQueries.push(deleteQuery);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Apply migration
|
||||
const migrationName =
|
||||
'copy_role_' +
|
||||
fromRole +
|
||||
'_' +
|
||||
action +
|
||||
'_query_permissions_for_' +
|
||||
tableNameWithSchema.replace('.', '_') +
|
||||
'_table_to_' +
|
||||
toRoles.join('_');
|
||||
|
||||
const requestMsg = 'Copying permissions';
|
||||
const successMsg = 'Permissions copied';
|
||||
const errorMsg = 'Permissions copy failed';
|
||||
|
||||
const customOnSuccess = onSuccess;
|
||||
const customOnError = () => {};
|
||||
|
||||
makeMigrationCall(
|
||||
dispatch,
|
||||
getState,
|
||||
permissionsUpQueries,
|
||||
permissionsDownQueries,
|
||||
migrationName,
|
||||
customOnSuccess,
|
||||
customOnError,
|
||||
requestMsg,
|
||||
successMsg,
|
||||
errorMsg
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
const permChangePermissions = changeType => {
|
||||
return (dispatch, getState) => {
|
||||
const allSchemas = getState().tables.allSchemas;
|
||||
@ -700,4 +828,5 @@ export {
|
||||
permSetApplySamePerm,
|
||||
permDelApplySamePerm,
|
||||
applySamePermissionsBulk,
|
||||
copyRolePermissions,
|
||||
};
|
||||
|
@ -45,6 +45,14 @@ import EnhancedInput from '../../../Common/InputChecker/InputChecker';
|
||||
import { setTable } from '../DataActions';
|
||||
import { getIngForm, getEdForm, escapeRegExp } from '../utils';
|
||||
import { allOperators, getLegacyOperator } from './PermissionBuilder/utils';
|
||||
import {
|
||||
permissionsSymbols,
|
||||
getAllRoles,
|
||||
getPermissionFilterString,
|
||||
getPermissionColumnAccessSummary,
|
||||
getTablePermissionsByRoles,
|
||||
getPermissionRowAccessSummary,
|
||||
} from '../PermissionsSummary/utils';
|
||||
import Button from '../../../Common/Button/Button';
|
||||
import { defaultPresetsState } from '../DataState';
|
||||
|
||||
@ -71,6 +79,32 @@ class Permissions extends Component {
|
||||
this.props.dispatch(setTable(this.props.tableName));
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const currPermissionsState = this.props.permissionsState;
|
||||
const prevPermissionsState = prevProps.permissionsState;
|
||||
|
||||
// scroll to edit section if role/query change
|
||||
if (
|
||||
(currPermissionsState.role &&
|
||||
currPermissionsState.role !== prevPermissionsState.role) ||
|
||||
(currPermissionsState.query &&
|
||||
currPermissionsState.query !== prevPermissionsState.query)
|
||||
) {
|
||||
document
|
||||
.getElementById('permission-edit-section')
|
||||
.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
|
||||
if (
|
||||
!prevPermissionsState.bulkSelect.length &&
|
||||
currPermissionsState.bulkSelect.length
|
||||
) {
|
||||
document
|
||||
.getElementById('bulk-section')
|
||||
.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
dispatch,
|
||||
@ -97,25 +131,7 @@ class Permissions extends Component {
|
||||
throw new NotFoundError();
|
||||
}
|
||||
|
||||
const styles = require('../TableModify/ModifyTable.scss');
|
||||
|
||||
const getAllRoles = allTableSchemas => {
|
||||
const _allRoles = [];
|
||||
|
||||
allTableSchemas.forEach(tableSchema => {
|
||||
if (tableSchema.permissions) {
|
||||
tableSchema.permissions.forEach(p => {
|
||||
if (!_allRoles.includes(p.role_name)) {
|
||||
_allRoles.push(p.role_name);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
_allRoles.sort();
|
||||
|
||||
return _allRoles;
|
||||
};
|
||||
const styles = require('./Permissions.scss');
|
||||
|
||||
const addTooltip = (text, tooltip) => {
|
||||
return (
|
||||
@ -128,10 +144,6 @@ class Permissions extends Component {
|
||||
);
|
||||
};
|
||||
|
||||
const getQueryFilterKey = query => {
|
||||
return query === 'insert' ? 'check' : 'filter';
|
||||
};
|
||||
|
||||
/********************/
|
||||
|
||||
const getAlertHtml = (
|
||||
@ -194,27 +206,6 @@ class Permissions extends Component {
|
||||
};
|
||||
|
||||
const getPermissionsTable = (tableSchema, queryTypes, roleList) => {
|
||||
const permissionsSymbols = {
|
||||
fullAccess: (
|
||||
<i
|
||||
className={'fa fa-check ' + styles.permissionSymbolFA}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
),
|
||||
noAccess: (
|
||||
<i
|
||||
className={'fa fa-times ' + styles.permissionSymbolNA}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
),
|
||||
partialAccess: (
|
||||
<i
|
||||
className={'fa fa-filter ' + styles.permissionSymbolPA}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
const getPermissionsLegend = () => (
|
||||
<div>
|
||||
<div className={styles.permissionsLegend}>
|
||||
@ -263,13 +254,17 @@ class Permissions extends Component {
|
||||
const getPermissionsTableHead = () => {
|
||||
const _permissionsHead = [];
|
||||
|
||||
_permissionsHead.push(<td key={-1}>Actions</td>);
|
||||
_permissionsHead.push(<td key={-2}>Role</td>);
|
||||
// push role head
|
||||
_permissionsHead.push(<th key={-2}>Role</th>);
|
||||
|
||||
// push action heads
|
||||
queryTypes.forEach((queryType, i) => {
|
||||
_permissionsHead.push(<td key={i}>{queryType}</td>);
|
||||
_permissionsHead.push(<th key={i}>{queryType}</th>);
|
||||
});
|
||||
|
||||
// push bulk actions head
|
||||
_permissionsHead.push(<th key={-1} />);
|
||||
|
||||
return (
|
||||
<thead>
|
||||
<tr>{_permissionsHead}</tr>
|
||||
@ -289,7 +284,7 @@ class Permissions extends Component {
|
||||
} else if (role !== '') {
|
||||
dispatch(permOpenEdit(tableSchema, role, queryType));
|
||||
} else {
|
||||
document.getElementById('newRoleInput').focus();
|
||||
document.getElementById('new-role-input').focus();
|
||||
}
|
||||
};
|
||||
|
||||
@ -316,9 +311,9 @@ class Permissions extends Component {
|
||||
// }
|
||||
// };
|
||||
|
||||
const getEditLink = () => {
|
||||
const getEditIcon = () => {
|
||||
return (
|
||||
<span className={styles.editPermsLink}>
|
||||
<span className={styles.editPermsIcon}>
|
||||
<i className="fa fa-pencil" aria-hidden="true" />
|
||||
</span>
|
||||
);
|
||||
@ -327,10 +322,7 @@ class Permissions extends Component {
|
||||
const getRoleQueryPermission = queryType => {
|
||||
let _permission;
|
||||
|
||||
const rolePermissions = {};
|
||||
tableSchema.permissions.forEach(
|
||||
p => (rolePermissions[p.role_name] = p.permissions)
|
||||
);
|
||||
const rolePermissions = getTablePermissionsByRoles(tableSchema);
|
||||
|
||||
if (role === 'admin') {
|
||||
_permission = permissionsSymbols.fullAccess;
|
||||
@ -378,6 +370,65 @@ class Permissions extends Component {
|
||||
};
|
||||
|
||||
const _permissionsRowHtml = [];
|
||||
|
||||
// push role value
|
||||
if (newPermRow) {
|
||||
const isNewRole = !roleList.includes(permissionsState.newRole);
|
||||
|
||||
_permissionsRowHtml.push(
|
||||
<th key={-2}>
|
||||
<input
|
||||
id="new-role-input"
|
||||
className={`form-control ${styles.newRoleInput}`}
|
||||
onChange={dispatchRoleNameChange}
|
||||
type="text"
|
||||
placeholder="Enter new role"
|
||||
value={isNewRole ? permissionsState.newRole : ''}
|
||||
data-test="role-textbox"
|
||||
/>
|
||||
</th>
|
||||
);
|
||||
} else {
|
||||
_permissionsRowHtml.push(<th key={-2}>{role}</th>);
|
||||
}
|
||||
|
||||
// push action permission value
|
||||
queryTypes.forEach((queryType, i) => {
|
||||
const isEditAllowed = role !== 'admin';
|
||||
const isCurrEdit =
|
||||
permissionsState.role === role &&
|
||||
permissionsState.query === queryType;
|
||||
|
||||
let editIcon = '';
|
||||
let className = '';
|
||||
let onClick = () => {};
|
||||
if (isEditAllowed) {
|
||||
className += styles.clickableCell;
|
||||
editIcon = getEditIcon();
|
||||
|
||||
if (isCurrEdit) {
|
||||
onClick = dispatchCloseEdit;
|
||||
className += ' ' + styles.currEdit;
|
||||
} else {
|
||||
onClick = dispatchOpenEdit(queryType);
|
||||
}
|
||||
}
|
||||
|
||||
_permissionsRowHtml.push(
|
||||
<td
|
||||
key={i}
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
title="Edit permissions"
|
||||
data-test={`${role}-${queryType}`}
|
||||
>
|
||||
{getRoleQueryPermission(queryType)}
|
||||
{editIcon}
|
||||
</td>
|
||||
);
|
||||
});
|
||||
|
||||
// push bulk action value
|
||||
if (role === 'admin' || role === '') {
|
||||
_permissionsRowHtml.push(<td key={-1} />);
|
||||
} else {
|
||||
@ -408,73 +459,20 @@ class Permissions extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (newPermRow) {
|
||||
const isNewRole = !roleList.includes(permissionsState.newRole);
|
||||
|
||||
_permissionsRowHtml.push(
|
||||
<td key={-2}>
|
||||
<input
|
||||
id="newRoleInput"
|
||||
className={`form-control ${styles.newRoleInput}`}
|
||||
onChange={dispatchRoleNameChange}
|
||||
type="text"
|
||||
placeholder="Enter new role"
|
||||
value={isNewRole ? permissionsState.newRole : ''}
|
||||
data-test="role-textbox"
|
||||
/>
|
||||
</td>
|
||||
);
|
||||
} else {
|
||||
_permissionsRowHtml.push(<td key={-2}>{role}</td>);
|
||||
}
|
||||
|
||||
queryTypes.forEach((queryType, i) => {
|
||||
const isEditAllowed = role !== 'admin';
|
||||
const isCurrEdit =
|
||||
permissionsState.role === role &&
|
||||
permissionsState.query === queryType;
|
||||
|
||||
let editLink = '';
|
||||
let className = '';
|
||||
let onClick = () => {};
|
||||
if (isEditAllowed) {
|
||||
editLink = getEditLink();
|
||||
|
||||
className += styles.clickableCell;
|
||||
onClick = dispatchOpenEdit(queryType);
|
||||
if (isCurrEdit) {
|
||||
onClick = dispatchCloseEdit;
|
||||
className += ` ${styles.currEdit}`;
|
||||
}
|
||||
}
|
||||
|
||||
_permissionsRowHtml.push(
|
||||
<td
|
||||
key={i}
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
title="Edit permissions"
|
||||
data-test={`${role}-${queryType}`}
|
||||
>
|
||||
{getRoleQueryPermission(queryType)}
|
||||
{editLink}
|
||||
</td>
|
||||
);
|
||||
});
|
||||
|
||||
return _permissionsRowHtml;
|
||||
};
|
||||
|
||||
// add admin to roles
|
||||
const _roleList = ['admin'].concat(roleList);
|
||||
|
||||
// add existing roles rows
|
||||
_roleList.forEach((role, i) => {
|
||||
_permissionsRowsHtml.push(
|
||||
<tr key={i}>{getPermissionsTableRow(role)}</tr>
|
||||
);
|
||||
});
|
||||
|
||||
// new role row
|
||||
// add new role row
|
||||
_permissionsRowsHtml.push(
|
||||
<tr key="newPerm">{getPermissionsTableRow('', true)}</tr>
|
||||
);
|
||||
@ -518,7 +516,7 @@ class Permissions extends Component {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.activeEdit}>
|
||||
<div id={'bulk-section'} className={styles.activeEdit}>
|
||||
<div className={styles.editPermsHeading}>Apply Bulk Actions</div>
|
||||
<div>
|
||||
<span className={styles.add_pad_right}>Selected Roles</span>
|
||||
@ -570,15 +568,12 @@ class Permissions extends Component {
|
||||
};
|
||||
|
||||
const getRowSection = () => {
|
||||
const filterKey = getQueryFilterKey(query);
|
||||
let filterString = getPermissionFilterString(
|
||||
permissionsState[query],
|
||||
query
|
||||
);
|
||||
|
||||
let filterString = '';
|
||||
if (permissionsState[query]) {
|
||||
filterString = JSON.stringify(permissionsState[query][filterKey]);
|
||||
}
|
||||
|
||||
const noAccess = filterString === '';
|
||||
const noChecks = filterString === '{}';
|
||||
const rowSectionStatus = getPermissionRowAccessSummary(filterString);
|
||||
|
||||
// replace legacy operator values
|
||||
allOperators.forEach(operator => {
|
||||
@ -612,12 +607,11 @@ class Permissions extends Component {
|
||||
return;
|
||||
}
|
||||
|
||||
const queryFilterKey = getQueryFilterKey(queryType);
|
||||
|
||||
let queryFilterString = '';
|
||||
if (permissionsState[queryType]) {
|
||||
queryFilterString = JSON.stringify(
|
||||
permissionsState[queryType][queryFilterKey]
|
||||
queryFilterString = getPermissionFilterString(
|
||||
permissionsState[queryType],
|
||||
queryType
|
||||
);
|
||||
}
|
||||
|
||||
@ -667,7 +661,9 @@ class Permissions extends Component {
|
||||
// TODO: add no access option
|
||||
|
||||
const addNoChecksOption = () => {
|
||||
const isSelected = !permissionsState.custom_checked && noChecks;
|
||||
const isSelected =
|
||||
!permissionsState.custom_checked &&
|
||||
rowSectionStatus === 'without any checks';
|
||||
|
||||
// Add allow all option
|
||||
let allowAllQueryInfo = '';
|
||||
@ -847,15 +843,6 @@ class Permissions extends Component {
|
||||
|
||||
const rowSectionTitle = 'Row ' + query + ' permissions';
|
||||
|
||||
let rowSectionStatus;
|
||||
if (noAccess) {
|
||||
rowSectionStatus = 'no access';
|
||||
} else if (noChecks) {
|
||||
rowSectionStatus = 'without any checks';
|
||||
} else {
|
||||
rowSectionStatus = 'with custom check';
|
||||
}
|
||||
|
||||
return (
|
||||
<CollapsibleToggle
|
||||
title={getSectionHeader(
|
||||
@ -865,7 +852,7 @@ class Permissions extends Component {
|
||||
)}
|
||||
useDefaultTitleStyle
|
||||
testId={'toggle-row-permission'}
|
||||
isOpen={noAccess}
|
||||
isOpen={rowSectionStatus === 'no access'}
|
||||
>
|
||||
<div className={styles.editPermsSection}>
|
||||
<div>
|
||||
@ -992,21 +979,10 @@ class Permissions extends Component {
|
||||
|
||||
const colSectionTitle = 'Column ' + query + ' permissions';
|
||||
|
||||
let colSectionStatus;
|
||||
if (
|
||||
!permissionsState[query] ||
|
||||
!permissionsState[query].columns.length
|
||||
) {
|
||||
colSectionStatus = 'no columns';
|
||||
} else if (
|
||||
permissionsState[query].columns === '*' ||
|
||||
permissionsState[query].columns.length ===
|
||||
tableSchema.columns.length
|
||||
) {
|
||||
colSectionStatus = 'all columns';
|
||||
} else {
|
||||
colSectionStatus = 'partial columns';
|
||||
}
|
||||
const colSectionStatus = getPermissionColumnAccessSummary(
|
||||
permissionsState[query],
|
||||
tableSchema.columns
|
||||
);
|
||||
|
||||
_columnSection = (
|
||||
<CollapsibleToggle
|
||||
@ -1527,7 +1503,7 @@ class Permissions extends Component {
|
||||
const actionsList = ['insert', 'select', 'update', 'delete'];
|
||||
|
||||
const getApplyToRow = (applyTo, index) => {
|
||||
const getSelect = (type, options) => {
|
||||
const getSelect = (type, options, value = '') => {
|
||||
const setApplyTo = e => {
|
||||
dispatch(permSetApplySamePerm(index, type, e.target.value));
|
||||
};
|
||||
@ -1548,7 +1524,7 @@ class Permissions extends Component {
|
||||
styles.add_mar_right +
|
||||
' input-sm form-control'
|
||||
}
|
||||
value={applyTo[type] || ''}
|
||||
value={applyTo[type] || value || ''}
|
||||
onChange={setApplyTo}
|
||||
disabled={noPermissions}
|
||||
title={noPermissions ? disabledCloneMsg : ''}
|
||||
@ -1568,7 +1544,7 @@ class Permissions extends Component {
|
||||
dispatch(permDelApplySamePerm(index));
|
||||
};
|
||||
|
||||
if (applyTo.table || applyTo.role || applyTo.action) {
|
||||
if (applyTo.table && applyTo.role && applyTo.action) {
|
||||
_removeIcon = (
|
||||
<i
|
||||
className={`${styles.fontAwosomeClose} fa-lg fa fa-times`}
|
||||
@ -1582,9 +1558,9 @@ class Permissions extends Component {
|
||||
|
||||
return (
|
||||
<div key={index} className={styles.add_mar_bottom_mid}>
|
||||
{getSelect('table', tableOptions)}
|
||||
{getSelect('table', tableOptions, permissionsState.table)}
|
||||
{getSelect('action', actionsList, permissionsState.query)}
|
||||
{getSelect('role', roleList)}
|
||||
{getSelect('action', actionsList)}
|
||||
{getRemoveIcon()}
|
||||
</div>
|
||||
);
|
||||
@ -1594,8 +1570,16 @@ class Permissions extends Component {
|
||||
_applyToListHtml.push(getApplyToRow(applyTo, i));
|
||||
});
|
||||
|
||||
// add empty row
|
||||
_applyToListHtml.push(getApplyToRow({}, applyToList.length));
|
||||
// add empty row (only if prev row is completely filled)
|
||||
const lastApplyTo = applyToList.length
|
||||
? applyToList[applyToList.length - 1]
|
||||
: null;
|
||||
if (
|
||||
!lastApplyTo ||
|
||||
(lastApplyTo.table && lastApplyTo.action && lastApplyTo.role)
|
||||
) {
|
||||
_applyToListHtml.push(getApplyToRow({}, applyToList.length));
|
||||
}
|
||||
|
||||
return _applyToListHtml;
|
||||
};
|
||||
@ -1610,6 +1594,10 @@ class Permissions extends Component {
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
const validApplyToList = permissionsState.applySamePermissions.filter(
|
||||
applyTo => applyTo.table && applyTo.action && applyTo.role
|
||||
);
|
||||
|
||||
clonePermissionsHtml = (
|
||||
<div>
|
||||
<hr />
|
||||
@ -1635,7 +1623,7 @@ class Permissions extends Component {
|
||||
className={styles.add_mar_top}
|
||||
color="yellow"
|
||||
size="sm"
|
||||
disabled={!permissionsState.applySamePermissions.length}
|
||||
disabled={!validApplyToList.length}
|
||||
>
|
||||
Save Permissions
|
||||
</Button>
|
||||
@ -1722,6 +1710,7 @@ class Permissions extends Component {
|
||||
|
||||
return (
|
||||
<div
|
||||
id={'permission-edit-section'}
|
||||
key={`${permissionsState.role}-${permissionsState.query}`}
|
||||
className={styles.activeEdit}
|
||||
>
|
||||
|
@ -0,0 +1,168 @@
|
||||
@import "../TableModify/ModifyTable.scss";
|
||||
|
||||
.permissionsTable {
|
||||
width: 85%;
|
||||
|
||||
td, th {
|
||||
text-align: center;
|
||||
vertical-align: middle !important;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
thead {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
td:last-child, th:last-child {
|
||||
border-left: 4px double #ddd;
|
||||
width: 10%;
|
||||
}
|
||||
|
||||
td:first-child, th:first-child {
|
||||
overflow: auto;
|
||||
border-right: 4px double #ddd;
|
||||
max-width: 250px;
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
//.permissionDelete {
|
||||
// cursor: pointer;
|
||||
// margin-left: 10px;
|
||||
//}
|
||||
|
||||
.bulkSelect {
|
||||
margin-right: 10px !important;
|
||||
}
|
||||
|
||||
// TODO: make common with Roles page
|
||||
.clickableCell {
|
||||
cursor: pointer;
|
||||
|
||||
.editPermsIcon {
|
||||
font-size: 12px;
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
color: #337ab7;
|
||||
cursor: pointer;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.clickableCell:hover {
|
||||
background-color: #ebf7de;
|
||||
|
||||
.editPermsIcon {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.currEdit, .currEdit:hover {
|
||||
background-color: #FFF3D5;
|
||||
color: #FD9540;
|
||||
|
||||
.editPermsIcon {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.permissionsLegend {
|
||||
font-size: 12px;
|
||||
margin-right: 5px;
|
||||
margin-bottom: 20px;
|
||||
//float: right;
|
||||
//clear: both;
|
||||
|
||||
.permissionsLegendValue {
|
||||
margin-right: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.newRoleInput {
|
||||
margin-left: 20px;
|
||||
width: calc(100% - 40px);
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.fkSelect {
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.fkInEdit {
|
||||
display: inline-block;
|
||||
width: 47%;
|
||||
}
|
||||
|
||||
.limitInput {
|
||||
display: inline-block;
|
||||
width: 100px;
|
||||
margin-left: 15px;
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
.activeEdit {
|
||||
.editPermsHeading {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
margin-bottom: 20px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.sectionStatus {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.editPermsSection {
|
||||
margin: 5px;
|
||||
padding: 10px;
|
||||
word-wrap: break-word;
|
||||
|
||||
.columnListElement {
|
||||
float: left;
|
||||
margin-right: 50px;
|
||||
}
|
||||
|
||||
.form_permission_insert_set_wrapper {
|
||||
.permission_insert_set_wrapper {
|
||||
.configure_insert_set_checkbox {
|
||||
position: relative;
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
label {
|
||||
cursor: pointer;
|
||||
min-height: 20px;
|
||||
font-weight: normal;
|
||||
|
||||
input:not([disabled]) {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.insertSetConfigRow {
|
||||
margin: 10px 0px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.input_element_wrapper {
|
||||
width: 20%;
|
||||
|
||||
i {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -11,6 +11,7 @@ export viewTableConnector from './TableBrowseRows/ViewTable';
|
||||
export addExistingTableViewConnector from './Add/AddExistingTableView';
|
||||
export addTableConnector from './Add/AddTable';
|
||||
export rawSQLConnector from './RawSQL/RawSQL';
|
||||
export permissionsSummaryConnector from './PermissionsSummary/PermissionsSummary';
|
||||
export insertItemConnector from './TableInsertItem/InsertItem';
|
||||
export editItemConnector from './TableBrowseRows/EditItem';
|
||||
export modifyTableConnector from './TableModify/ModifyTable';
|
||||
|
@ -77,7 +77,7 @@ const EventSubSidebar = ({
|
||||
data-test={trigger}
|
||||
>
|
||||
<i
|
||||
className={styles.tableIcon + ' fa fa-table'}
|
||||
className={styles.tableIcon + ' fa fa-send-o'}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{trigger}
|
||||
|
@ -65,7 +65,7 @@ const RemoteSchemaSubSidebar = ({
|
||||
data-test={d.name}
|
||||
>
|
||||
<i
|
||||
className={styles.tableIcon + ' fa fa-table'}
|
||||
className={styles.tableIcon + ' fa fa-code-fork'}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{d.name}
|
||||
|
Loading…
Reference in New Issue
Block a user