mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-14 17:02:49 +03:00
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:
parent
ff3c58f230
commit
66a3d8dab5
@ -33,3 +33,6 @@ export const makeDataAPIOptions = (
|
||||
body,
|
||||
failOnStatusCode: false,
|
||||
});
|
||||
|
||||
export const getRemoteSchemaRoleName = (i: number, roleName: string) =>
|
||||
`test-role-${roleName}-${i}`;
|
||||
|
@ -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();
|
||||
|
||||
// }
|
@ -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
|
||||
|
13
console/package-lock.json
generated
13
console/package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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,
|
||||
|
@ -5,6 +5,9 @@ const tabInfo = {
|
||||
modify: {
|
||||
display_text: 'Modify',
|
||||
},
|
||||
permissions: {
|
||||
display_text: 'Permissions',
|
||||
},
|
||||
};
|
||||
|
||||
export default tabInfo;
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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;
|
@ -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>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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
|
||||
>({});
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
||||
};
|
@ -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>
|
||||
);
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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: '',
|
||||
|
@ -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: [],
|
54
console/src/components/Services/RemoteSchema/types.ts
Normal file
54
console/src/components/Services/RemoteSchema/types.ts
Normal 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;
|
||||
};
|
19
console/src/hooks/useDebounceEffect.ts
Normal file
19
console/src/hooks/useDebounceEffect.ts
Normal 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]);
|
||||
};
|
@ -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));
|
||||
}
|
||||
);
|
||||
|
@ -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[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user