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:
Rikin Kachhia 2019-08-20 19:12:15 +05:30 committed by GitHub
parent 2c108daef8
commit 03ea997c3b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 2360 additions and 882 deletions

View File

@ -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);

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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

View 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;

View File

@ -140,15 +140,6 @@ a.expanded {
}
}
.iClickable {
}
.iClickable:hover {
cursor: pointer;
color: #b85c27;
transition: 0.2s;
}
.insertBox {
min-width: 270px;
}

View 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;

View File

@ -1,5 +1,5 @@
@import "../Common.scss";
.gqlCompatibilityWarning {
.warningSymbol {
color: #d9534f;
}

View 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;
};

View 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;
};

View 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`;
};

View File

@ -0,0 +1,3 @@
export const getPathRoot = path => {
return path.split('/')[1];
};

View 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,
},
};
};

View File

@ -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}
/>
&nbsp;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}>

View File

@ -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 {

View File

@ -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
}`;

View File

@ -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()}

View File

@ -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);

View File

@ -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>{' '}
&nbsp; &nbsp;
</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>{' '}
&nbsp; &nbsp;
</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>{' '}
&nbsp; &nbsp;
</h4>

View File

@ -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>&nbsp; &nbsp;
<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>{' '}
&nbsp; &nbsp;
</div>
@ -317,10 +314,7 @@ const ForeignKeySelector = ({
<div className={`${styles.add_mar_bottom_mid}`}>
<b>On Delete Violation:</b>&nbsp; &nbsp;
<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>{' '}
&nbsp; &nbsp;
</div>

View File

@ -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"

View File

@ -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

View File

@ -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);
}
}

View 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;
};

View File

@ -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>
);
};

View File

@ -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>
);

View File

@ -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>
);

View File

@ -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;
}

View File

@ -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,
};

View File

@ -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}
>

View File

@ -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%;
}
}
}
}
}
}
}

View File

@ -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';

View File

@ -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}

View File

@ -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}