diff --git a/console/cypress/helpers/remoteSchemaHelpers.ts b/console/cypress/helpers/remoteSchemaHelpers.ts index df8981b7af7..7b74759cb1c 100644 --- a/console/cypress/helpers/remoteSchemaHelpers.ts +++ b/console/cypress/helpers/remoteSchemaHelpers.ts @@ -33,3 +33,6 @@ export const makeDataAPIOptions = ( body, failOnStatusCode: false, }); + +export const getRemoteSchemaRoleName = (i: number, roleName: string) => + `test-role-${roleName}-${i}`; diff --git a/console/cypress/integration/remote-schemas/create-remote-schema/spec.ts b/console/cypress/integration/remote-schemas/create-remote-schema/spec.ts index 8005b9d730b..76205a993d6 100644 --- a/console/cypress/integration/remote-schemas/create-remote-schema/spec.ts +++ b/console/cypress/integration/remote-schemas/create-remote-schema/spec.ts @@ -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(); + +// } \ No newline at end of file diff --git a/console/cypress/integration/remote-schemas/create-remote-schema/test.ts b/console/cypress/integration/remote-schemas/create-remote-schema/test.ts index 455e239435f..e92642a885c 100644 --- a/console/cypress/integration/remote-schemas/create-remote-schema/test.ts +++ b/console/cypress/integration/remote-schemas/create-remote-schema/test.ts @@ -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 diff --git a/console/package-lock.json b/console/package-lock.json index 5afd6874ded..42be4ff570c 100644 --- a/console/package-lock.json +++ b/console/package-lock.json @@ -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", diff --git a/console/package.json b/console/package.json index 2ccba5bc246..1a93dfd7899 100644 --- a/console/package.json +++ b/console/package.json @@ -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", diff --git a/console/src/components/Common/Permissions/PermissionStyles.scss b/console/src/components/Common/Permissions/PermissionStyles.scss index e528fcb2174..8a2e4274a04 100644 --- a/console/src/components/Common/Permissions/PermissionStyles.scss +++ b/console/src/components/Common/Permissions/PermissionStyles.scss @@ -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 { diff --git a/console/src/components/Common/utils/jsUtils.tsx b/console/src/components/Common/utils/jsUtils.tsx index c379efe8edd..c27a82ca05b 100644 --- a/console/src/components/Common/utils/jsUtils.tsx +++ b/console/src/components/Common/utils/jsUtils.tsx @@ -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); diff --git a/console/src/components/Services/RemoteSchema/Actions.js b/console/src/components/Services/RemoteSchema/Actions.js index c9c5c1049fa..7308ad3f288 100644 --- a/console/src/components/Services/RemoteSchema/Actions.js +++ b/console/src/components/Services/RemoteSchema/Actions.js @@ -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, }; diff --git a/console/src/components/Services/RemoteSchema/Add/addRemoteSchemaReducer.js b/console/src/components/Services/RemoteSchema/Add/addRemoteSchemaReducer.js index 8e8d8615570..dc1a9fb4e83 100644 --- a/console/src/components/Services/RemoteSchema/Add/addRemoteSchemaReducer.js +++ b/console/src/components/Services/RemoteSchema/Add/addRemoteSchemaReducer.js @@ -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, diff --git a/console/src/components/Services/RemoteSchema/Edit/tabInfo.js b/console/src/components/Services/RemoteSchema/Edit/tabInfo.js index b1cd0952828..9c464294303 100644 --- a/console/src/components/Services/RemoteSchema/Edit/tabInfo.js +++ b/console/src/components/Services/RemoteSchema/Edit/tabInfo.js @@ -5,6 +5,9 @@ const tabInfo = { modify: { display_text: 'Modify', }, + permissions: { + display_text: 'Permissions', + }, }; export default tabInfo; diff --git a/console/src/components/Services/RemoteSchema/Permissions/ArgSelect.tsx b/console/src/components/Services/RemoteSchema/Permissions/ArgSelect.tsx new file mode 100644 index 00000000000..b35250ba48e --- /dev/null +++ b/console/src/components/Services/RemoteSchema/Permissions/ArgSelect.tsx @@ -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) => void; +} + +export const ArgSelect: React.FC = ({ + 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( + value && + ((typeof value === 'string' && value.length > 0) || + typeof value === 'number') + ) + ); + const prevState = useRef>(); + 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) => { + 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 ( + <> + + {!expanded && ( + + )} + {expanded && ( + + )} +
    + {expanded && + Object.values(children).map(i => { + if (typeof value === 'string') return undefined; + const childVal = + value && typeof value === 'object' ? value[i?.name] : undefined; + return ( +
  • + setArgVal({ [k]: val })} + valueField={i} + value={childVal} + level={level + 1} + /> +
  • + ); + })} +
+ + ); + } + return ( +
  • + +
  • + ); +}; diff --git a/console/src/components/Services/RemoteSchema/Permissions/BulkSelect.tsx b/console/src/components/Services/RemoteSchema/Permissions/BulkSelect.tsx new file mode 100644 index 00000000000..b8910da9f7e --- /dev/null +++ b/console/src/components/Services/RemoteSchema/Permissions/BulkSelect.tsx @@ -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 = ({ + bulkSelect, + permRemoveMultipleRoles, +}) => { + const getSelectedRoles = () => { + return bulkSelect.map((role: string) => { + return ( + + {role}{' '} + + ); + }); + }; + + const handleBulkRemoveClick = () => { + const confirmMessage = + 'This will remove all currently set permissions for the selected role(s)'; + const isOk = getConfirmation(confirmMessage); + if (isOk) { + permRemoveMultipleRoles(); + } + }; + + return ( +
    +
    Apply Bulk Actions
    +
    + Selected Roles + {getSelectedRoles()} +
    +
    + +
    +
    + ); +}; + +export default BulkSelect; diff --git a/console/src/components/Services/RemoteSchema/Permissions/CollapsedField.tsx b/console/src/components/Services/RemoteSchema/Permissions/CollapsedField.tsx new file mode 100644 index 00000000000..0527d8db6db --- /dev/null +++ b/console/src/components/Services/RemoteSchema/Permissions/CollapsedField.tsx @@ -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) => void; + onExpand: (e: React.MouseEvent) => void; + expanded: boolean; +} +export const CollapsedField: React.FC = ({ + field: i, + onClick, + onExpand = () => {}, + expanded, +}) => ( + <> + + {i.return && ( + <> + : + + {i.return} + + + )} + +); diff --git a/console/src/components/Services/RemoteSchema/Permissions/Field.tsx b/console/src/components/Services/RemoteSchema/Permissions/Field.tsx new file mode 100644 index 00000000000..da68c90d520 --- /dev/null +++ b/console/src/components/Services/RemoteSchema/Permissions/Field.tsx @@ -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 = ({ + 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>(initState); + const setArg = useCallback( + (vStr: Record) => { + 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) => { + return { ...argTree, [i.name]: fieldVal }; + }); + } + }, [fieldVal]); + + const handleClick = (e: MouseEvent) => { + e.preventDefault(); + const target = e.target as HTMLAnchorElement; + const selectedTypeName = target.id; + + // context from PermissionEditor.tsx + context.scrollToElement(selectedTypeName); + }; + + if (!i.checked) + return ( + + ); + return ( + <> + + {i.name} + + {i.args && ' ('} + {i.args && ( +
      + {i.args && + Object.entries(i.args).map(([k, v]) => ( + + ))} +
    + )} + {i.args && ' )'} + {i.return && ( + + : + + {i.return} + + + )} + + ); +}; diff --git a/console/src/components/Services/RemoteSchema/Permissions/Pen.tsx b/console/src/components/Services/RemoteSchema/Permissions/Pen.tsx new file mode 100644 index 00000000000..f5357b654f1 --- /dev/null +++ b/console/src/components/Services/RemoteSchema/Permissions/Pen.tsx @@ -0,0 +1,23 @@ +import React from 'react'; + +const Pen = () => { + return ( + + + + + ); +}; + +export default Pen; diff --git a/console/src/components/Services/RemoteSchema/Permissions/PermissionEditor.tsx b/console/src/components/Services/RemoteSchema/Permissions/PermissionEditor.tsx new file mode 100644 index 00000000000..98fd65f7250 --- /dev/null +++ b/console/src/components/Services/RemoteSchema/Permissions/PermissionEditor.tsx @@ -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 = props => { + const { + permissionEdit, + isEditing, + isFetching, + schemaDefinition, + permCloseEdit, + saveRemoteSchemaPermission, + removeRemoteSchemaPermission, + setSchemaDefinition, + remoteSchemaFields, + introspectionSchema, + } = props; + + const [state, setState] = useState( + remoteSchemaFields + ); + const [argTree, setArgTree] = useState({}); // 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 ( +
    +
    + + + {/* below helps to debug the SDL */} + {/* {resultString} */} + +
    + + {!(isNewRole || isNewPerm) && ( + + )} + +
    + ); +}; + +export default PermissionEditor; diff --git a/console/src/components/Services/RemoteSchema/Permissions/Permissions.tsx b/console/src/components/Services/RemoteSchema/Permissions/Permissions.tsx new file mode 100644 index 00000000000..a871d2566ec --- /dev/null +++ b/console/src/components/Services/RemoteSchema/Permissions/Permissions.tsx @@ -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 = 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 ( +
    + Error introspecting remote schema.{' '} + + {' '} + Try again{' '} + +
    + ); + } + + return ( +
    + + + {!!bulkSelect.length && ( + + )} +
    + {!readOnlyMode && ( + + )} +
    +
    + ); +}; + +export default Permissions; diff --git a/console/src/components/Services/RemoteSchema/Permissions/PermissionsTable.tsx b/console/src/components/Services/RemoteSchema/Permissions/PermissionsTable.tsx new file mode 100644 index 00000000000..c6b30365035 --- /dev/null +++ b/console/src/components/Services/RemoteSchema/Permissions/PermissionsTable.tsx @@ -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 = ({ + allRoles, + currentRemoteSchema, + permissionEdit, + isEditing, + bulkSelect, + readOnlyMode, + permSetRoleName, + permSetBulkSelect, + setSchemaDefinition, + permOpenEdit, + permCloseEdit, +}) => { + const allPermissions = currentRemoteSchema?.permissions || []; + + const headings = ['Role', ...queryTypes]; + + const dispatchRoleNameChange = (e: ChangeEvent) => { + permSetRoleName(e.target.value?.trim()); + }; + + const getEditIcon = () => { + return ( + +