console: add inherited roles tab

Co-authored-by: Abhijeet Singh Khangarot <26903230+abhi40308@users.noreply.github.com>
Co-authored-by: Aleksandra Sikora <9019397+beerose@users.noreply.github.com>
GitOrigin-RevId: b6edc26b96e2cd0db11e7951b7941a631932d125
This commit is contained in:
Vijay Prasanna 2021-03-10 16:14:15 +05:30 committed by hasura-bot
parent d16bc4fe46
commit 89e26d3e9f
21 changed files with 809 additions and 4 deletions

View File

@ -10,6 +10,8 @@
- server: inherited roles for PG queries and subscription
- server: fix issue when a remote relationship's joining field had a custom GraphQL name defined (fix #6626)
- server: fix handling of nullable object relationships (fix #6633)
- console: add inherited roles support (#483)
- console: add permissions support for mssql tables (#677)
- cli: support rest endpoints
- cli: support mssql sources
- cli: use relative paths in metadata !include directives
@ -27,7 +29,6 @@
- server/mssql: fix text values erroneously being parsed as varchar
- server: improve errors messages for inconsistent sources
- console: add relationship tab for mssql tables (#677)
- console: add permissions support for mssql tables (#677)
- build: fix the packaging of static console assets (fix #6610)
- server: make REST endpoint errors compatible with inconsistent metadata

View File

@ -217,4 +217,4 @@
"engines": {
"node": ">=8.9.1"
}
}
}

View File

@ -22,6 +22,7 @@ export interface MainState {
is_admin_secret_set: boolean;
is_auth_hook_set: boolean;
is_jwt_set: boolean;
experimental_features: string[];
jwt: {
claims_namespace: string;
claims_format: string;
@ -58,6 +59,7 @@ const defaultState: MainState = {
is_function_permissions_inferred: true,
is_admin_secret_set: false,
is_auth_hook_set: false,
experimental_features: [],
is_jwt_set: false,
jwt: {
claims_namespace: '',

View File

@ -0,0 +1,145 @@
import React, { useState } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import {
getInheritedRoles,
rolesSelector,
} from '../../../../metadata/selector';
import { Dispatch, ReduxState } from '../../../../types';
import styles from '../Settings.scss';
import InheritedRolesTable, {
InheritedRolesTableProps,
} from './InheritedRolesTable';
import { RoleActionsInterface } from './types';
import InheritedRolesEditor, { EditorProps } from './InheritedRolesEditor';
import { InheritedRole } from '../../../../metadata/types';
import { Badge, Heading } from '../../../UIKit/atoms';
import { getConfirmation } from '../../../Common/utils/jsUtils';
import {
deleteInheritedRoleAction,
addInheritedRoleAction,
updateInheritedRoleAction,
} from '../../../../metadata/actions';
export const ActionContext = React.createContext<RoleActionsInterface | null>(
null
);
const InheritedRoles: React.FC<Props> = props => {
const { allRoles, inheritedRoles, experimentalFeatures, dispatch } = props;
const [inheritedRoleName, setInheritedRoleName] = useState('');
const [inheritedRole, setInheritedRole] = useState<InheritedRole | null>(
null
);
const [isCollapsed, setIsCollapsed] = useState(true);
const setEditorState = (
roleName: string = inheritedRoleName,
role: InheritedRole | null = inheritedRole,
collapsed: boolean = isCollapsed
) => {
setIsCollapsed(collapsed);
setInheritedRole(role);
setInheritedRoleName(roleName);
};
const onAdd = (roleName: string) => {
setEditorState(roleName, null, false);
};
const onRoleNameChange = (roleName: string) => {
setEditorState(roleName, null);
};
const onEdit = (role: InheritedRole) => {
setEditorState('', role, false);
};
const resetState = () => {
setEditorState('', null, true);
};
const onDelete = (role: InheritedRole) => {
const confirmMessage = `This will delete the inherited role "${role?.role_name}"`;
const isOk = getConfirmation(confirmMessage);
if (isOk) {
dispatch(deleteInheritedRoleAction(role?.role_name));
}
};
const onSave = (role: InheritedRole) => {
if (inheritedRole) {
dispatch(updateInheritedRoleAction(role.role_name, role.role_set));
} else {
dispatch(addInheritedRoleAction(role.role_name, role.role_set));
}
resetState();
};
return (
<div
className={`${styles.clear_fix} ${styles.padd_left} ${styles.padd_top} ${styles.metadata_wrapper} container-fluid`}
>
{experimentalFeatures &&
experimentalFeatures.includes('inherited_roles') ? (
<>
<div className={styles.header}>
<Heading fontSize="24px">Inherited Roles</Heading>
<span className={styles.headerBadge}>
<Badge type="experimental" />
</span>
</div>
<div className={styles.add_mar_top}>
Inherited roles will combine the permissions of 2 or more roles.
</div>
<ActionContext.Provider
value={{ onEdit, onAdd, onDelete, onRoleNameChange }}
>
<InheritedRolesTable inheritedRoles={inheritedRoles} />
</ActionContext.Provider>
<InheritedRolesEditor
allRoles={allRoles}
cancelCb={resetState}
isCollapsed={isCollapsed}
onSave={onSave}
inheritedRoleName={inheritedRoleName}
inheritedRole={inheritedRole}
/>
</>
) : (
<div>
Inherited roles is currently an experimental feature. To enable
inherited roles, start the Hasura server with environment variable
<code>
HASURA_GRAPHQL_EXPERIMENTAL_FEATURES: &quot;inherited_roles&quot;
</code>
</div>
)}
</div>
);
};
const mapStateToProps = (state: ReduxState) => {
return {
inheritedRoles: getInheritedRoles(state),
allRoles: rolesSelector(state),
experimentalFeatures: state.main.serverConfig.data.experimental_features,
};
};
const mapDispatchToProps = (dispatch: Dispatch) => {
return {
dispatch,
};
};
const connector = connect(mapStateToProps, mapDispatchToProps);
type InjectedProps = ConnectedProps<typeof connector>;
type ComponentProps = InheritedRolesTableProps & EditorProps;
type Props = ComponentProps & InjectedProps;
const connectedInheritedRoles = connector(InheritedRoles);
export default connectedInheritedRoles;

View File

@ -0,0 +1,212 @@
import React, { useState, useEffect } from 'react';
import styles from '../../Settings/Settings.scss';
import Button from '../../../Common/Button';
import TextInput from '../../../Common/TextInput/TextInput';
import { InheritedRole } from '../../../../metadata/types';
type Mode = 'create' | 'edit';
export type EditorProps = {
allRoles: string[];
// Pass the Inherited Role object when editing an existing Role
inheritedRole?: InheritedRole | null;
// Pass the the Role name while creating a new Object.
inheritedRoleName?: string;
onSave: (inheritedRole: InheritedRole) => void;
isCollapsed: boolean;
cancelCb: () => void;
};
const InheritedRolesEditor: React.FC<EditorProps> = ({
allRoles,
onSave,
cancelCb,
...props
}) => {
const [inheritedRoleName, setInheritedRoleName] = useState(
props.inheritedRoleName
);
const [inheritedRole, setInheritedRole] = useState(props.inheritedRole);
const [isCollapsed, setIsCollapsed] = useState(props.isCollapsed);
type Option = {
value: typeof allRoles[number];
isChecked: true | false;
};
const [mode, setMode] = useState<Mode>(() =>
inheritedRole ? 'edit' : 'create'
);
const defaultOptions = allRoles.map(role => ({
value: role,
isChecked:
mode === 'create'
? false
: inheritedRole?.role_set.includes(role) || false,
}));
const [options, setOptions] = useState(defaultOptions);
useEffect(() => {
setInheritedRoleName(props.inheritedRoleName);
setInheritedRole(props.inheritedRole);
setIsCollapsed(props.isCollapsed);
const updatedMode = props.inheritedRole ? 'edit' : 'create';
setMode(updatedMode);
setOptions(
allRoles.map(role => ({
value: role,
isChecked:
updatedMode === 'create'
? false
: props.inheritedRole?.role_set.includes(role) || false,
}))
);
}, [
props.inheritedRoleName,
props.inheritedRole,
props.isCollapsed,
allRoles,
]);
const [filterText, setFilterText] = useState('');
const filterTextChange = (e: React.ChangeEvent<HTMLInputElement>) => {
e.persist();
setFilterText(e.target.value);
};
const selectAll = () => {
const allOptions = allRoles.map(role => ({ value: role, isChecked: true }));
setOptions(allOptions);
};
const clearAll = () => {
const allOptions = allRoles.map(role => ({
value: role,
isChecked: false,
}));
setOptions(allOptions);
};
const checkboxValueChange = (e: React.ChangeEvent<HTMLInputElement>) => {
e.persist();
setOptions(
options.map(option => {
if (option.value !== e.target.value) return option;
return { value: option.value, isChecked: !option.isChecked };
})
);
};
const saveRole = () => {
const response: InheritedRole = {
role_name: '',
role_set: [],
};
if (mode === 'create') {
response.role_name = inheritedRoleName || '';
} else {
response.role_name = inheritedRole?.role_name || '';
}
response.role_set = options
.filter((option: Option) => option.isChecked)
.map((option: Option) => option.value);
onSave(response);
};
return (
<>
{!isCollapsed && (
<div className={styles.RolesEditor}>
<div>
<div className={styles.editorHeader}>
<Button
color="white"
size="xs"
onClick={() => {
cancelCb();
}}
>
Cancel
</Button>
<div className={styles.roleNameContainer}>
{mode === 'create' ? (
<div>
<b>Create Role:</b> {inheritedRoleName}{' '}
</div>
) : (
<div>
<b>Edit Role:</b> {inheritedRole?.role_name}
</div>
)}
</div>
</div>
<hr />
<div className={styles.filterContainer}>
<TextInput
onChange={filterTextChange}
value={filterText}
placeholder="Filter Roles..."
bsclass="max-width-250"
/>
<div>
<Button color="white" size="xs" onClick={selectAll}>
Select all
</Button>{' '}
<Button color="white" size="xs" onClick={clearAll}>
Clear all
</Button>
</div>
</div>
<br />
<div>
{!options.length
? 'No singular/Non-inherited Roles available'
: options
.filter(
(option: Option) =>
option.value.includes(filterText) || !filterText.length
)
.map((option: Option, index) => (
<div key={index} className={styles.roleOption}>
<input
type="checkbox"
checked={option.isChecked}
onChange={checkboxValueChange}
value={option.value}
required
/>{' '}
{option.value}{' '}
</div>
))}
</div>
<hr />
<div>
<Button
color="yellow"
onClick={saveRole}
disabled={
!options.filter((option: Option) => option.isChecked).length
}
>
Save Role
</Button>
</div>
</div>
</div>
)}
</>
);
};
export default InheritedRolesEditor;

View File

@ -0,0 +1,25 @@
.fix_column_width {
width: 30%;
}
.margin_right {
margin-right: 5px;
}
.create_button_styles {
margin-left: 5px;
margin-bottom: 4px;
}
.tab_background {
background-color: #f2f2f2;
}
.margin_top {
margin-top: 20px;
}
.input_box_styles {
width: 200px;
display: inline;
}

View File

@ -0,0 +1,26 @@
import React from 'react';
import InheritedRolesTableHeader from './TableHeader';
import InheritedRolesTableBody from './TableBody';
import { InheritedRole } from '../../../../metadata/types';
import styles from './InheritedRolesStyles.scss';
export type InheritedRolesTableProps = {
inheritedRoles: InheritedRole[];
};
const InheritedRolesTable: React.FC<InheritedRolesTableProps> = ({
inheritedRoles,
}) => {
const headings = ['Inherited Role', 'Role Set', 'Actions'];
return (
<div className={styles.margin_top}>
<table className="table table-bordered">
<InheritedRolesTableHeader headings={headings} />
<InheritedRolesTableBody inheritedRoles={inheritedRoles} />
</table>
</div>
);
};
export default InheritedRolesTable;

View File

@ -0,0 +1,22 @@
import React from 'react';
import { InheritedRole } from '../../../../metadata/types';
import TableRow from './TableRow';
type TableBodyProps = {
inheritedRoles: InheritedRole[];
};
const TableBody: React.FC<TableBodyProps> = props => {
const { inheritedRoles } = props;
return (
<tbody>
{inheritedRoles.map((inheritedRole, i) => (
<TableRow key={i} inheritedRole={inheritedRole} />
))}
<TableRow key={inheritedRoles.length} />
</tbody>
);
};
export default TableBody;

View File

@ -0,0 +1,27 @@
import React from 'react';
import styles from './InheritedRolesStyles.scss';
type TableHeaderProps = {
headings: string[];
};
const TableHeader: React.FC<TableHeaderProps> = props => {
const { headings } = props;
return (
<thead>
<tr>
{headings.map((heading, index) =>
heading === 'Inherited Role' ? (
<th key={index} className={styles.fix_column_width}>
{heading}
</th>
) : (
<th key={index}>{heading}</th>
)
)}
</tr>
</thead>
);
};
export default TableHeader;

View File

@ -0,0 +1,69 @@
import React, { ChangeEvent, useContext, useState } from 'react';
import Button from '../../../Common/Button';
import { ActionContext } from './InheritedRoles';
import { InheritedRole } from '../../../../metadata/types';
import styles from './InheritedRolesStyles.scss';
type TableRowProps = {
inheritedRole?: InheritedRole;
};
const TableRow: React.FC<TableRowProps> = ({ inheritedRole }) => {
const [roleName, setRoleName] = useState<string>('');
const rowCells = [];
const context = useContext(ActionContext);
const onRoleNameChange = (e: ChangeEvent<HTMLInputElement>) => {
setRoleName(e.target.value?.trim());
context?.onRoleNameChange(e.target.value?.trim());
};
if (inheritedRole) {
rowCells.push(<th key="role-textbox">{inheritedRole.role_name}</th>);
rowCells.push(<td key="role-set">{inheritedRole.role_set.join(', ')}</td>);
rowCells.push(
<td key="actions">
<Button
size="sm"
color="white"
className={styles.margin_right}
onClick={() => context?.onEdit(inheritedRole)}
>
Edit
</Button>
<Button
size="sm"
color="red"
onClick={() => context?.onDelete(inheritedRole)}
>
Remove
</Button>
</td>
);
} else {
rowCells.push(
<th key="role-textbox" colSpan={3}>
<input
id="new-role-input"
className={`form-control ${styles.input_box_styles}`}
onChange={onRoleNameChange}
type="text"
placeholder="Enter new role"
value={roleName}
/>
<Button
color="yellow"
className={styles.create_button_styles}
disabled={roleName.length === 0}
onClick={() => context?.onAdd(roleName)}
>
Create
</Button>
</th>
);
}
return <tr>{rowCells}</tr>;
};
export default TableRow;

View File

@ -0,0 +1,8 @@
import { InheritedRole } from '../../../../metadata/types';
export interface RoleActionsInterface {
onEdit: (inhertitedRole: InheritedRole) => void;
onDelete: (inhertitedRole: InheritedRole) => void;
onAdd: (inheritedRoleName: string) => void;
onRoleNameChange: (InheritedRoleName: string) => void;
}

View File

@ -67,4 +67,46 @@
}
}
}
}
.RolesEditor {
padding: 15px;
background-color: white;
border: 1px solid #ccc;
max-width: inherit;
}
.filterContainer {
display: grid;
row-gap: 5px;
}
.roleOption {
padding: 2px;
}
.inheritedRoleNameContainer {
padding: 10px;
}
[type="checkbox"] {
vertical-align:top;
}
.header {
background: none;
display: flex;
align-items: center;
}
.headerBadge {
padding-left: 10px;
}
.editorHeader {
display: flex;
}
.roleNameContainer{
padding-left: 15px;
}

View File

@ -19,7 +19,13 @@ type SidebarProps = {
metadata: Metadata;
};
type SectionDataKey = 'actions' | 'status' | 'allow-list' | 'logout' | 'about';
type SectionDataKey =
| 'actions'
| 'status'
| 'allow-list'
| 'logout'
| 'about'
| 'inherited-roles';
interface SectionData {
key: SectionDataKey;
@ -83,6 +89,13 @@ const Sidebar: React.FC<SidebarProps> = ({ location, metadata }) => {
title: 'About',
});
sectionsData.push({
key: 'inherited-roles',
link: '/settings/inherited-roles',
dataTestVal: 'inherited-roles-link',
title: 'Inherited Roles',
});
const currentLocation = location.pathname;
const sections: JSX.Element[] = [];

View File

@ -11,6 +11,7 @@ export type AllowedBadges =
| 'feature'
| 'security'
| 'error'
| 'experimental'
| 'rest-GET'
| 'rest-PUT'
| 'rest-POST'
@ -79,6 +80,12 @@ export const Badge: React.FC<ExtendedBadgeProps> = ({
security
</StyledBadge>
);
case 'experimental':
return (
<StyledBadge {...props} bg="#DBEAFE" color="#1E40AF">
experimental
</StyledBadge>
);
case 'rest-GET':
return (
<StyledBadge {...restApiProps} bg="#e6f7ff" color="#006699">

View File

@ -10,6 +10,9 @@ import {
deleteAllowedQueryQuery,
createAllowListQuery,
addAllowedQueriesQuery,
addInheritedRole,
deleteInheritedRole,
updateInheritedRole,
getReloadCacheAndGetInconsistentObjectsQuery,
reloadRemoteSchemaCacheAndGetInconsistentObjectsQuery,
updateAllowedQueryQuery,
@ -142,6 +145,26 @@ export interface ReloadDataSourceError {
data: string;
}
export interface AddInheritedRole {
type: 'Metadata/ADD_INHERITED_ROLE';
data: {
role_name: string;
role_set: string[];
};
}
export interface DeleteInheritedRole {
type: 'Metadata/DELETE_INHERITED_ROLE';
data: string;
}
export interface UpdateInheritedRole {
type: 'Metadata/UPDATE_INHERITED_ROLE';
data: {
role_name: string;
role_set: string[];
};
}
export interface AddRestEndpoint {
type: 'Metadata/ADD_REST_ENDPOINT';
data: RestEndpointEntry[];
@ -173,6 +196,9 @@ export type MetadataActions =
| RemoveDataSourceError
| ReloadDataSourceRequest
| ReloadDataSourceError
| AddInheritedRole
| DeleteInheritedRole
| UpdateInheritedRole
| AddRestEndpoint
| DropRestEndpoint
| { type: typeof UPDATE_CURRENT_DATA_SOURCE; source: string };
@ -819,6 +845,120 @@ export const addAllowedQueries = (
};
};
export const addInheritedRoleAction = (
role_name: string,
role_set: string[],
callback?: any
): Thunk<void, MetadataActions> => {
return (dispatch, getState) => {
const upQuery = addInheritedRole(role_name, role_set);
const migrationName = `add_inherited_role`;
const requestMsg = 'Adding inherited role...';
const successMsg = 'Added inherited role';
const errorMsg = 'Adding inherited role failed';
const onSuccess = () => {
dispatch({
type: 'Metadata/ADD_INHERITED_ROLE',
data: { role_name, role_set },
});
callback();
};
const onError = () => {};
makeMigrationCall(
dispatch,
getState,
[upQuery],
undefined,
migrationName,
onSuccess,
onError,
requestMsg,
successMsg,
errorMsg
);
};
};
export const deleteInheritedRoleAction = (
role_name: string,
callback?: () => void
): Thunk<void, MetadataActions> => {
return (dispatch, getState) => {
const upQuery = deleteInheritedRole(role_name);
const migrationName = `delete_inherited_role`;
const requestMsg = 'Deleting inherited role...';
const successMsg = 'Deleted inherited role';
const errorMsg = 'Deleting inherited role failed';
const onSuccess = () => {
dispatch({ type: 'Metadata/DELETE_INHERITED_ROLE', data: role_name });
if (callback) {
callback();
}
};
const onError = () => {};
makeMigrationCall(
dispatch,
getState,
[upQuery],
undefined,
migrationName,
onSuccess,
onError,
requestMsg,
successMsg,
errorMsg
);
};
};
export const updateInheritedRoleAction = (
role_name: string,
role_set: string[],
callback?: () => void
): Thunk<void, MetadataActions> => {
return (dispatch, getState) => {
const upQuery = updateInheritedRole(role_name, role_set);
const migrationName = `update_inherited_role`;
const requestMsg = 'Updating inherited role...';
const successMsg = 'Updated inherited role';
const errorMsg = 'Updating inherited role failed';
const onSuccess = () => {
dispatch({
type: 'Metadata/UPDATE_INHERITED_ROLE',
data: { role_name, role_set },
});
if (callback) {
callback();
}
};
const onError = () => {};
makeMigrationCall(
dispatch,
getState,
[upQuery],
undefined,
migrationName,
onSuccess,
onError,
requestMsg,
successMsg,
errorMsg
);
};
};
export const addRESTEndpoint = (
queryObj: RestEndpointEntry,
request: string,

View File

@ -1,5 +1,10 @@
import { MetadataActions } from './actions';
import { QueryCollection, HasuraMetadataV3, RestEndpointEntry } from './types';
import {
QueryCollection,
HasuraMetadataV3,
RestEndpointEntry,
InheritedRole,
} from './types';
import { allowedQueriesCollection } from './utils';
type MetadataState = {
@ -9,6 +14,7 @@ type MetadataState = {
inconsistentObjects: any[];
ongoingRequest: boolean; // deprecate
allowedQueries: QueryCollection[];
inheritedRoles: InheritedRole[];
rest_endpoints?: RestEndpointEntry[];
};
@ -19,6 +25,7 @@ const defaultState: MetadataState = {
inconsistentObjects: [],
ongoingRequest: false,
allowedQueries: [],
inheritedRoles: [],
};
export const metadataReducer = (
@ -34,6 +41,7 @@ export const metadataReducer = (
action.data?.query_collections?.find(
query => query.name === allowedQueriesCollection
)?.definition.queries || [],
inheritedRoles: action.data?.inherited_roles,
loading: false,
error: null,
};
@ -116,6 +124,28 @@ export const metadataReducer = (
),
],
};
case 'Metadata/ADD_INHERITED_ROLE':
return {
...state,
inheritedRoles: [...state.inheritedRoles, action.data],
};
case 'Metadata/DELETE_INHERITED_ROLE':
return {
...state,
inheritedRoles: [
...state.inheritedRoles.filter(ir => ir.role_name !== action.data),
],
};
case 'Metadata/UPDATE_INHERITED_ROLE':
return {
...state,
inheritedRoles: [
...state.inheritedRoles.map(ir =>
ir.role_name === action.data.role_name ? action.data : ir
),
],
};
case 'Metadata/ADD_REST_ENDPOINT':
return {
...state,

View File

@ -343,6 +343,9 @@ export const getCronTriggers = createSelector(getMetadata, metadata => {
export const getAllowedQueries = (state: ReduxState) =>
state.metadata.allowedQueries || [];
export const getInheritedRoles = (state: ReduxState) =>
state.metadata.inheritedRoles || [];
export const getDataSources = createSelector(getMetadata, metadata => {
const sources: DataSource[] = [];
metadata?.sources.forEach(source => {

View File

@ -911,6 +911,11 @@ export interface MetadataDataSource {
allowlist?: AllowList[];
}
export interface InheritedRole {
role_name: string;
role_set: string[];
}
export interface HasuraMetadataV3 {
version: 3;
sources: MetadataDataSource[];
@ -919,5 +924,6 @@ export interface HasuraMetadataV3 {
custom_types?: CustomTypes;
cron_triggers?: CronTrigger[];
query_collections: QueryCollectionEntry[];
inherited_roles: InheritedRole[];
rest_endpoints?: RestEndpointEntry[];
}

View File

@ -107,6 +107,26 @@ export const getReloadCacheAndGetInconsistentObjectsQuery = (
],
});
export const addInheritedRole = (roleName: string, roleSet: string[]) => ({
type: 'add_inherited_role',
args: {
role_name: roleName,
role_set: roleSet,
},
});
export const deleteInheritedRole = (roleName: string) => ({
type: 'drop_inherited_role',
args: {
role_name: roleName,
},
});
export const updateInheritedRole = (roleName: string, roleSet: string[]) => ({
type: 'bulk',
args: [deleteInheritedRole(roleName), addInheritedRole(roleName, roleSet)],
});
export const isMetadataEmpty = (metadataObject: HasuraMetadataV3) => {
const { actions, sources, remote_schemas } = metadataObject;
const hasRemoteSchema = remote_schemas && remote_schemas.length;

View File

@ -34,6 +34,7 @@ import ApiContainer from './components/Services/ApiExplorer/Container';
import metadataOptionsConnector from './components/Services/Settings/MetadataOptions/MetadataOptions';
import metadataStatusConnector from './components/Services/Settings/MetadataStatus/MetadataStatus';
import allowedQueriesConnector from './components/Services/Settings/AllowedQueries/AllowedQueries';
import inheritedRolesConnector from './components/Services/Settings/InheritedRoles/InheritedRoles';
import logoutConnector from './components/Services/Settings/Logout/Logout';
import aboutConnector from './components/Services/Settings/About/About';
@ -157,6 +158,7 @@ const routes = store => {
/>
<Route path="logout" component={logoutConnector(connect)} />
<Route path="about" component={aboutConnector(connect)} />
<Route path="inherited-roles" component={inheritedRolesConnector} />
</Route>
{dataRouter}
{remoteSchemaRouter}

View File

@ -139,11 +139,16 @@ form {
0 0 0px rgba(102, 175, 233, 0.6);
}
.form-control {
// border-radius: 0;
height: auto;
}
.max-width-250 {
max-width: 250px;
}
select.form-control {
border-radius: 0;
-webkit-border-radius: 0px;