console: remote schema permissions

GITHUB_PR_NUMBER: 6156
GITHUB_PR_URL: https://github.com/hasura/graphql-engine/pull/6156

Co-authored-by: Abhijeet Singh Khangarot <26903230+abhi40308@users.noreply.github.com>
Co-authored-by: Sooraj <8408875+soorajshankar@users.noreply.github.com>
GitOrigin-RevId: 3ddd61fc24bd1416e66a84579372b7a372dd4293
This commit is contained in:
hasura-bot 2021-01-28 21:28:36 +05:30
parent ff3c58f230
commit 66a3d8dab5
35 changed files with 2903 additions and 35 deletions

View File

@ -33,3 +33,6 @@ export const makeDataAPIOptions = (
body,
failOnStatusCode: false,
});
export const getRemoteSchemaRoleName = (i: number, roleName: string) =>
`test-role-${roleName}-${i}`;

View File

@ -5,6 +5,7 @@ import {
getInvalidRemoteSchemaUrl,
getRemoteGraphQLURL,
getRemoteGraphQLURLFromEnv,
getRemoteSchemaRoleName,
} from '../../../helpers/remoteSchemaHelpers';
import { validateRS, ResultType } from '../../validators/validators';
@ -264,3 +265,45 @@ export const deleteRemoteSchema = () => {
cy.get(getElementFromAlias('delete-confirmation-error')).should('not.exist');
};
export const visitRemoteSchemaPermissionsTab = () => {
cy.visit(
`${baseUrl}/remote-schemas/manage/${getRemoteSchemaName(
1,
testName
)}/permissions`
);
cy.wait(5000);
};
export const createSimpleRemoteSchemaPermission = () => {
cy.get(getElementFromAlias('role-textbox'))
.clear()
.type(getRemoteSchemaRoleName(1, testName));
cy.get(getElementFromAlias(`${getRemoteSchemaRoleName(1, testName)}-Permission`))
.click();
cy.wait(2000);
cy.get(getElementFromAlias('field-__query_root'))
.click();
cy.get(getElementFromAlias('checkbox-test')).click()
cy.get(getElementFromAlias('pen-limit')).click()
cy.get(getElementFromAlias('input-limit')).type('1')
cy.get(getElementFromAlias('save-remote-schema-permissions'))
.click({ force: true });
cy.wait(15000);
cy.url().should(
'eq',
`${baseUrl}/remote-schemas/manage/${getRemoteSchemaName(
1,
testName
)}/permissions`
);
cy.wait(5000);
};
// export const deleteRemoteSchemaPermission = () => {
// cy.get(getElementFromAlias('delete-remote-schema-permissions'))
// .click();
// }

View File

@ -16,6 +16,8 @@ import {
passWithRemoteSchemaHeader,
passWithEditRemoteSchema,
deleteRemoteSchema,
visitRemoteSchemaPermissionsTab,
createSimpleRemoteSchemaPermission,
} from './spec';
const setup = () => {
@ -33,11 +35,11 @@ const setup = () => {
export const runCreateRemoteSchemaTableTests = () => {
describe('Create Remote Schema', () => {
it(
'Create table button opens the correct route',
'Add remote schema button opens the correct route',
checkCreateRemoteSchemaRoute
);
it(
'Fails to create remote schema without name',
'Fails to create remote schema without valid url',
failRSWithInvalidRemoteUrl
);
it('Create a simple remote schema', createSimpleRemoteSchema);
@ -50,8 +52,19 @@ export const runCreateRemoteSchemaTableTests = () => {
'Delete simple remote schema fail due to user confirmation error',
deleteSimpleRemoteSchemaFailUserConfirmationError
);
it(
'Visits the remote schema permissions tab',
visitRemoteSchemaPermissionsTab
);
it(
'Create a simple remote schema permission role',
createSimpleRemoteSchemaPermission
);
it('Delete simple remote schema', deleteSimpleRemoteSchema);
it('Fails to create remote schema with url from env', failWithRemoteSchemaEnvUrl);
it(
'Fails to create remote schema with url from env',
failWithRemoteSchemaEnvUrl
);
it(
'Fails to create remote schema with headers from env',
failWithRemoteSchemaEnvHeader

View File

@ -3274,6 +3274,14 @@
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.162.tgz",
"integrity": "sha512-alvcho1kRUnnD1Gcl4J+hK0eencvzq9rmzvFPRmP5rPHx9VVsJj6bKLTATPVf9ktgv4ujzh7T+XWKp+jhuODig=="
},
"@types/lodash.merge": {
"version": "4.6.6",
"resolved": "https://registry.npmjs.org/@types/lodash.merge/-/lodash.merge-4.6.6.tgz",
"integrity": "sha512-IB90krzMf7YpfgP3u/EvZEdXVvm4e3gJbUvh5ieuI+o+XqiNEt6fCzqNRaiLlPVScLI59RxIGZMQ3+Ko/DJ8vQ==",
"requires": {
"@types/lodash": "*"
}
},
"@types/memory-fs": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@types/memory-fs/-/memory-fs-0.3.2.tgz",
@ -12244,6 +12252,11 @@
"integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=",
"dev": true
},
"lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
},
"lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",

View File

@ -52,6 +52,7 @@
"@types/highlight.js": "9.12.4",
"@types/reselect": "^2.2.0",
"@types/lodash": "^4.14.159",
"@types/lodash.merge": "^4.6.6",
"@types/sql-formatter": "2.3.0",
"ace-builds": "^1.4.11",
"apollo-link": "1.2.14",
@ -69,6 +70,7 @@
"jsonwebtoken": "8.5.1",
"jwt-decode": "2.2.0",
"less": "3.11.1",
"lodash.merge": "4.6.2",
"moment": "^2.26.0",
"piping": "0.3.2",
"prop-types": "15.7.2",

View File

@ -1,9 +1,10 @@
@import "../../Services/Data/TableModify/ModifyTable.scss";
@import '../../Services/Data/TableModify/ModifyTable.scss';
.permissionsTable {
width: 85%;
width: 85%;
td, th {
td,
th {
text-align: center;
vertical-align: middle !important;
position: relative;
@ -13,27 +14,28 @@ width: 85%;
font-weight: bold;
}
td:first-child, th:first-child {
td:first-child,
th:first-child {
overflow: auto;
border-right: 4px double #ddd;
max-width: 250px;
width: 250px;
}
//.permissionDelete {
// cursor: pointer;
// margin-left: 10px;
//}
//.permissionDelete {
// cursor: pointer;
// margin-left: 10px;
//}
.bulkSelect {
margin-right: 10px !important;
}
// TODO: make common with Roles page
// TODO: make common with Roles page
.clickableCell {
cursor: pointer;
.editPermsIcon {
.editPermsIcon {
font-size: 12px;
position: absolute;
right: 10px;
@ -46,16 +48,17 @@ width: 85%;
.clickableCell:hover {
background-color: #ebf7de;
.editPermsIcon {
.editPermsIcon {
display: inline-block;
}
}
.currEdit, .currEdit:hover {
background-color: #FFF3D5;
color: #FD9540;
.currEdit,
.currEdit:hover {
background-color: #fff3d5;
color: #fd9540;
.editPermsIcon {
.editPermsIcon {
display: inline-block;
}
}
@ -117,34 +120,34 @@ width: 85%;
margin-top: 10px;
margin-bottom: 10px;
label {
label {
cursor: pointer;
min-height: 20px;
font-weight: normal;
input:not([disabled]) {
input:not([disabled]) {
cursor: pointer;
}
}
}
.insertSetConfigRow {
.insertSetConfigRow {
margin: 10px 0px;
display: flex;
align-items: center;
.input_element_wrapper {
.input_element_wrapper {
width: 20%;
i {
i {
cursor: pointer;
}
select {
select {
width: 100%;
}
input {
input {
width: 100%;
}
}
@ -152,10 +155,64 @@ width: 85%;
}
}
}
.tree {
a {
color: #2f88e7;
text-decoration: none;
}
button {
border: 0;
cursor: pointer;
background-color: transparent;
transition: 0.1s;
&:hover {
text-shadow: 0px 0px 2px #d9d9d9;
}
&:focus {
outline: auto;
}
}
ul {
list-style: none;
}
label {
transition: 0.1s;
&:hover {
text-shadow: 0px 0px 2px #d9d9d9;
}
}
}
.sessionVarButton {
margin-left: 10px;
color: #080;
font-size: 10px;
font-weight: bold;
cursor: pointer;
}
.treeNodes {
ul {
border-left: 2px dotted #ccc;
}
}
.fw_medium {
font-weight: 400;
}
.fw_large {
font-weight: 600;
}
.argSelect {
cursor: pointer;
}
}
.permissionSymbolNA {
color: firebrick;
color: firebrick;
}
.permissionSymbolFA {

View File

@ -126,6 +126,21 @@ export function isJsonString(str: string) {
}
return true;
}
export const isNumberString = (str: string | number) =>
!Number.isNaN(Number(str));
export const isArrayString = (str: string) => {
try {
if (isJsonString(str) && Array.isArray(JSON.parse(str))) {
return true;
}
} catch (e) {
return false;
}
return false;
};
/* ARRAY utils */
export const deleteArrayElementAtIndex = (array: unknown[], index: number) => {
return array.splice(index, 1);

View File

@ -8,6 +8,24 @@ import { CLI_CONSOLE_MODE, SERVER_CONSOLE_MODE } from '../../../constants';
import { loadMigrationStatus } from '../../Main/Actions';
import { handleMigrationErrors } from '../../../utils/migration';
import { showSuccessNotification } from '../Common/Notification';
import { makeMigrationCall } from '../Data/DataActions';
import { getConfirmation } from '../../Common/utils/jsUtils';
import {
makeRequest as makePermRequest,
setRequestSuccess as setPermRequestSuccess,
setRequestFailure as setPermRequestFailure,
permSetRoleName,
permCloseEdit,
permResetBulkSelect,
} from './Permissions/reducer';
import {
getRemoteSchemaPermissionQueries,
getCreateRemoteSchemaPermissionQuery,
getDropRemoteSchemaPermissionQuery,
} from './Permissions/utils';
import Migration from '../../../utils/migration/Migration';
import { exportMetadata } from '../../../metadata/actions';
import { getRemoteSchemas } from '../../../metadata/selector';
/* Action constants */
@ -118,9 +136,189 @@ const makeRequest = (
};
};
const saveRemoteSchemaPermission = (successCb, errorCb) => {
return (dispatch, getState) => {
const allRemoteSchemas = getRemoteSchemas(getState());
const {
listData: { viewRemoteSchema: currentRemoteSchemaName },
permissions: { permissionEdit, schemaDefinition },
} = getState().remoteSchemas;
const currentRemoteSchema = allRemoteSchemas.find(
rs => rs.name === currentRemoteSchemaName
);
const allPermissions = currentRemoteSchema?.permissions || [];
const { upQueries, downQueries } = getRemoteSchemaPermissionQueries(
permissionEdit,
allPermissions,
currentRemoteSchemaName,
schemaDefinition
);
const migrationName = `save_remote_schema_permission`;
const requestMsg = 'Saving permission...';
const successMsg = 'Permission saved successfully';
const errorMsg = 'Saving permission failed';
const customOnSuccess = () => {
dispatch(exportMetadata());
dispatch(setPermRequestSuccess());
if (successCb) {
successCb();
}
};
const customOnError = () => {
dispatch(setPermRequestFailure());
if (errorCb) {
errorCb();
}
};
dispatch(makePermRequest());
makeMigrationCall(
dispatch,
getState,
upQueries,
downQueries,
migrationName,
customOnSuccess,
customOnError,
requestMsg,
successMsg,
errorMsg
);
};
};
const removeRemoteSchemaPermission = (successCb, errorCb) => {
return (dispatch, getState) => {
const isOk = getConfirmation(
'This will remove the permission for this role'
);
if (!isOk) return;
const {
listData: { viewRemoteSchema: currentRemoteSchema },
permissions: { permissionEdit, schemaDefinition },
} = getState().remoteSchemas;
const { role } = permissionEdit;
const upQuery = getDropRemoteSchemaPermissionQuery(
role,
currentRemoteSchema
);
const downQuery = getCreateRemoteSchemaPermissionQuery(
{ role },
currentRemoteSchema,
schemaDefinition
);
const migrationName = 'remove_remoteSchema_perm';
const requestMsg = 'Removing permission...';
const successMsg = 'Permission removed successfully';
const errorMsg = 'Removing permission failed';
const customOnSuccess = () => {
dispatch(exportMetadata());
dispatch(setPermRequestSuccess());
if (successCb) {
successCb();
}
};
const customOnError = () => {
dispatch(setPermRequestFailure());
if (errorCb) {
errorCb();
}
};
dispatch(makePermRequest());
makeMigrationCall(
dispatch,
getState,
[upQuery],
[downQuery],
migrationName,
customOnSuccess,
customOnError,
requestMsg,
successMsg,
errorMsg
);
};
};
const permRemoveMultipleRoles = () => {
return (dispatch, getState) => {
const allRemoteSchemas = getRemoteSchemas(getState());
const {
listData: { viewRemoteSchema: currentRemoteSchemaName },
permissions: { bulkSelect },
} = getState().remoteSchemas;
const currentRemoteSchema = allRemoteSchemas.find(
rs => rs.name === currentRemoteSchemaName
);
const currentPermissions = currentRemoteSchema.permissions;
const roles = bulkSelect;
const migration = new Migration();
roles.map(role => {
const currentRolePermission = currentPermissions.filter(el => {
return el.role === role;
});
const upQuery = getDropRemoteSchemaPermissionQuery(
role,
currentRemoteSchemaName
);
const downQuery = getCreateRemoteSchemaPermissionQuery(
{ role },
currentRemoteSchemaName,
currentRolePermission[0].definition.schema
);
migration.add(upQuery, downQuery);
});
// Apply migration
const migrationName = 'bulk_remove_remoteSchema_perm';
const requestMsg = 'Removing permissions...';
const successMsg = 'Permission removed successfully';
const errorMsg = 'Removing permission failed';
const customOnSuccess = () => {
dispatch(permSetRoleName(''));
dispatch(permCloseEdit());
dispatch(permResetBulkSelect());
};
const customOnError = () => {};
makeMigrationCall(
dispatch,
getState,
migration.upMigration,
migration.downMigration,
migrationName,
customOnSuccess,
customOnError,
requestMsg,
successMsg,
errorMsg
);
};
};
export {
VIEW_REMOTE_SCHEMA,
makeRequest,
saveRemoteSchemaPermission,
removeRemoteSchemaPermission,
permRemoveMultipleRoles,
FILTER_REMOTE_SCHEMAS,
SET_REMOTE_SCHEMAS,
};

View File

@ -72,9 +72,9 @@ const getReqHeader = headers => {
};
if (h.type === 'static') {
reqHead.value = h.value;
reqHead.value = h.value?.trim();
} else {
reqHead.value_from_env = h.value;
reqHead.value_from_env = h.value?.trim();
}
requestHeaders.push(reqHead);
@ -123,8 +123,8 @@ const addRemoteSchema = () => {
const resolveObj = {
name: currState.name.trim().replace(/ +/g, ''),
definition: {
url: currState.manualUrl,
url_from_env: currState.envName,
url: currState.manualUrl?.trim(),
url_from_env: currState.envName?.trim(),
headers: [],
timeout_seconds: timeoutSeconds,
forward_client_headers: currState.forwardClientHeaders,

View File

@ -5,6 +5,9 @@ const tabInfo = {
modify: {
display_text: 'Modify',
},
permissions: {
display_text: 'Permissions',
},
};
export default tabInfo;

View File

@ -0,0 +1,122 @@
import React, { useRef, useEffect, useState, ReactText } from 'react';
import merge from 'lodash.merge';
import { GraphQLInputField } from 'graphql';
import { getChildArguments } from './utils';
import RSPInput from './RSPInput';
import { ArgTreeType } from './types';
import styles from '../../../Common/Permissions/PermissionStyles.scss';
interface ArgSelectProps {
valueField: GraphQLInputField;
keyName: string;
value?: ArgTreeType | ReactText;
level: number;
setArg: (e: Record<string, unknown>) => void;
}
export const ArgSelect: React.FC<ArgSelectProps> = ({
keyName: k,
valueField: v,
value,
level,
setArg = e => console.log(e),
}) => {
const [expanded, setExpanded] = useState(false);
const autoExpanded = useRef(false);
const [editMode, setEditMode] = useState<boolean>(
Boolean(
value &&
((typeof value === 'string' && value.length > 0) ||
typeof value === 'number')
)
);
const prevState = useRef<Record<string, any>>();
useEffect(() => {
if (value && typeof value === 'string' && value.length > 0 && !editMode) {
// show value instead of pen icon, if the value is defined in the prop
setEditMode(true);
}
}, [value, editMode]);
useEffect(() => {
// auto expand args when there is prefilled values
// happens only first time when the node is created
if (value && k && !expanded && !autoExpanded.current) {
setExpanded(true);
autoExpanded.current = true;
}
}, [value, k, expanded]);
const { children } = getChildArguments(v as GraphQLInputField);
const setArgVal = (val: Record<string, any>) => {
const prevVal = prevState.current;
if (prevVal) {
const newState = merge(prevVal, val);
setArg(newState);
prevState.current = newState;
} else {
setArg(val);
prevState.current = val;
}
};
const toggleExpandMode = () => setExpanded(b => !b);
if (children) {
return (
<>
<button onClick={toggleExpandMode} style={{ marginLeft: '-1em' }}>
{expanded ? '-' : '+'}
</button>
{!expanded && (
<label
className={`${styles.argSelect} ${styles.fw_medium}`}
htmlFor={k}
>
{k}:
</label>
)}
{expanded && (
<label
className={`${styles.argSelect} ${styles.fw_large}`}
htmlFor={k}
>
{k}:
</label>
)}
<ul>
{expanded &&
Object.values(children).map(i => {
if (typeof value === 'string') return undefined;
const childVal =
value && typeof value === 'object' ? value[i?.name] : undefined;
return (
<li key={i.name}>
<ArgSelect
keyName={i.name}
setArg={val => setArgVal({ [k]: val })}
valueField={i}
value={childVal}
level={level + 1}
/>
</li>
);
})}
</ul>
</>
);
}
return (
<li>
<RSPInput
v={v as GraphQLInputField}
k={k}
editMode={editMode}
setArgVal={setArgVal}
value={value}
setEditMode={setEditMode}
/>
</li>
);
};

View File

@ -0,0 +1,50 @@
import React from 'react';
import { getConfirmation } from '../../../Common/utils/jsUtils';
import Button from '../../../Common/Button/Button';
import styles from '../../../Common/Permissions/PermissionStyles.scss';
export type BulkSelectProps = {
bulkSelect: string[];
permRemoveMultipleRoles: () => void;
};
const BulkSelect: React.FC<BulkSelectProps> = ({
bulkSelect,
permRemoveMultipleRoles,
}) => {
const getSelectedRoles = () => {
return bulkSelect.map((role: string) => {
return (
<span key={role} className={styles.add_pad_right}>
<b>{role}</b>{' '}
</span>
);
});
};
const handleBulkRemoveClick = () => {
const confirmMessage =
'This will remove all currently set permissions for the selected role(s)';
const isOk = getConfirmation(confirmMessage);
if (isOk) {
permRemoveMultipleRoles();
}
};
return (
<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>
{getSelectedRoles()}
</div>
<div className={`${styles.add_mar_top} ${styles.add_mar_bottom_mid}`}>
<Button onClick={handleBulkRemoveClick} color="red" size="sm">
Remove All Permissions
</Button>
</div>
</div>
);
};
export default BulkSelect;

View File

@ -0,0 +1,43 @@
import React from 'react';
import { FieldType } from './types';
import styles from '../../../Common/Permissions/PermissionStyles.scss';
interface CollapsedFieldProps {
field: FieldType;
onClick: (e: React.MouseEvent<HTMLAnchorElement>) => void;
onExpand: (e: React.MouseEvent<HTMLButtonElement>) => void;
expanded: boolean;
}
export const CollapsedField: React.FC<CollapsedFieldProps> = ({
field: i,
onClick,
onExpand = () => {},
expanded,
}) => (
<>
<button data-test={`field-${i.typeName}`} onClick={onExpand} id={i.name}>
{expanded && (
<span className={`${styles.padd_small_left} ${styles.fw_large}`}>
{i.name}
</span>
)}
{!expanded && (
<span className={`${styles.padd_small_left} ${styles.fw_medium}`}>
{i.name}
</span>
)}
</button>
{i.return && (
<>
:
<a
onClick={onClick}
id={`${i.return.replace(/[^\w\s]/gi, '')}`}
href={`${i.return.replace(/[^\w\s]/gi, '')}`}
>
{i.return}
</a>
</>
)}
</>
);

View File

@ -0,0 +1,117 @@
import React, {
useCallback,
useContext,
useEffect,
useState,
MouseEvent,
} from 'react';
import { FieldType } from './types';
import { PermissionEditorContext } from './context';
import { CollapsedField } from './CollapsedField';
import { ArgSelect } from './ArgSelect';
import { isEmpty } from '../../../Common/utils/jsUtils';
import styles from '../../../Common/Permissions/PermissionStyles.scss';
import { generateTypeString } from './utils';
export interface FieldProps {
i: FieldType;
setItem: (e: FieldType) => void;
onExpand?: () => void;
expanded: boolean;
}
export const Field: React.FC<FieldProps> = ({
i,
setItem = e => console.log(e),
onExpand = console.log,
expanded,
}) => {
const context: any = useContext(PermissionEditorContext);
const initState =
context.argTree && context.argTree[i.name]
? { ...context.argTree[i.name] }
: {};
const [fieldVal, setfieldVal] = useState<Record<string, any>>(initState);
const setArg = useCallback(
(vStr: Record<string, unknown>) => {
setfieldVal(oldVal => {
const newState = {
...oldVal,
...vStr,
};
return newState;
});
},
[setItem, i]
);
useEffect(() => {
if (
fieldVal &&
fieldVal !== {} &&
Object.keys(fieldVal).length > 0 &&
!isEmpty(fieldVal)
) {
context.setArgTree((argTree: Record<string, any>) => {
return { ...argTree, [i.name]: fieldVal };
});
}
}, [fieldVal]);
const handleClick = (e: MouseEvent<HTMLAnchorElement>) => {
e.preventDefault();
const target = e.target as HTMLAnchorElement;
const selectedTypeName = target.id;
// context from PermissionEditor.tsx
context.scrollToElement(selectedTypeName);
};
if (!i.checked)
return (
<CollapsedField
field={i}
onClick={handleClick}
onExpand={onExpand}
expanded={expanded}
/>
);
return (
<>
<span
className={`${styles.padd_small_left} ${styles.fw_large}`}
id={i.name}
>
{i.name}
</span>
{i.args && ' ('}
{i.args && (
<ul data-test={i.name}>
{i.args &&
Object.entries(i.args).map(([k, v]) => (
<ArgSelect
key={k}
keyName={k}
valueField={v}
value={fieldVal[k]}
setArg={setArg}
level={0}
/>
))}
</ul>
)}
{i.args && ' )'}
{i.return && (
<span className={styles.fw_large}>
:
<a
onClick={handleClick}
id={generateTypeString(i.return || '')}
href={`./permissions#${generateTypeString(i.return || '')}`}
>
{i.return}
</a>
</span>
)}
</>
);
};

View File

@ -0,0 +1,23 @@
import React from 'react';
const Pen = () => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="feather feather-edit-3"
>
<path d="M12 20h9" />
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" />
</svg>
);
};
export default Pen;

View File

@ -0,0 +1,170 @@
import React, { useEffect, useState } from 'react';
import { GraphQLSchema } from 'graphql';
import { generateSDL, getArgTreeFromPermissionSDL } from './utils';
import Button from '../../../Common/Button/Button';
import styles from '../../../Common/Permissions/PermissionStyles.scss';
import {
RemoteSchemaFields,
FieldType,
ArgTreeType,
PermissionEdit,
} from './types';
import { PermissionEditorContext } from './context';
import Tree from './Tree';
import { isEmpty } from '../../../Common/utils/jsUtils';
type PermissionEditorProps = {
permissionEdit: PermissionEdit;
isEditing: boolean;
isFetching: boolean;
schemaDefinition: string;
remoteSchemaFields: RemoteSchemaFields[];
introspectionSchema: GraphQLSchema;
setSchemaDefinition: (data: string) => void;
permCloseEdit: () => void;
saveRemoteSchemaPermission: (
successCb?: () => void,
errorCb?: () => void
) => void;
removeRemoteSchemaPermission: (
successCb?: () => void,
errorCb?: () => void
) => void;
};
const PermissionEditor: React.FC<PermissionEditorProps> = props => {
const {
permissionEdit,
isEditing,
isFetching,
schemaDefinition,
permCloseEdit,
saveRemoteSchemaPermission,
removeRemoteSchemaPermission,
setSchemaDefinition,
remoteSchemaFields,
introspectionSchema,
} = props;
const [state, setState] = useState<RemoteSchemaFields[] | FieldType[]>(
remoteSchemaFields
);
const [argTree, setArgTree] = useState<ArgTreeType>({}); // all @presets as an object tree
const [resultString, setResultString] = useState(''); // Generated SDL
const { isNewRole, isNewPerm } = permissionEdit;
useEffect(() => {
if (!state) return;
setResultString(generateSDL(state, argTree));
}, [state, argTree]);
useEffect(() => {
setState(remoteSchemaFields);
setResultString(schemaDefinition);
}, [remoteSchemaFields]);
useEffect(() => {
if (!isEmpty(schemaDefinition)) {
try {
const newArgTree = getArgTreeFromPermissionSDL(
schemaDefinition,
introspectionSchema
);
setArgTree(newArgTree);
} catch (e) {
console.error(e);
}
}
}, [schemaDefinition]);
if (!isEditing) return null;
const buttonStyle = styles.add_mar_right;
const closeEditor = () => {
permCloseEdit();
};
const save = () => {
saveRemoteSchemaPermission(closeEditor);
};
const saveFunc = () => {
setSchemaDefinition(resultString);
save();
};
const removeFunc = () => {
removeRemoteSchemaPermission(closeEditor);
};
const scrollToElement = (path: string) => {
let id = `type ${path}`;
let el = document.getElementById(id);
if (!el) {
// input types
id = `input ${path}`;
el = document.getElementById(id);
}
if (el) {
el.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'nearest',
});
setTimeout(() => {
// focusing element with css outline
// there is no callback for scrollIntoView, this is a hack to make UX better,
// simple implementation compared to adding another onscroll listener
if (el) el.focus();
}, 800);
}
};
const isSaveDisabled = isEmpty(resultString) || isFetching;
return (
<div className={styles.activeEdit}>
<div className={styles.tree}>
<PermissionEditorContext.Provider
value={{ argTree, setArgTree, scrollToElement }}
>
<Tree
key={permissionEdit.isNewRole ? 'NEW' : permissionEdit.role}
list={state as FieldType[]}
setState={setState}
permissionEdit={permissionEdit}
/>
{/* below helps to debug the SDL */}
{/* <code style={{ whiteSpace: 'pre-wrap' }}>{resultString}</code> */}
</PermissionEditorContext.Provider>
</div>
<Button
onClick={saveFunc}
color="yellow"
className={buttonStyle}
disabled={isSaveDisabled}
data-test="save-remote-schema-permissions"
>
Save Permissions
</Button>
{!(isNewRole || isNewPerm) && (
<Button
onClick={removeFunc}
color="red"
className={buttonStyle}
disabled={isFetching}
data-test="delete-remote-schema-permissions"
>
Remove Permissions
</Button>
)}
<Button color="white" className={buttonStyle} onClick={closeEditor}>
Cancel
</Button>
</div>
);
};
export default PermissionEditor;

View File

@ -0,0 +1,166 @@
import React, { useEffect, useState } from 'react';
import Helmet from 'react-helmet';
import { GraphQLSchema } from 'graphql';
import PermissionsTable from './PermissionsTable';
import PermissionEditor from './PermissionEditor';
import { useIntrospectionSchemaRemote } from '../graphqlUtils';
import globals from '../../../../Globals';
import styles from '../../../Common/Permissions/PermissionStyles.scss';
import { getRemoteSchemaFields, buildSchemaFromRoleDefn } from './utils';
import {
RemoteSchemaFields,
PermissionEdit,
PermOpenEditType,
PermissionsType,
} from './types';
import BulkSelect from './BulkSelect';
import { Dispatch } from '../../../../types';
export type PermissionsProps = {
allRoles: string[];
currentRemoteSchema: {
name: string;
permissions?: PermissionsType[];
};
bulkSelect: string[];
readOnlyMode: boolean;
permissionEdit: PermissionEdit;
isEditing: boolean;
isFetching: boolean;
schemaDefinition: string;
setSchemaDefinition: (data: string) => void;
permOpenEdit: PermOpenEditType;
permCloseEdit: () => void;
permSetBulkSelect: (checked: boolean, role: string) => void;
permSetRoleName: (name: string) => void;
dispatch: Dispatch;
fetchRoleList: () => void;
setDefaults: () => void;
saveRemoteSchemaPermission: (
successCb?: () => void,
errorCb?: () => void
) => void;
removeRemoteSchemaPermission: (
successCb?: () => void,
errorCb?: () => void
) => void;
permRemoveMultipleRoles: () => void;
};
const Permissions: React.FC<PermissionsProps> = props => {
const {
allRoles,
currentRemoteSchema,
permissionEdit,
isEditing,
isFetching,
bulkSelect,
schemaDefinition,
readOnlyMode = false,
dispatch,
setDefaults,
permCloseEdit,
saveRemoteSchemaPermission,
removeRemoteSchemaPermission,
setSchemaDefinition,
permRemoveMultipleRoles,
permOpenEdit,
permSetBulkSelect,
permSetRoleName,
} = props;
const [remoteSchemaFields, setRemoteSchemaFields] = useState<
RemoteSchemaFields[]
>([]);
React.useEffect(() => {
return () => {
setDefaults();
};
}, [setDefaults]);
const res = useIntrospectionSchemaRemote(
currentRemoteSchema.name,
{
'x-hasura-admin-secret': globals.adminSecret,
},
dispatch
);
const schema = res.schema as GraphQLSchema | null;
const { error, introspect } = res;
useEffect(() => {
if (!schema) return;
const isNewRole: boolean = permissionEdit.isNewRole;
let permissionsSchema: GraphQLSchema | null = null;
if (!isNewRole && !!schemaDefinition) {
permissionsSchema = buildSchemaFromRoleDefn(schemaDefinition);
}
// when server throws error while saving new role, do not reset the remoteSchemaFields
// persist the user defined schema in th UI
if (isNewRole && schemaDefinition) return;
if (schema)
setRemoteSchemaFields(getRemoteSchemaFields(schema, permissionsSchema));
}, [schema, permissionEdit?.isNewRole, schemaDefinition]);
if (error || !schema) {
return (
<div>
Error introspecting remote schema.{' '}
<a onClick={introspect} className={styles.cursorPointer} role="button">
{' '}
Try again{' '}
</a>
</div>
);
}
return (
<div>
<Helmet
title={`Permissions - ${currentRemoteSchema.name} - Remote Schemas | Hasura`}
/>
<PermissionsTable
allRoles={allRoles}
currentRemoteSchema={currentRemoteSchema}
permissionEdit={permissionEdit}
isEditing={isEditing}
bulkSelect={bulkSelect}
readOnlyMode={readOnlyMode}
permSetRoleName={permSetRoleName}
permSetBulkSelect={permSetBulkSelect}
setSchemaDefinition={setSchemaDefinition}
permOpenEdit={permOpenEdit}
permCloseEdit={permCloseEdit}
/>
{!!bulkSelect.length && (
<BulkSelect
bulkSelect={bulkSelect}
permRemoveMultipleRoles={permRemoveMultipleRoles}
/>
)}
<div className={`${styles.add_mar_bottom}`}>
{!readOnlyMode && (
<PermissionEditor
key={permissionEdit.isNewRole ? 'NEW' : permissionEdit.role}
permissionEdit={permissionEdit}
isFetching={isFetching}
isEditing={isEditing}
schemaDefinition={schemaDefinition}
remoteSchemaFields={remoteSchemaFields}
permCloseEdit={permCloseEdit}
saveRemoteSchemaPermission={saveRemoteSchemaPermission}
removeRemoteSchemaPermission={removeRemoteSchemaPermission}
setSchemaDefinition={setSchemaDefinition}
introspectionSchema={schema}
/>
)}
</div>
</div>
);
};
export default Permissions;

View File

@ -0,0 +1,203 @@
import React, { ChangeEvent } from 'react';
import styles from '../../../Common/Permissions/PermissionStyles.scss';
import PermTableHeader from '../../../Common/Permissions/TableHeader';
import PermTableBody from '../../../Common/Permissions/TableBody';
import { permissionsSymbols } from '../../../Common/Permissions/PermissionSymbols';
import { findRemoteSchemaPermission } from './utils';
import {
RolePermissions,
PermOpenEditType,
PermissionsType,
PermissionEdit,
} from './types';
export type PermissionsTableProps = {
setSchemaDefinition: (data: string) => void;
permOpenEdit: PermOpenEditType;
permCloseEdit: () => void;
permSetBulkSelect: (checked: boolean, role: string) => void;
permSetRoleName: (name: string) => void;
allRoles: string[];
currentRemoteSchema: {
name: string;
permissions?: PermissionsType[];
};
bulkSelect: string[];
readOnlyMode: boolean;
permissionEdit: PermissionEdit;
isEditing: boolean;
};
const queryTypes = ['Permission'];
const PermissionsTable: React.FC<PermissionsTableProps> = ({
allRoles,
currentRemoteSchema,
permissionEdit,
isEditing,
bulkSelect,
readOnlyMode,
permSetRoleName,
permSetBulkSelect,
setSchemaDefinition,
permOpenEdit,
permCloseEdit,
}) => {
const allPermissions = currentRemoteSchema?.permissions || [];
const headings = ['Role', ...queryTypes];
const dispatchRoleNameChange = (e: ChangeEvent<HTMLInputElement>) => {
permSetRoleName(e.target.value?.trim());
};
const getEditIcon = () => {
return (
<span className={styles.editPermsIcon}>
<i className="fa fa-pencil" aria-hidden="true" />
</span>
);
};
const getBulkCheckbox = (role: string, isNewRole: boolean) => {
const dispatchBulkSelect = (e: ChangeEvent<HTMLInputElement>) => {
const isChecked = e.target.checked;
const selectedRole = e.target.getAttribute('data-role');
permSetBulkSelect(isChecked, selectedRole as string);
};
const disableCheckbox = !findRemoteSchemaPermission(allPermissions, role);
return {
showCheckbox: !(role === 'admin' || isNewRole),
disableCheckbox,
title: disableCheckbox
? 'No permissions exist'
: 'Select for bulk actions',
bulkSelect,
onChange: dispatchBulkSelect,
role,
isNewRole,
checked: bulkSelect.find((e: any) => e === role),
};
};
// get root types for a given role
const getQueryTypes = (role: string, isNewRole: boolean) => {
return queryTypes.map(queryType => {
const dispatchOpenEdit = () => () => {
if (isNewRole && !!role) {
setSchemaDefinition('');
permOpenEdit(role, isNewRole, true);
} else if (role) {
const existingPerm = findRemoteSchemaPermission(allPermissions, role);
permOpenEdit(role, isNewRole, !existingPerm);
if (existingPerm) {
const schemaDefinitionSdl = existingPerm.definition.schema;
setSchemaDefinition(schemaDefinitionSdl);
} else {
setSchemaDefinition('');
}
} else {
const inputFocusElem = document.getElementById('new-role-input');
if (inputFocusElem) {
inputFocusElem.focus();
}
}
};
const dispatchCloseEdit = () => {
permCloseEdit();
setSchemaDefinition('');
};
const isCurrEdit =
isEditing &&
(permissionEdit.role === role ||
(permissionEdit.isNewRole && permissionEdit.newRole === role));
let editIcon;
let className = '';
let onClick = () => {};
if (role !== 'admin' && !readOnlyMode) {
editIcon = getEditIcon();
if (isCurrEdit) {
onClick = dispatchCloseEdit;
className += styles.currEdit;
} else {
className += styles.clickableCell;
onClick = dispatchOpenEdit();
}
}
const getRoleQueryPermission = () => {
let permissionAccess;
if (role === 'admin') {
permissionAccess = permissionsSymbols.fullAccess;
} else if (isNewRole) {
permissionAccess = permissionsSymbols.noAccess;
} else {
const existingPerm = findRemoteSchemaPermission(allPermissions, role);
if (!existingPerm) {
permissionAccess = permissionsSymbols.noAccess;
} else {
permissionAccess = permissionsSymbols.fullAccess;
}
}
return permissionAccess;
};
return {
permType: queryType,
className,
editIcon,
onClick,
dataTest: `${role}-${queryType}`,
access: getRoleQueryPermission(),
};
});
};
// form rolesList and permissions metadata associated with each role
const roleList = ['admin', ...allRoles];
const rolePermissions: RolePermissions[] = roleList.map(r => {
return {
roleName: r,
permTypes: getQueryTypes(r, false),
bulkSection: getBulkCheckbox(r, false),
};
});
// push permissions metadata associated with the new role
rolePermissions.push({
roleName: permissionEdit.newRole,
permTypes: getQueryTypes(permissionEdit.newRole, true),
bulkSection: getBulkCheckbox(permissionEdit.newRole, true),
isNewRole: true,
});
return (
<div>
<div>
<div className={styles.permissionsLegend}>
<span className={styles.permissionsLegendValue}>
{permissionsSymbols.fullAccess} : allowed
</span>
<span className={styles.permissionsLegendValue}>
{permissionsSymbols.noAccess} : not allowed
</span>
</div>
</div>
<table className={`table table-bordered ${styles.permissionsTable}`}>
<PermTableHeader headings={headings} />
<PermTableBody
rolePermissions={rolePermissions}
dispatchRoleNameChange={dispatchRoleNameChange}
/>
</table>
</div>
);
};
export default PermissionsTable;

View File

@ -0,0 +1,103 @@
import React, { useState, useEffect, ReactText } from 'react';
import { GraphQLEnumType, GraphQLInputField, GraphQLScalarType } from 'graphql';
import Pen from './Pen';
import { useDebouncedEffect } from '../../../../hooks/useDebounceEffect';
import { isNumberString } from '../../../Common/utils/jsUtils';
import { ArgTreeType } from './types';
import styles from '../../../Common/Permissions/PermissionStyles.scss';
interface RSPInputProps {
k: string;
editMode: boolean;
value?: ArgTreeType | ReactText;
v: GraphQLInputField;
setArgVal: (v: Record<string, unknown>) => void;
setEditMode: (b: boolean) => void;
}
const RSPInputComponent: React.FC<RSPInputProps> = ({
k,
editMode,
value = '',
setArgVal,
v,
setEditMode,
}) => {
const isSessionvar = () => {
if (
v.type instanceof GraphQLScalarType ||
v.type instanceof GraphQLEnumType
)
return true;
return false;
};
const [localValue, setLocalValue] = useState<ReactText>(
typeof value === 'object' ? '' : value
);
const inputRef = React.useRef<HTMLInputElement>(null);
// focus the input element; onClick of Pen Icon
useEffect(() => {
if (editMode && inputRef && inputRef.current) inputRef.current.focus();
}, [editMode]);
useDebouncedEffect(
() => {
if (
(v?.type?.inspect() === 'Int' || v?.type?.inspect() === 'Int!') &&
localValue &&
isNumberString(localValue)
) {
if (localValue === '0') return setArgVal({ [v?.name]: 0 });
return setArgVal({ [v?.name]: Number(localValue) });
}
setArgVal({ [v?.name]: localValue });
},
500,
[localValue]
);
const toggleSessionVariable = (e: React.MouseEvent) => {
const input = e.target as HTMLButtonElement;
setLocalValue(input.value);
};
return (
<>
<label htmlFor={k}> {k}:</label>
{editMode ? (
<>
<input
value={localValue}
ref={inputRef}
data-test={`input-${k}`}
style={{
border: 0,
borderBottom: '2px dotted black',
borderRadius: 0,
}}
onChange={e => setLocalValue(e.target.value)}
/>
{isSessionvar() && (
<button
value="X-Hasura-User-Id"
onClick={toggleSessionVariable}
className={styles.sessionVarButton}
>
[X-Hasura-User-Id]
</button>
)}
</>
) : (
<button data-test={`pen-${k}`} onClick={() => setEditMode(true)}>
<Pen />
</button>
)}
</>
);
};
const RSPInput = React.memo(RSPInputComponent);
export default RSPInput;

View File

@ -0,0 +1,88 @@
import React from 'react';
import CommonTabLayout from '../../../Common/Layout/CommonTabLayout/CommonTabLayout';
import { NotFoundError } from '../../../Error/PageNotFound';
import { appPrefix } from '../constants';
import styles from '../RemoteSchema.scss';
import { RemoteSchema } from '../../../../metadata/types';
const tabInfo = {
details: {
display_text: 'Details',
},
modify: {
display_text: 'Modify',
},
permissions: {
display_text: 'Permissions',
},
};
export type RSPWrapperProps = {
params: { remoteSchemaName: string };
allRemoteSchemas?: RemoteSchema[];
tabName: string;
viewRemoteSchema: (data: string) => void;
permissionRenderer: (currentRemoteSchema: RemoteSchema) => React.ReactNode;
};
const RSPWrapper: React.FC<RSPWrapperProps> = ({
params: { remoteSchemaName },
allRemoteSchemas,
tabName,
viewRemoteSchema,
permissionRenderer,
}) => {
React.useEffect(() => {
viewRemoteSchema(remoteSchemaName);
return () => {
viewRemoteSchema('');
};
}, [remoteSchemaName]);
const currentRemoteSchema =
allRemoteSchemas &&
allRemoteSchemas.find(rs => rs.name === remoteSchemaName);
if (!currentRemoteSchema) {
viewRemoteSchema('');
throw new NotFoundError();
}
const breadCrumbs = [
{
title: 'Remote schemas',
url: appPrefix,
},
{
title: 'Manage',
url: `${appPrefix}/manage`,
},
{
title: remoteSchemaName,
url: `${appPrefix}/manage/${remoteSchemaName}/modify`,
},
{
title: tabName,
url: '',
},
];
return (
<>
<CommonTabLayout
appPrefix={appPrefix}
currentTab={tabName}
heading={remoteSchemaName}
tabsInfo={tabInfo}
breadCrumbs={breadCrumbs}
baseUrl={`${appPrefix}/manage/${remoteSchemaName}`}
showLoader={false}
testPrefix="remote-schema-container-tabs"
/>
<div className={styles.add_pad_top}>
{permissionRenderer(currentRemoteSchema)}
</div>
</>
);
};
export default RSPWrapper;

View File

@ -0,0 +1,121 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { FieldType, ExpandedItems, PermissionEdit } from './types';
import { Field } from './Field';
import styles from '../../../Common/Permissions/PermissionStyles.scss';
import { addDepFields, getExpandeItems } from './utils';
type RSPTreeComponentProps = {
list: FieldType[];
depth?: number;
permissionEdit?: PermissionEdit;
setState: (d: FieldType[], t?: FieldType) => void;
onExpand?: () => void;
};
const Tree: React.FC<RSPTreeComponentProps> = ({
list,
setState,
depth = 1,
permissionEdit,
}) => {
const [expandedItems, setExpandedItems] = useState<ExpandedItems>({});
const prevIsNewRole = useRef(false);
const onCheck = useCallback(
ix => (e: React.FormEvent<HTMLInputElement>) => {
const newList = [...list] as FieldType[];
const target = e.target as HTMLInputElement;
newList[ix] = { ...list[ix], checked: target.checked };
setState([...newList], newList[ix]);
},
[setState, list]
);
const setItem = useCallback(
(ix: number) => (newState: FieldType) => {
const newList = [...list];
newList[ix] = { ...newState };
setState([...newList]);
},
[setState, list]
);
const setValue = useCallback(
ix => (newState: FieldType[], field?: FieldType) => {
let newList = [...list];
newList[ix] = { ...list[ix], children: [...newState] };
if (field && field.checked) newList = addDepFields(newList, field);
setState([...newList]);
},
[setState, list]
);
useEffect(() => {
const expandedItemsFromList = getExpandeItems(list);
setExpandedItems({ ...expandedItems, ...expandedItemsFromList }); // this will only handle expand, it wont collapse anything which are already expanded.
}, [list]);
useEffect(() => {
if (
permissionEdit?.isNewRole &&
permissionEdit?.isNewRole !== prevIsNewRole.current
) {
// ignore the new role name change event
setExpandedItems({});
prevIsNewRole.current = permissionEdit.isNewRole;
}
}, [permissionEdit]);
const toggleExpand = (ix: number) => () => {
setExpandedItems(oldExpandedItems => {
const newState = !oldExpandedItems[ix];
const newExpandeditems = { ...oldExpandedItems, [ix]: newState };
return newExpandeditems;
});
};
return (
<ul>
{list.map(
(i: FieldType, ix) =>
!i.name.startsWith('enum') &&
!i.name.startsWith('scalar') && (
<li key={i.name} className={styles.treeNodes}>
{i.checked !== undefined && (
<input
type="checkbox"
id={i.name}
name={i.name}
checked={i.checked}
data-test={`checkbox-${i.name}`}
onChange={onCheck(ix)}
/>
)}
{i.children && (
<button onClick={toggleExpand(ix)}>
{expandedItems[ix] ? '-' : '+'}
</button>
)}
<Field
i={i}
setItem={setItem(ix)}
key={i.name}
onExpand={toggleExpand(ix)}
expanded={expandedItems[ix]}
/>
{i.children && expandedItems[ix] && (
<MemoizedTree
list={i.children}
depth={depth + 1}
setState={setValue(ix)}
/>
)}
</li>
)
)}
</ul>
);
};
const MemoizedTree = React.memo(Tree);
export default MemoizedTree;

View File

@ -0,0 +1,11 @@
import React from 'react';
import { ArgTreeType } from './types';
interface PermissionEditorContextType {
argTree?: ArgTreeType;
setArgTree?: React.Dispatch<React.SetStateAction<ArgTreeType>>;
scrollToElement?: (s: string) => void;
}
export const PermissionEditorContext = React.createContext<
PermissionEditorContextType
>({});

View File

@ -0,0 +1,96 @@
import React from 'react';
import { connect, ConnectedProps } from 'react-redux';
import Permissions, { PermissionsProps } from './Permissions';
import RSPWrapper, { RSPWrapperProps } from './RSPWrapper';
import {
permRemoveMultipleRoles,
VIEW_REMOTE_SCHEMA,
saveRemoteSchemaPermission,
removeRemoteSchemaPermission,
} from '../Actions';
import {
permCloseEdit,
setSchemaDefinition,
setDefaults,
permOpenEdit,
permSetRoleName,
permSetBulkSelect,
} from './reducer';
import { Dispatch, ReduxState } from '../../../../types';
import {
getRemoteSchemas,
rolesSelector,
getRemoteSchemaPermissions,
} from '../../../../metadata/selector';
import { RemoteSchema } from '../../../../metadata/types';
export type RSPContainerProps = {
allRoles: string[];
allRemoteSchemas: RemoteSchema[];
params: { remoteSchemaName: string };
viewRemoteSchema: (data: string) => void;
};
const RSP: React.FC<Props> = props => {
const { allRoles, allRemoteSchemas, params, viewRemoteSchema } = props;
return (
<RSPWrapper
params={params}
allRemoteSchemas={allRemoteSchemas}
tabName="permissions"
viewRemoteSchema={viewRemoteSchema}
permissionRenderer={currentRemoteSchema => (
<Permissions
allRoles={allRoles}
{...props}
{...{ currentRemoteSchema }}
/>
)}
/>
);
};
const mapStateToProps = (state: ReduxState) => {
return {
...getRemoteSchemaPermissions(state),
...state.remoteSchemas,
allRoles: rolesSelector(state),
allRemoteSchemas: getRemoteSchemas(state),
readOnlyMode: state.main.readOnlyMode,
};
};
const mapDispatchToProps = (dispatch: Dispatch) => {
return {
dispatch,
permRemoveMultipleRoles: () => dispatch(permRemoveMultipleRoles()),
viewRemoteSchema: (data: string) =>
dispatch({ type: VIEW_REMOTE_SCHEMA, data }),
saveRemoteSchemaPermission: (
successCb?: () => void,
errorCb?: () => void
) => dispatch(saveRemoteSchemaPermission(successCb, errorCb)),
removeRemoteSchemaPermission: (
successCb?: () => void,
errorCb?: () => void
) => dispatch(removeRemoteSchemaPermission(successCb, errorCb)),
setSchemaDefinition: (data: string) => dispatch(setSchemaDefinition(data)),
setDefaults: () => dispatch(setDefaults()),
permCloseEdit: () => dispatch(permCloseEdit()),
permOpenEdit: (role: string, newRole: boolean, existingPerms: boolean) =>
dispatch(permOpenEdit(role, newRole, existingPerms)),
permSetBulkSelect: (checked: boolean, role: string) =>
dispatch(permSetBulkSelect(checked, role)),
permSetRoleName: (name: string) => dispatch(permSetRoleName(name)),
};
};
const connector = connect(mapStateToProps, mapDispatchToProps);
type InjectedProps = ConnectedProps<typeof connector>;
type ComponentProps = RSPWrapperProps & PermissionsProps;
type Props = ComponentProps & InjectedProps;
const RSPContainer = connector(RSP);
export default RSPContainer;

View File

@ -0,0 +1,158 @@
import defaultState from './state';
import { Dispatch } from '../../../../types';
import { updateBulkSelect } from './utils';
import {
PERMISSIONS_OPEN_EDIT,
PERMISSIONS_CLOSE_EDIT,
SET_ROLE_NAME,
SET_DEFAULTS,
SET_SCHEMA_DEFINITION,
PERM_DESELECT_BULK,
PERM_SELECT_BULK,
PERM_RESET_BULK_SELECT,
MAKE_REQUEST,
REQUEST_FAILURE,
REQUEST_SUCCESS,
PermOpenEdit,
PermCloseEdit,
PermSetRoleName,
SetDefaults,
SetSchemaDefinition,
PermSelectBulk,
PermDeslectBulk,
PermResetBulkSelect,
MakeRequest,
SetRequestFailure,
SetRequestSuccess,
RSPEvents,
} from './types';
export const permOpenEdit = (
role: string,
isNewRole: boolean,
isNewPerm: boolean
): PermOpenEdit => ({
type: PERMISSIONS_OPEN_EDIT,
role,
isNewRole,
isNewPerm,
});
export const permCloseEdit = (): PermCloseEdit => ({
type: PERMISSIONS_CLOSE_EDIT,
});
export const permSetRoleName = (rolename: string): PermSetRoleName => ({
type: SET_ROLE_NAME,
rolename,
});
export const setDefaults = (): SetDefaults => ({
type: SET_DEFAULTS,
});
export const setSchemaDefinition = (
definition: string
): SetSchemaDefinition => ({
type: SET_SCHEMA_DEFINITION,
definition,
});
export const permSelectBulk = (selectedRole: string): PermSelectBulk => ({
type: PERM_SELECT_BULK,
selectedRole,
});
export const permDeslectBulk = (selectedRole: string): PermDeslectBulk => ({
type: PERM_DESELECT_BULK,
selectedRole,
});
export const permResetBulkSelect = (): PermResetBulkSelect => ({
type: PERM_RESET_BULK_SELECT,
});
export const makeRequest = (): MakeRequest => ({ type: MAKE_REQUEST });
export const setRequestSuccess = (): SetRequestSuccess => ({
type: REQUEST_SUCCESS,
});
export const setRequestFailure = (): SetRequestFailure => ({
type: REQUEST_FAILURE,
});
export const permSetBulkSelect = (isChecked: boolean, selectedRole: string) => {
return (dispatch: Dispatch) => {
if (isChecked) {
dispatch(permSelectBulk(selectedRole));
} else {
dispatch(permDeslectBulk(selectedRole));
}
};
};
const reducer = (state = defaultState, action: RSPEvents) => {
switch (action.type) {
case MAKE_REQUEST:
return {
...state,
isFetching: true,
};
case REQUEST_SUCCESS:
case REQUEST_FAILURE:
return {
...state,
isFetching: false,
};
case PERMISSIONS_OPEN_EDIT:
return {
...state,
isEditing: true,
permissionEdit: {
...state.permissionEdit,
isNewRole: !!action.isNewRole,
isNewPerm: !!action.isNewPerm,
role: action.role,
filter: {},
},
};
case PERMISSIONS_CLOSE_EDIT:
return {
...state,
isEditing: false,
permissionEdit: { ...defaultState.permissionEdit },
};
case SET_SCHEMA_DEFINITION:
return {
...state,
schemaDefinition: action.definition,
};
case SET_ROLE_NAME:
return {
...state,
permissionEdit: {
...state.permissionEdit,
newRole: action.rolename,
},
};
case PERM_SELECT_BULK:
return {
...state,
bulkSelect: updateBulkSelect(
state.bulkSelect,
action.selectedRole,
true
),
};
case PERM_DESELECT_BULK:
return {
...state,
bulkSelect: updateBulkSelect(
state.bulkSelect,
action.selectedRole,
false
),
};
case PERM_RESET_BULK_SELECT:
return {
...state,
bulkSelect: [],
};
case SET_DEFAULTS:
return defaultState;
default:
return state;
}
};
export default reducer;

View File

@ -0,0 +1,24 @@
import { PermissionEdit } from './types';
export type RemoteSchemaPermissionsState = {
isEditing: false;
isFetching: false;
permissionEdit: PermissionEdit;
schemaDefinition: string;
bulkSelect: string[];
};
const state: RemoteSchemaPermissionsState = {
isEditing: false,
isFetching: false,
permissionEdit: {
newRole: '',
isNewRole: false,
isNewPerm: false,
role: '',
},
schemaDefinition: '',
bulkSelect: [],
};
export default state;

View File

@ -0,0 +1,157 @@
import {
GraphQLField,
GraphQLArgument,
GraphQLInputFieldMap,
GraphQLEnumValue,
GraphQLType,
} from 'graphql';
import { Action as ReduxAction } from 'redux';
import { Dispatch } from '../../../../types';
export const PERMISSIONS_OPEN_EDIT =
'RemoteSchemas/Permissions/PERMISSIONS_OPEN_EDIT';
export const PERMISSIONS_CLOSE_EDIT =
'RemoteSchemas/Permissions/PERMISSIONS_CLOSE_EDIT';
export const SET_ROLE_NAME = 'RemoteSchemas/Permissions/SET_ROLE_NAME';
export const SET_DEFAULTS = 'RemoteSchemas/Permissions/SET_DEFAULTS';
export const SET_SCHEMA_DEFINITION =
'RemoteSchemas/Permissions/SET_SCHEMA_DEFINITION';
export const PERM_SELECT_BULK = 'RemoteSchemas/Permissions/PERM_SELECT_BULK';
export const PERM_DESELECT_BULK =
'RemoteSchemas/Permissions/PERM_DESELECT_BULK';
export const PERM_RESET_BULK_SELECT =
'RemoteSchemas/Permissions/PERM_RESET_BULK_SELECT';
export const MAKE_REQUEST = 'RemoteSchemas/Permissions/MAKE_REQUEST';
export const REQUEST_SUCCESS = 'RemoteSchemas/Permissions/REQUEST_SUCCESS';
export const REQUEST_FAILURE = 'RemoteSchemas/Permissions/REQUEST_FAILURE';
export type PermOpenEditType = (
role: string,
newRole: boolean,
existingPerms: boolean
) => void;
export type PermissionEdit = {
newRole: string;
isNewRole: boolean;
isNewPerm: boolean;
role: string;
};
export type ArgTreeType = {
[key: string]: string | number | ArgTreeType;
};
export type Actions = {
setSchemaDefinition: (data: string) => void;
permOpenEdit: PermOpenEditType;
permCloseEdit: () => void;
permSetBulkSelect: (checked: boolean, role: string) => void;
permSetRoleName: (name: string) => void;
dispatch: Dispatch;
fetchRoleList: () => void;
setDefaults: () => void;
saveRemoteSchemaPermission: (data: any) => void;
removeRemoteSchemaPermission: (data: any) => void;
permRemoveMultipleRoles: () => void;
};
export type RolePermissions = {
roleName: string;
permTypes: Record<string, any>;
bulkSection: Record<string, any>;
isNewRole?: boolean;
};
export type PermissionsType = {
definition: { schema: string };
role: string;
remote_schema_name: string;
comment: string | null;
};
export type ChildArgumentType = {
children?: GraphQLInputFieldMap | GraphQLEnumValue[];
path?: string;
childrenType?: GraphQLType;
};
export type CustomFieldType = {
name: string;
checked: boolean;
args?: GraphQLArgument[];
return?: string;
typeName?: string;
children?: FieldType[];
};
export type FieldType = CustomFieldType & GraphQLField<any, any>;
export type RemoteSchemaFields =
| {
name: string;
typeName: string;
children: FieldType[] | CustomFieldType[];
}
| FieldType;
export type ExpandedItems = {
[key: string]: boolean;
};
/*
* Redux Action types
*/
export interface PermOpenEdit extends ReduxAction {
type: typeof PERMISSIONS_OPEN_EDIT;
role: string;
isNewRole: boolean;
isNewPerm: boolean;
}
export interface PermCloseEdit extends ReduxAction {
type: typeof PERMISSIONS_CLOSE_EDIT;
}
export interface PermSetRoleName extends ReduxAction {
type: typeof SET_ROLE_NAME;
rolename: string;
}
export interface SetDefaults extends ReduxAction {
type: typeof SET_DEFAULTS;
}
export interface SetSchemaDefinition extends ReduxAction {
type: typeof SET_SCHEMA_DEFINITION;
definition: string;
}
export interface PermSelectBulk extends ReduxAction {
type: typeof PERM_SELECT_BULK;
selectedRole: string;
}
export interface PermDeslectBulk extends ReduxAction {
type: typeof PERM_DESELECT_BULK;
selectedRole: string;
}
export interface PermResetBulkSelect extends ReduxAction {
type: typeof PERM_RESET_BULK_SELECT;
}
export interface MakeRequest extends ReduxAction {
type: typeof MAKE_REQUEST;
}
export interface SetRequestSuccess extends ReduxAction {
type: typeof REQUEST_SUCCESS;
}
export interface SetRequestFailure extends ReduxAction {
type: typeof REQUEST_FAILURE;
}
export type RSPEvents =
| PermOpenEdit
| PermCloseEdit
| PermSetRoleName
| SetDefaults
| SetSchemaDefinition
| PermSelectBulk
| PermDeslectBulk
| PermResetBulkSelect
| MakeRequest
| SetRequestFailure
| SetRequestSuccess;

View File

@ -0,0 +1,777 @@
import {
GraphQLEnumType,
GraphQLInputObjectType,
GraphQLNonNull,
GraphQLObjectType,
GraphQLScalarType,
GraphQLSchema,
parse,
DocumentNode,
ObjectFieldNode,
FieldDefinitionNode,
InputValueDefinitionNode,
ArgumentNode,
ObjectTypeDefinitionNode,
GraphQLInputField,
GraphQLList,
GraphQLInputFieldMap,
GraphQLFieldMap,
ValueNode,
GraphQLInputType,
buildSchema,
} from 'graphql';
import {
isJsonString,
isEmpty,
isArrayString,
} from '../../../Common/utils/jsUtils';
import {
PermissionEdit,
RemoteSchemaFields,
FieldType,
ArgTreeType,
PermissionsType,
CustomFieldType,
ChildArgumentType,
ExpandedItems,
} from './types';
import Migration from '../../../../utils/migration/Migration';
export const findRemoteSchemaPermission = (
perms: PermissionsType[],
role: string
) => {
return perms.find(p => p.role === role);
};
export const getCreateRemoteSchemaPermissionQuery = (
def: { role: string },
remoteSchemaName: string,
schemaDefinition: string
) => {
return {
type: 'add_remote_schema_permissions',
args: {
remote_schema: remoteSchemaName,
role: def.role,
definition: {
schema: schemaDefinition,
},
},
};
};
export const getDropRemoteSchemaPermissionQuery = (
role: string,
remoteSchemaName: string
) => {
return {
type: 'drop_remote_schema_permissions',
args: {
remote_schema: remoteSchemaName,
role,
},
};
};
export const getRemoteSchemaPermissionQueries = (
permissionEdit: PermissionEdit,
allPermissions: PermissionsType[],
remoteSchemaName: string,
schemaDefinition: string
) => {
const { role, newRole } = permissionEdit;
const permRole = (newRole || role).trim();
const existingPerm = findRemoteSchemaPermission(allPermissions, permRole);
const migration = new Migration();
if (newRole || (!newRole && !existingPerm)) {
migration.add(
getCreateRemoteSchemaPermissionQuery(
{
role: permRole,
},
remoteSchemaName,
schemaDefinition
),
getDropRemoteSchemaPermissionQuery(permRole, remoteSchemaName)
);
}
if (existingPerm) {
migration.add(
getDropRemoteSchemaPermissionQuery(permRole, remoteSchemaName),
getDropRemoteSchemaPermissionQuery(permRole, remoteSchemaName)
);
migration.add(
getCreateRemoteSchemaPermissionQuery(
{ role: permRole },
remoteSchemaName,
schemaDefinition
),
getCreateRemoteSchemaPermissionQuery(
{ role: permRole },
remoteSchemaName,
existingPerm.definition.schema
)
);
}
return {
upQueries: migration.upMigration,
downQueries: migration.downMigration,
};
};
export const updateBulkSelect = (
bulkSelect: string[],
selectedRole: string,
isAdd: boolean
) => {
const bulkRes = isAdd
? [...bulkSelect, selectedRole]
: bulkSelect.filter(e => e !== selectedRole);
return bulkRes;
};
/**
* Sets query_root and mutation_root in UI tree.
* @param introspectionSchema Remote Schema introspection schema.
* @param permissionsSchema Permissions coming from saved role.
* @param typeS Type of args.
* @returns Array of schema fields (query_root and mutation_root)
*/
export const getTree = (
introspectionSchema: GraphQLSchema | null,
permissionsSchema: GraphQLSchema | null,
typeS: string
) => {
const introspectionSchemaFields =
typeS === 'QUERY'
? introspectionSchema!.getQueryType()?.getFields()
: introspectionSchema!.getMutationType()?.getFields();
let permissionsSchemaFields:
| GraphQLFieldMap<any, any, Record<string, any>>
| null
| undefined = null;
if (permissionsSchema !== null) {
permissionsSchemaFields =
typeS === 'QUERY'
? permissionsSchema!.getQueryType()?.getFields()
: permissionsSchema!.getMutationType()?.getFields();
}
if (introspectionSchemaFields) {
return Object.values(introspectionSchemaFields).map(
({ name, args: argArray, type, ...rest }: any) => {
let checked = false;
const args = argArray.reduce((p: ArgTreeType, c: FieldType) => {
return { ...p, [c.name]: { ...c } };
}, {});
if (
permissionsSchema !== null &&
permissionsSchemaFields &&
name in permissionsSchemaFields
) {
checked = true;
}
return { name, checked, args, return: type.toString(), ...rest };
}
);
}
return [];
};
export const getSchemaRoots = (schema: GraphQLSchema) => {
if (!schema) return [];
const res = [schema.getQueryType()?.name]; // query root will be always present
if (schema.getMutationType()?.name) res.push(schema.getMutationType()?.name);
if (schema.getSubscriptionType()?.name)
res.push(schema.getSubscriptionType()?.name);
return res;
};
/**
* Sets input types, object types, scalar types and enum types in UI tree.
* @param introspectionSchema - Remote schema introspection schema.
* @param permissionsSchema - Permissions coming from saved role.
* @returns Array of all types
*/
export const getType = (
introspectionSchema: GraphQLSchema | null,
permissionsSchema: GraphQLSchema | null
) => {
const introspectionSchemaFields = introspectionSchema!.getTypeMap();
let permissionsSchemaFields: any = null;
if (permissionsSchema !== null) {
permissionsSchemaFields = permissionsSchema!.getTypeMap();
}
const enumTypes: RemoteSchemaFields[] = [];
const scalarTypes: RemoteSchemaFields[] = [];
const inputObjectTypes: RemoteSchemaFields[] = [];
const objectTypes: RemoteSchemaFields[] = [];
Object.entries(introspectionSchemaFields).forEach(([key, value]: any) => {
if (
!(
value instanceof GraphQLObjectType ||
value instanceof GraphQLInputObjectType ||
value instanceof GraphQLEnumType ||
value instanceof GraphQLScalarType
)
)
return;
const name = value.inspect();
const roots = introspectionSchema
? getSchemaRoots(introspectionSchema)
: [];
if (roots.includes(name)) return;
if (name.startsWith('__')) return;
const type: RemoteSchemaFields = {
name: ``,
typeName: ``,
children: [],
};
type.typeName = name;
if (value instanceof GraphQLEnumType) {
type.name = `enum ${name}`;
const values = value.getValues();
const childArray: CustomFieldType[] = [];
let checked = false;
if (
permissionsSchema !== null &&
permissionsSchemaFields !== null &&
key in permissionsSchemaFields
)
checked = true;
values.forEach(val => {
childArray.push({
name: val.name,
checked,
});
});
type.children = childArray;
enumTypes.push(type);
} else if (value instanceof GraphQLScalarType) {
type.name = `scalar ${name}`;
let checked = false;
if (
permissionsSchema !== null &&
permissionsSchemaFields !== null &&
key in permissionsSchemaFields
)
checked = true;
const childArray: CustomFieldType[] = [{ name: type.name, checked }];
type.children = childArray;
scalarTypes.push(type);
} else if (value instanceof GraphQLObjectType) {
type.name = `type ${name}`;
} else if (value instanceof GraphQLInputObjectType) {
type.name = `input ${name}`;
}
if (
value instanceof GraphQLObjectType ||
value instanceof GraphQLInputObjectType
) {
const childArray: CustomFieldType[] = [];
const fieldVal = value.getFields();
let permissionsFieldVal: GraphQLFieldMap<any, any, any> = {};
let isFieldPresent = true;
// Check if the type is present in the permission schema coming from user.
if (permissionsSchema !== null && permissionsSchemaFields !== null) {
if (key in permissionsSchemaFields) {
permissionsFieldVal = permissionsSchemaFields[key].getFields();
} else {
isFieldPresent = false;
}
}
// Checked is true when type is present and the fields are present in type
Object.entries(fieldVal).forEach(([k, v]) => {
let checked = false;
if (
permissionsSchema !== null &&
isFieldPresent &&
k in permissionsFieldVal
) {
checked = true;
}
childArray.push({
name: v.name,
checked,
return: v.type.toString(),
});
});
type.children = childArray;
if (value instanceof GraphQLObjectType) objectTypes.push(type);
if (value instanceof GraphQLInputObjectType) inputObjectTypes.push(type);
}
});
return [...objectTypes, ...inputObjectTypes, ...enumTypes, ...scalarTypes];
};
export const getRemoteSchemaFields = (
schema: GraphQLSchema,
permissionsSchema: GraphQLSchema | null
): RemoteSchemaFields[] => {
const types = getType(schema, permissionsSchema);
const queryRoot = schema?.getQueryType()?.name;
const mutationRoot = schema?.getMutationType()?.name;
const remoteFields = [
{
name: `type ${queryRoot}`,
typeName: '__query_root',
children: getTree(schema, permissionsSchema, 'QUERY'),
},
];
if (mutationRoot) {
remoteFields.push({
name: `type ${mutationRoot}`,
typeName: '__mutation_root',
children: getTree(schema, permissionsSchema, 'MUTATION'),
});
}
return [...remoteFields, ...types];
};
// method that tells whether the field is nested or not, if nested it returns the children
export const getChildArguments = (v: GraphQLInputField): ChildArgumentType => {
if (typeof v === 'string') return {}; // value field
if (v?.type instanceof GraphQLInputObjectType && v?.type?.getFields)
return {
children: v?.type?.getFields(),
path: 'type._fields',
childrenType: v?.type,
};
// 1st order
if (v?.type instanceof GraphQLNonNull || v?.type instanceof GraphQLList) {
const children = getChildArguments({
type: v?.type.ofType,
} as GraphQLInputField).children;
if (isEmpty(children)) return {};
return {
children,
path: 'type.ofType',
childrenType: v?.type?.ofType,
};
}
return {};
};
const isList = (gqlArg: GraphQLInputField, value: string) =>
gqlArg &&
gqlArg.type instanceof GraphQLList &&
typeof value === 'string' &&
isArrayString(value) &&
!value.toLowerCase().startsWith('x-hasura');
// utility function for getSDLField
const serialiseArgs = (args: ArgTreeType, argDef: GraphQLInputField) => {
let res = '{';
const { children } = getChildArguments(argDef);
Object.entries(args).forEach(([key, value]) => {
if (isEmpty(value) || isEmpty(children)) {
return;
}
const gqlArgs = children as GraphQLInputFieldMap;
const gqlArg = gqlArgs[key];
if (typeof value === 'string' || typeof value === 'number') {
let val;
const isEnum =
gqlArg &&
gqlArg.type instanceof GraphQLEnumType &&
typeof value === 'string' &&
!value.toLowerCase().startsWith('x-hasura');
switch (true) {
case isEnum:
val = `${key}:${value}`; // no double quotes
break;
case typeof value === 'number':
val = `${key}: ${value} `;
break;
case typeof value === 'string' && isList(gqlArg, value):
val = `${key}: ${value} `;
break;
default:
val = `${key}:"${value}"`;
break;
}
if (res === '{') {
res = `${res} ${val}`;
} else {
res = `${res} , ${val}`;
}
} else if (value && typeof value === 'object') {
if (children && typeof children === 'object' && gqlArg) {
const valString = serialiseArgs(value, gqlArg);
if (valString && res === '{') res = `${res} ${key}: ${valString}`;
else if (valString) res = `${res} , ${key}: ${valString}`;
}
}
});
if (res === `{`) return; // dont return string when there is no value
return `${res}}`;
};
const isEnumType = (type: GraphQLInputType): boolean => {
if (type instanceof GraphQLList || type instanceof GraphQLNonNull)
return isEnumType(type.ofType);
else if (type instanceof GraphQLEnumType) return true;
return false;
};
// Check if type belongs to default gql scalar types
const checkDefaultGQLScalarType = (typeName: string): boolean => {
const gqlDefaultTypes = ['Boolean', 'Float', 'String', 'Int', 'ID'];
if (gqlDefaultTypes.indexOf(typeName) > -1) return true;
return false;
};
const checkEmptyType = (type: RemoteSchemaFields) => {
const isChecked = (element: FieldType | CustomFieldType) => element.checked;
if (type.children) return type.children.some(isChecked);
};
/**
* Builds the SDL string for each field / type.
* @param type - Data source object containing a schema field.
* @param argTree - Arguments tree in case of types with argument presets.
* @returns SDL string for passed field.
*/
const getSDLField = (
type: RemoteSchemaFields,
argTree: Record<string, any> | null
): string => {
if (!checkEmptyType(type)) return ''; // check if no child is selected for a type
let result = ``;
const typeName: string = type.name;
// add scalar fields to SDL
if (typeName.startsWith('scalar')) {
if (type.typeName && checkDefaultGQLScalarType(type.typeName))
return result; // if default GQL scalar type, return empty string
result = `${typeName}`;
return `${result}\n`;
}
// add other fields to SDL
result = `${typeName}{`;
if (type.children)
type.children.forEach(f => {
if (!f.checked) return null;
let fieldStr = f.name;
// enum types don't have args
if (!typeName.startsWith('enum')) {
if (f.args && !isEmpty(f.args)) {
fieldStr = `${fieldStr}(`;
Object.values(f.args).forEach((arg: GraphQLInputField) => {
let valueStr = `${arg.name} : ${arg.type.inspect()}`;
if (argTree && argTree[f.name] && argTree[f.name][arg.name]) {
const argName = argTree[f.name][arg.name];
let unquoted;
const isEnum =
typeof argName === 'string' &&
argName &&
!argName.toLowerCase().startsWith('x-hasura') &&
isEnumType(arg.type);
if (typeof argName === 'object') {
unquoted = serialiseArgs(argName, arg);
} else if (typeof argName === 'number') {
unquoted = `${argName}`;
} else if (isEnum) {
unquoted = `${argName}`;
} else {
unquoted = `"${argName}"`;
}
if (!isEmpty(unquoted))
valueStr = `${valueStr} @preset(value: ${unquoted})`;
}
fieldStr = `${fieldStr + valueStr} `;
});
fieldStr = `${fieldStr})`;
fieldStr = `${fieldStr}: ${f.return}`;
} else fieldStr = `${fieldStr} : ${f.return}`; // normal data type - ie: without arguments/ presets
}
result = `${result}
${fieldStr}`;
});
return `${result}\n}`;
};
/**
* Generate SDL string having input types and object types.
* @param types - Remote schema introspection schema.
* @returns String having all enum types and scalar types.
*/
export const generateSDL = (
types: RemoteSchemaFields[] | FieldType[],
argTree: ArgTreeType
) => {
let prefix = `schema{`;
let result = '';
types.forEach(type => {
const fieldDef = getSDLField(type, argTree);
if (!isEmpty(fieldDef) && type.typeName === '__query_root' && type.name) {
const name = type.name.split(' ')[1];
prefix = `${prefix}
query: ${name}`;
}
if (
!isEmpty(fieldDef) &&
type.typeName === '__mutation_root' &&
type.name
) {
const name = type.name.split(' ')[1];
prefix = `${prefix}
mutation: ${name}`;
}
if (!isEmpty(fieldDef)) result = `${result}\n${fieldDef}\n`;
});
prefix = `${prefix}
}\n`;
if (isEmpty(result)) return '';
return `${prefix} ${result}`;
};
export const addPresetDefinition = (schema: string) => `scalar PresetValue\n
directive @preset(
value: PresetValue
) on INPUT_FIELD_DEFINITION | ARGUMENT_DEFINITION\n
${schema}`;
export const buildSchemaFromRoleDefn = (roleDefinition: string) => {
let permissionsSchema: GraphQLSchema | null = null;
try {
const newDef = addPresetDefinition(roleDefinition);
permissionsSchema = buildSchema(newDef);
} catch (err) {
return null;
}
return permissionsSchema;
};
const addToArrayString = (acc: string, newStr: unknown, withQuotes = false) => {
if (acc !== '') {
if (withQuotes) acc = `${acc}, "${newStr}"`;
else acc = `${acc}, ${newStr}`;
} else acc = `[${newStr}`;
return acc;
};
const parseObjectField = (arg: ArgumentNode | ObjectFieldNode) => {
if (arg?.value?.kind === 'IntValue' && arg?.value?.value)
return arg?.value?.value;
if (arg?.value?.kind === 'FloatValue' && arg?.value?.value)
return arg?.value?.value;
if (arg?.value?.kind === 'StringValue' && arg?.value?.value)
return arg?.value?.value;
if (arg?.value?.kind === 'BooleanValue' && arg?.value?.value)
return arg?.value?.value;
if (arg?.value?.kind === 'EnumValue' && arg?.value?.value)
return arg?.value?.value;
if (arg?.value?.kind === 'NullValue') return null;
// nested values
if (
arg?.value?.kind === 'ObjectValue' &&
arg?.value?.fields &&
arg?.value?.fields?.length > 0
) {
const res: Record<string, any> = {};
arg?.value?.fields.forEach((f: ObjectFieldNode) => {
res[f.name.value] = parseObjectField(f);
});
return res;
}
// Array values
if (
arg?.value?.kind === 'ListValue' &&
arg?.value?.values &&
arg?.value?.values?.length > 0
) {
let res = '';
arg.value.values.forEach((v: ValueNode) => {
if (v.kind === 'IntValue' || v.kind === 'FloatValue') {
res = addToArrayString(res, v.value);
} else if (v.kind === 'BooleanValue') {
res = addToArrayString(res, v.value);
} else if (v.kind === 'StringValue') {
res = addToArrayString(res, v.value);
}
});
return `${res}]`;
}
};
const getDirectives = (field: InputValueDefinitionNode) => {
let res: unknown | Record<string, any>;
const preset = field?.directives?.find(dir => dir?.name?.value === 'preset');
if (preset?.arguments && preset?.arguments[0])
res = parseObjectField(preset.arguments[0]);
if (typeof res === 'object') return res;
if (typeof res === 'string' && isJsonString(res)) return JSON.parse(res);
return res;
};
const getPresets = (field: FieldDefinitionNode) => {
const res: Record<string, any> = {};
field?.arguments?.forEach(arg => {
if (arg.directives && arg.directives.length > 0)
res[arg?.name?.value] = getDirectives(arg);
});
return res;
};
const getFieldsMap = (fields: FieldDefinitionNode[]) => {
const res: Record<string, any> = {};
fields.forEach(field => {
res[field?.name?.value] = getPresets(field);
});
return res;
};
export const getArgTreeFromPermissionSDL = (
definition: string,
introspectionSchema: GraphQLSchema
) => {
const roots = getSchemaRoots(introspectionSchema);
try {
const schema: DocumentNode = parse(definition);
const defs = schema.definitions as ObjectTypeDefinitionNode[];
const argTree =
defs &&
defs.reduce((acc = [], i) => {
if (i.name && i.fields && roots.includes(i?.name?.value)) {
const res = getFieldsMap(i.fields as FieldDefinitionNode[]);
return { ...acc, ...res };
}
return acc;
}, {});
return argTree;
} catch (e) {
console.error(e);
return {};
}
};
export const generateTypeString = (str: string) => str.replace(/[^\w\s]/gi, '');
// Removes [,],! from params, and returns a new string
export const getTrimmedReturnType = (value: string): string => {
const typeName = value.replace(/[[\]!]+/g, '');
return typeName;
};
const getDeps = (field: FieldType, res = new Set<string>([])) => {
if (field.return) res.add(getTrimmedReturnType(field.return));
if (field.args)
Object.values(field.args).forEach(arg => {
if (!(arg.type instanceof GraphQLScalarType)) {
const subType = getTrimmedReturnType(arg.type.inspect());
res.add(subType);
}
});
return res;
};
const addTypesRecursively = (
list: FieldType[],
typeList: Set<string>,
alreadyChecked: Array<string>
): FieldType[] => {
// if "alreadychecked" has then remove from typelist, if not then add to alreadychecked
alreadyChecked.forEach(key => {
if (typeList.has(key)) {
typeList.delete(key);
}
});
typeList.forEach(value => {
alreadyChecked.push(value);
});
// exit condition
// if typelist is empty
if (typeList.size === 0) return list;
const newList = list.map((fld: FieldType) => {
const newField = { ...fld };
if (fld.typeName && typeList.has(fld.typeName)) {
if (newField.children) {
const partiallyChecked = newField.children.find(({ checked }) => {
if (checked) return true;
return false;
});
if (!partiallyChecked)
newField.children = newField.children.map(ch => {
if (ch.return) typeList.add(getTrimmedReturnType(ch.return));
return {
...ch,
checked: true,
};
});
}
}
return newField;
});
return addTypesRecursively(newList, typeList, alreadyChecked);
};
export const addDepFields = (list: FieldType[], field: FieldType) => {
const deps = getDeps(field);
const alreadyChecked: Array<string> = [];
const newList = addTypesRecursively(list, deps, alreadyChecked);
return newList;
};
export const getExpandeItems = (list: FieldType[]) => {
const res: ExpandedItems = {};
list.forEach((item: FieldType, ix) => {
const hasValidChildren = item?.children?.find(i => i.checked === true);
if (!isEmpty(hasValidChildren)) res[ix] = true;
});
return res;
};

View File

@ -8,6 +8,7 @@ import {
addConnector,
editConnector,
viewConnector,
permissionsConnector,
} from '.';
import { FILTER_REMOTE_SCHEMAS } from './Actions';
@ -75,6 +76,10 @@ const getRemoteSchemaRouter = connect => {
path=":remoteSchemaName/modify"
component={editConnector(connect)}
/>
<Route
path=":remoteSchemaName/permissions"
component={permissionsConnector}
/>
</Route>
</Route>
);

View File

@ -3,6 +3,7 @@ import landingConnector from './Landing/RemoteSchema';
import addConnector from './Add/Add';
import editConnector from './Edit/Edit';
import viewConnector from './Edit/View';
import permissionsConnector from './Permissions/index';
import getRemoteSchemaRouter from './RemoteSchemaRouter';
import remoteSchemaReducer from './remoteSchemaReducer';
@ -12,6 +13,7 @@ export {
addConnector,
editConnector,
viewConnector,
permissionsConnector,
getRemoteSchemaRouter,
remoteSchemaReducer,
};

View File

@ -2,11 +2,13 @@ import { combineReducers } from 'redux';
import listReducer from './Actions';
import addReducer from './Add/addRemoteSchemaReducer';
import permissionsReducer from './Permissions/reducer';
import headerReducer from '../../Common/Layout/ReusableHeader/HeaderReducer';
const remoteSchemaReducer = combineReducers({
addData: addReducer,
listData: listReducer,
permissions: permissionsReducer,
headerData: headerReducer('REMOTE_SCHEMA', [
{
name: '',

View File

@ -1,11 +1,13 @@
const asyncState = {
import { AsyncState, ListState, AddState } from './types';
const asyncState: AsyncState = {
isRequesting: false,
isError: false,
isFetching: false,
isFetchError: null,
};
const listState = {
const listState: ListState = {
remoteSchemas: [],
filtered: [],
searchQuery: '',
@ -13,7 +15,7 @@ const listState = {
...asyncState,
};
const addState = {
const addState: AddState = {
manualUrl: '',
envName: null,
headers: [],

View File

@ -0,0 +1,54 @@
import { RemoteSchemaPermissionsState } from './Permissions/state';
import { RemoteSchema } from '../../../metadata/types';
type HeaderState = {
headers: [
{
name: string;
type: string;
value: string;
}
];
};
export type AsyncState = {
isRequesting: boolean;
isError: any;
isFetching: boolean;
isFetchError: any;
};
export type EditState = {
id: number;
isModify: boolean;
originalName: string;
originalHeaders: any[];
originalUrl: string;
originalEnvUrl: string;
originalTimeoutConf: string;
originalForwardClientHeaders: boolean;
};
export type AddState = AsyncState & {
manualUrl: string;
envName: any;
headers: any[];
timeoutConf: string;
name: string;
forwardClientHeaders: boolean;
editState: EditState;
};
export type ListState = AsyncState & {
remoteSchemas: RemoteSchema[];
filtered: any[];
searchQuery: string;
viewRemoteSchema: string;
};
export type RemoteSchemaState = {
addData: AddState;
listData: ListState;
permissions: RemoteSchemaPermissionsState;
headerData: HeaderState;
};

View File

@ -0,0 +1,19 @@
import { useCallback, useEffect, DependencyList } from 'react';
export const useDebouncedEffect = (
effect: (...arg: unknown[]) => void,
delay: number,
deps: DependencyList
) => {
const callback = useCallback(effect, deps);
useEffect(() => {
const handler = setTimeout(() => {
callback();
}, delay);
return () => {
clearTimeout(handler);
};
}, [callback, delay]);
};

View File

@ -21,6 +21,9 @@ export const getDataSourceMetadata = (state: ReduxState) => {
export const getRemoteSchemas = (state: ReduxState) => {
return state.metadata.metadataObject?.remote_schemas ?? [];
};
export const getRemoteSchemaPermissions = (state: ReduxState) => {
return state.remoteSchemas.permissions ?? {};
};
export const getInitDataSource = (
state: ReduxState
@ -90,8 +93,8 @@ const permKeys: Array<keyof PermKeys> = [
'delete_permissions',
];
export const rolesSelector = createSelector(
[getTablesFromAllSources, getActions],
(tables, actions) => {
[getTablesFromAllSources, getActions, getRemoteSchemas],
(tables, actions, remoteSchemas) => {
const roleNames: string[] = [];
tables?.forEach(table =>
permKeys.forEach(key =>
@ -103,6 +106,9 @@ export const rolesSelector = createSelector(
actions?.forEach(action =>
action.permissions?.forEach(p => roleNames.push(p.role))
);
remoteSchemas?.forEach(remoteSchema => {
remoteSchema?.permissions?.forEach(p => roleNames.push(p.role));
});
return Array.from(new Set(roleNames));
}
);

View File

@ -1,4 +1,5 @@
import { Driver } from '../dataSources';
import { PermissionsType } from '../components/Services/RemoteSchema/Permissions/types';
export type DataSource = {
name: string;
@ -552,6 +553,7 @@ export interface RemoteSchema {
definition: RemoteSchemaDef;
/** Comment */
comment?: string;
permissions?: PermissionsType[];
}
/**