mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-09-20 06:58:39 +03:00
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:
parent
d16bc4fe46
commit
89e26d3e9f
@ -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
|
||||
|
||||
|
@ -217,4 +217,4 @@
|
||||
"engines": {
|
||||
"node": ">=8.9.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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: '',
|
||||
|
@ -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: "inherited_roles"
|
||||
</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;
|
@ -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;
|
@ -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;
|
||||
}
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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[] = [];
|
||||
|
@ -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">
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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 => {
|
||||
|
@ -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[];
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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}
|
||||
|
5
console/src/theme/bootstrap.overrides.scss
vendored
5
console/src/theme/bootstrap.overrides.scss
vendored
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user