From 7a25f4a5c4523a09c826205c580a6257c106d11d Mon Sep 17 00:00:00 2001 From: kriks7iitk Date: Wed, 10 Jul 2024 13:48:26 +0530 Subject: [PATCH 1/2] Merge group revamp --- .vscode/settings.json | 3 +- .../cypress/constants/texts/manageGroups.js | 2 +- frontend/assets/translations/en.json | 3 +- .../ee/components/UsersPage/UsersTable.jsx | 81 +- frontend/src/App/App.jsx | 4 +- frontend/src/Editor/Editor.jsx | 7 +- frontend/src/Editor/Viewer.jsx | 14 +- frontend/src/HomePage/HomePage.jsx | 27 +- .../AddEditResourcePermissionsModal.jsx | 145 +++ .../AppPermissionActionContainer.jsx | 67 ++ .../AddResourcePermissionsMenu.jsx | 61 ++ .../AppResourcePermission.jsx | 151 +++ frontend/src/ManageGranularAccess/index.jsx | 535 ++++++++++ .../ChangeRoleModal.jsx | 75 ++ .../constant.js | 70 ++ .../grpPermissionResc.theme.scss | 146 +++ .../index.jsx | 998 ++++++++++++++++++ .../ManageGroupPermissions.jsx | 2 +- .../ErrorModal/ErrorModal.jsx | 55 + .../ManageGroupPermissionsV2.jsx | 788 ++++++++++++++ .../ManageGroupPermissionsV2/ResourceChip.jsx | 103 ++ .../groupPermissions.theme.scss | 458 ++++++++ .../ManageOrgConstants/ManageOrgConstants.jsx | 15 +- .../src/ManageOrgUsers/InviteUsersForm.jsx | 113 +- .../src/ManageOrgUsers/ManageOrgUsers.jsx | 55 +- .../ManageOrgUsers/ManageOrgUsersDrawer.jsx | 30 +- .../src/ManageOrgUsers/UserGroupsSelect.jsx | 42 +- frontend/src/ManageOrgVars/ManageOrgVars.jsx | 5 +- .../src/TooljetDatabase/Forms/styles.scss | 2 +- frontend/src/WorkspaceConstants/index.jsx | 5 +- frontend/src/_components/LogoNavDropdown.jsx | 21 +- frontend/src/_components/MultiSelectUser.jsx | 13 +- .../OrganizationLogin/OrganizationLogin.jsx | 2 +- frontend/src/_components/SearchBox.jsx | 12 + frontend/src/_helpers/utils.js | 6 +- .../src/_services/authentication.service.js | 2 + .../_services/groupPermission.v2.service.js | 190 ++++ frontend/src/_services/index.js | 1 + frontend/src/_styles/groups-permissions.scss | 146 +++ frontend/src/_styles/theme.scss | 154 ++- .../src/_styles/widgets/multi-select.scss | 4 + frontend/src/_ui/AppButton/AppButton.scss | 2 +- .../_ui/Icon/solidIcons/GranularAccess.jsx | 19 + frontend/src/_ui/Icon/solidIcons/UserGear.jsx | 19 + .../src/_ui/Icon/solidIcons/UserGroups.jsx | 39 + frontend/src/_ui/Icon/solidIcons/index.js | 6 + frontend/src/_ui/Layout/index.jsx | 5 +- frontend/src/_ui/Modal/AppsSelect.jsx | 204 ++++ frontend/src/_ui/Modal/appSelect.theme.scss | 228 ++++ frontend/src/_ui/Modal/index.jsx | 32 +- package.json | 6 + server/ee/services/oauth/oauth.service.ts | 11 +- .../1714015513342-AddGroupPermissionsTable.ts | 37 + .../1714015541245-AddGroupUsersTable.ts | 25 + ...14015564318-AddGranularPermissionsTable.ts | 32 + ...4015596201-AddAppsGroupPermissionsTable.ts | 28 + .../1714015615904-AddGroupAppsTable.ts | 23 + ...0-CreateDefaultGroupInExistingWorkspace.ts | 149 +++ ...sersToRespectiveRolesBuilderAndEndUsers.ts | 124 +++ ...737529-MigrateCustomGroupToNewUserGroup.ts | 213 ++++ ...-DropGroupPermissionsOlderRelatedTables.ts | 9 + server/src/app.module.ts | 6 +- server/src/constants/global.constant.ts | 96 ++ server/src/controllers/app.controller.ts | 1 + .../app_environments.controller.ts | 2 + .../app_import_export.controller.ts | 5 +- server/src/controllers/apps.controller.ts | 43 +- server/src/controllers/apps.controller.v2.ts | 39 +- server/src/controllers/comment.controller.ts | 11 +- server/src/controllers/folders.controller.ts | 13 +- .../global_data_sources.controller.ts | 9 +- .../group_permissions.controller.ts | 118 --- .../group_permissions.controller.v2.ts | 214 ++++ .../import_export_resources.controller.ts | 7 +- .../controllers/library_apps.controller.ts | 3 +- .../organization_users.controller.ts | 21 +- .../controllers/organizations.controller.ts | 9 +- server/src/controllers/plugins.controller.ts | 7 +- server/src/controllers/thread.controller.ts | 11 +- server/src/dto/granular-permissions.dto.ts | 58 + server/src/dto/group_permissions.dto.ts | 81 ++ server/src/dto/invite-new-user.dto.ts | 7 +- server/src/entities/app.entity.ts | 8 +- server/src/entities/app_base.entity.ts | 2 + .../entities/apps_group_permissions.entity.ts | 46 + .../entities/granular_permissions.entity.ts | 47 + server/src/entities/group_apps.entity.ts | 38 + .../src/entities/group_permissions.entity.ts | 65 ++ server/src/entities/group_users.entity.ts | 38 + server/src/entities/organization.entity.ts | 7 +- server/src/entities/user.entity.ts | 21 +- .../helpers/db-utility/db-search.helper.ts | 16 + .../db-utility/db-utility.interface.ts | 8 + .../src/helpers/db_constraints.constants.ts | 3 + server/src/helpers/queries.ts | 105 +- server/src/modules/apps/apps.module.ts | 8 +- server/src/modules/auth/auth.module.ts | 10 +- .../casl/abilities/apps-ability.factory.ts | 121 ++- .../abilities/comments-ability.factory.ts | 37 +- .../abilities/data-queries-ability.factory.ts | 54 + .../casl/abilities/folders-ability.factory.ts | 26 +- .../global-datasource-ability.factory.ts | 33 +- ...g-environment-variables-ability.factory.ts | 14 +- .../organization-constants-ability.factory.ts | 28 +- .../casl/abilities/plugins-ability.factory.ts | 22 +- .../casl/abilities/threads-ability.factory.ts | 42 +- .../abilities/tooljet-db-ability.factory.ts | 12 +- .../src/modules/casl/casl-ability.factory.ts | 41 +- server/src/modules/casl/casl.module.ts | 16 +- .../data_queries/data_queries.module.ts | 6 +- .../data_sources/data_sources.module.ts | 6 +- server/src/modules/folders/folders.module.ts | 7 +- .../group_permissions.module.ts | 34 - .../org_environment_variables.module.ts | 6 +- .../organization_constants.module.ts | 6 +- .../organizations/constant/constants.ts | 7 + .../organizations/organizations.module.ts | 10 +- .../constants/permissions-ability.constant.ts | 33 + .../permissions-ability.interface.ts | 48 + .../modules/permissions/permissions.module.ts | 9 + .../utility/permission-ability.utility.ts | 73 ++ server/src/modules/seeds/seeds.module.ts | 2 + .../granular-permissions.constant.ts | 45 + .../constants/group-permissions.constant.ts | 127 +++ .../granular-permissions.interface.ts | 67 ++ .../interface/group-permissions.interface.ts | 43 + .../group-permissions.utility.service.ts | 228 ++++ .../user_resource_permissions.module.ts | 15 + .../utility/granular-permissios.utility.ts | 75 ++ .../utility/group-permissions.utility.ts | 217 ++++ server/src/modules/users/users.module.ts | 7 +- .../src/services/app_import_export.service.ts | 28 - server/src/services/apps.service.ts | 90 +- server/src/services/auth.service.ts | 48 +- server/src/services/folders.service.ts | 184 ++-- .../services/granular_permissions.service.ts | 254 +++++ .../src/services/group_permissions.service.ts | 516 --------- .../services/group_permissions.service.v2.ts | 278 +++++ server/src/services/metadata.service.ts | 26 +- .../services/organization_users.service.ts | 31 +- server/src/services/organizations.service.ts | 151 ++- .../services/permissions-ability.service.ts | 90 ++ server/src/services/seeds.service.ts | 56 +- server/src/services/user-role.service.ts | 148 +++ server/src/services/users.service.ts | 366 ++----- server/tsconfig.json | 4 +- 146 files changed, 8896 insertions(+), 1898 deletions(-) create mode 100644 frontend/src/ManageGranularAccess/AddEditResourceModal/AddEditResourcePermissionsModal.jsx create mode 100644 frontend/src/ManageGranularAccess/AddEditResourceModal/AppPermissionActionContainer.jsx create mode 100644 frontend/src/ManageGranularAccess/AddResourcePermissionsMenu.jsx create mode 100644 frontend/src/ManageGranularAccess/AppResourcePermission.jsx create mode 100644 frontend/src/ManageGranularAccess/index.jsx create mode 100644 frontend/src/ManageGroupPermissionResourcesV2/ChangeRoleModal.jsx create mode 100644 frontend/src/ManageGroupPermissionResourcesV2/constant.js create mode 100644 frontend/src/ManageGroupPermissionResourcesV2/grpPermissionResc.theme.scss create mode 100644 frontend/src/ManageGroupPermissionResourcesV2/index.jsx create mode 100644 frontend/src/ManageGroupPermissionsV2/ErrorModal/ErrorModal.jsx create mode 100644 frontend/src/ManageGroupPermissionsV2/ManageGroupPermissionsV2.jsx create mode 100644 frontend/src/ManageGroupPermissionsV2/ResourceChip.jsx create mode 100644 frontend/src/ManageGroupPermissionsV2/groupPermissions.theme.scss create mode 100644 frontend/src/_services/groupPermission.v2.service.js create mode 100644 frontend/src/_styles/groups-permissions.scss create mode 100644 frontend/src/_ui/Icon/solidIcons/GranularAccess.jsx create mode 100644 frontend/src/_ui/Icon/solidIcons/UserGear.jsx create mode 100644 frontend/src/_ui/Icon/solidIcons/UserGroups.jsx create mode 100644 frontend/src/_ui/Modal/AppsSelect.jsx create mode 100644 frontend/src/_ui/Modal/appSelect.theme.scss create mode 100644 server/migrations/1714015513342-AddGroupPermissionsTable.ts create mode 100644 server/migrations/1714015541245-AddGroupUsersTable.ts create mode 100644 server/migrations/1714015564318-AddGranularPermissionsTable.ts create mode 100644 server/migrations/1714015596201-AddAppsGroupPermissionsTable.ts create mode 100644 server/migrations/1714015615904-AddGroupAppsTable.ts create mode 100644 server/migrations/1720352990850-CreateDefaultGroupInExistingWorkspace.ts create mode 100644 server/migrations/1720365772516-AddingUsersToRespectiveRolesBuilderAndEndUsers.ts create mode 100644 server/migrations/1720434737529-MigrateCustomGroupToNewUserGroup.ts create mode 100644 server/migrations/1720513124281-DropGroupPermissionsOlderRelatedTables.ts create mode 100644 server/src/constants/global.constant.ts delete mode 100644 server/src/controllers/group_permissions.controller.ts create mode 100644 server/src/controllers/group_permissions.controller.v2.ts create mode 100644 server/src/dto/granular-permissions.dto.ts create mode 100644 server/src/dto/group_permissions.dto.ts create mode 100644 server/src/entities/apps_group_permissions.entity.ts create mode 100644 server/src/entities/granular_permissions.entity.ts create mode 100644 server/src/entities/group_apps.entity.ts create mode 100644 server/src/entities/group_permissions.entity.ts create mode 100644 server/src/entities/group_users.entity.ts create mode 100644 server/src/helpers/db-utility/db-search.helper.ts create mode 100644 server/src/helpers/db-utility/db-utility.interface.ts create mode 100644 server/src/modules/casl/abilities/data-queries-ability.factory.ts delete mode 100644 server/src/modules/group_permissions/group_permissions.module.ts create mode 100644 server/src/modules/organizations/constant/constants.ts create mode 100644 server/src/modules/permissions/constants/permissions-ability.constant.ts create mode 100644 server/src/modules/permissions/interface/permissions-ability.interface.ts create mode 100644 server/src/modules/permissions/permissions.module.ts create mode 100644 server/src/modules/permissions/utility/permission-ability.utility.ts create mode 100644 server/src/modules/user_resource_permissions/constants/granular-permissions.constant.ts create mode 100644 server/src/modules/user_resource_permissions/constants/group-permissions.constant.ts create mode 100644 server/src/modules/user_resource_permissions/interface/granular-permissions.interface.ts create mode 100644 server/src/modules/user_resource_permissions/interface/group-permissions.interface.ts create mode 100644 server/src/modules/user_resource_permissions/services/group-permissions.utility.service.ts create mode 100644 server/src/modules/user_resource_permissions/user_resource_permissions.module.ts create mode 100644 server/src/modules/user_resource_permissions/utility/granular-permissios.utility.ts create mode 100644 server/src/modules/user_resource_permissions/utility/group-permissions.utility.ts create mode 100644 server/src/services/granular_permissions.service.ts delete mode 100644 server/src/services/group_permissions.service.ts create mode 100644 server/src/services/group_permissions.service.v2.ts create mode 100644 server/src/services/permissions-ability.service.ts create mode 100644 server/src/services/user-role.service.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 24a4397851..35cab5fefc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -23,5 +23,6 @@ ], "url": "https://raw.githubusercontent.com/ToolJet/ToolJet/develop/plugins/schemas/manifest.schema.json" } - ] + ], + "CodeGPT.apiKey": "CodeGPT Plus Beta" } \ No newline at end of file diff --git a/cypress-tests/cypress/constants/texts/manageGroups.js b/cypress-tests/cypress/constants/texts/manageGroups.js index 2f41ac7648..d608c96cdf 100644 --- a/cypress-tests/cypress/constants/texts/manageGroups.js +++ b/cypress-tests/cypress/constants/texts/manageGroups.js @@ -4,7 +4,7 @@ export const groupsText = { tableHeader: "Name", allUsers: "All users", admin: "Admin", - cardTitle: "Add new group", + cardTitle: "Create new group", cancelButton: "Cancel", createGroupButton: "Create Group", groupNameExistToast: "Group name already exist", diff --git a/frontend/assets/translations/en.json b/frontend/assets/translations/en.json index 4ea8ea391a..2536e1f576 100644 --- a/frontend/assets/translations/en.json +++ b/frontend/assets/translations/en.json @@ -9,6 +9,7 @@ "search": "Search", "update": "Update", "delete": "Delete", + "remove":"Remove", "add": "Add", "view": "View", "create": "Create", @@ -269,7 +270,7 @@ "userGroups": "User Groups", "createNewGroup": "Create new group", "updateGroup": "Update group", - "addNewGroup": "Add new group", + "addNewGroup": "Create new group", "enterName": "Enter group name", "createGroup": "Create Group", "name": "Name" diff --git a/frontend/ee/components/UsersPage/UsersTable.jsx b/frontend/ee/components/UsersPage/UsersTable.jsx index 64fff66932..10ebff63a8 100644 --- a/frontend/ee/components/UsersPage/UsersTable.jsx +++ b/frontend/ee/components/UsersPage/UsersTable.jsx @@ -8,6 +8,7 @@ import SolidIcon from '@/_ui/Icon/SolidIcons'; import { Tooltip } from 'react-tooltip'; import UsersActionMenu from './UsersActionMenu'; import { humanizeifDefaultGroupName, decodeEntities } from '@/_helpers/utils'; +import { ToolTip } from '@/_components/ToolTip'; const UsersTable = ({ isLoading, @@ -34,10 +35,10 @@ const UsersTable = ({ {translator('header.organization.menus.manageUsers.name', 'Name')} - - {translator('header.organization.menus.manageUsers.email', 'Email')} + + User role - Groups + Custom groups {users && users[0]?.status ? ( {translator('header.organization.menus.manageUsers.status', 'Status')} @@ -90,22 +91,20 @@ const UsersTable = ({ user.last_name ? user.last_name[0] : '' }`} /> - - {decodeEntities(user.name)} - +
+ + {decodeEntities(user.name)} + + + {user.email} + +
- - - {user.email} - - - + group.name)} isRole={true} /> + group.name)} /> {user.status && ( { +const GroupChipTD = ({ groups = [], isRole = false }) => { const [showAllGroups, setShowAllGroups] = useState(false); const groupsListRef = useRef(); @@ -213,20 +212,23 @@ const GroupChipTD = ({ groups = [] }) => { return arr; } - const orderedArray = moveValuesToLast(groups, ['all_users', 'admin']); + const orderedArray = groups; const toggleAllGroupsList = (e) => { setShowAllGroups(!showAllGroups); }; const renderGroupChip = (group, index) => ( - - {humanizeifDefaultGroupName(group)} - + + + {humanizeifDefaultGroupName(group)} + + ); return ( { @@ -235,28 +237,31 @@ const GroupChipTD = ({ groups = [] }) => { className={cx('text-muted groups-name-cell', { 'groups-hover': orderedArray.length > 2 })} >
- {orderedArray.slice(0, 2).map((group, index) => { - if (orderedArray.length <= 2) { - return renderGroupChip(group, index); - } + {orderedArray.length === 0 ? ( +
-
+ ) : ( + orderedArray.slice(0, 2).map((group, index) => { + if (orderedArray.length <= 2) { + return renderGroupChip(group, index); + } - if (orderedArray.length > 2) { - if (index === 1) { + if (orderedArray.length > 2 && index === 1) { return ( - <> - - {' '} - +{orderedArray.length - 1} more - + + {renderGroupChip(group, index)} + +{orderedArray.length - 2} more {showAllGroups && ( -
{groups.map((group, index) => renderGroupChip(group, index))}
+
+ {orderedArray.slice(2).map((group, index) => renderGroupChip(group, index))} +
)} - +
); } + return renderGroupChip(group, index); - } - })} + }) + )}
); diff --git a/frontend/src/App/App.jsx b/frontend/src/App/App.jsx index b9652f2279..5b2d0e2b66 100644 --- a/frontend/src/App/App.jsx +++ b/frontend/src/App/App.jsx @@ -36,9 +36,9 @@ import { useAppDataStore } from '@/_stores/appDataStore'; import cx from 'classnames'; import useAppDarkMode from '@/_hooks/useAppDarkMode'; import { ManageOrgUsers } from '@/ManageOrgUsers'; -import { ManageGroupPermissions } from '@/ManageGroupPermissions'; import OrganizationLogin from '@/_components/OrganizationLogin/OrganizationLogin'; import { ManageOrgVars } from '@/ManageOrgVars'; +import { ManageGroupPermissionsV2 } from '@/ManageGroupPermissionsV2/ManageGroupPermissionsV2'; import { setFaviconAndTitle } from '@white-label/whiteLabelling'; const AppWrapper = (props) => { @@ -310,7 +310,7 @@ class AppComponent extends React.Component { path="groups" element={ - + } /> diff --git a/frontend/src/Editor/Editor.jsx b/frontend/src/Editor/Editor.jsx index ae0adffb0e..314c259b69 100644 --- a/frontend/src/Editor/Editor.jsx +++ b/frontend/src/Editor/Editor.jsx @@ -223,12 +223,15 @@ const EditorComponent = (props) => { // Subscribe to changes in the current session using RxJS observable pattern const subscription = authenticationService.currentSession.subscribe((currentSession) => { - if (currentUser && currentSession?.group_permissions) { + if (currentUser && (currentSession?.group_permissions || currentSession?.role)) { const userVars = { email: currentUser.email, firstName: currentUser.first_name, lastName: currentUser.last_name, - groups: currentSession.group_permissions?.map((group) => group.group), + groups: currentSession?.group_permissions + ? ['all_users', ...currentSession.group_permissions.map((group) => group.name)] + : ['all_users'], + role: currentSession?.role?.name, }; const appUserDetails = { diff --git a/frontend/src/Editor/Viewer.jsx b/frontend/src/Editor/Viewer.jsx index 8597127db4..bb2a81b19f 100644 --- a/frontend/src/Editor/Viewer.jsx +++ b/frontend/src/Editor/Viewer.jsx @@ -280,13 +280,16 @@ class ViewerComponent extends React.Component { const currentUser = this.state.currentUser; let userVars = {}; - + const currentSessionValue = authenticationService.currentSessionValue; if (currentUser) { userVars = { email: currentUser.email, firstName: currentUser.first_name, lastName: currentUser.last_name, - groups: authenticationService.currentSessionValue?.group_permissions.map((group) => group.group), + groups: currentSessionValue?.group_permissions + ? ['All Users', ...currentSessionValue.group_permissions.map((group) => group.name)] + : ['All Users'], + role: currentSessionValue?.role?.name, }; } @@ -550,15 +553,18 @@ class ViewerComponent extends React.Component { const versionId = this.props.versionId; if (currentSession?.load_app && slug) { - if (currentSession?.group_permissions) { + if (currentSession?.group_permissions || currentSession?.role) { useAppDataStore.getState().actions.setAppId(appId); const currentUser = currentSession.current_user; + const currentSessionValue = authenticationService.currentSessionValue; const userVars = { email: currentUser.email, firstName: currentUser.first_name, lastName: currentUser.last_name, - groups: currentSession?.group_permissions?.map((group) => group.group), + groups: currentSessionValue?.group_permissions + ? ['All Users', ...currentSessionValue.group_permissions.map((group) => group.name)] + : ['All Users'], }; this.props.setCurrentState({ globals: { diff --git a/frontend/src/HomePage/HomePage.jsx b/frontend/src/HomePage/HomePage.jsx index 945987efd8..e11f5c2ed8 100644 --- a/frontend/src/HomePage/HomePage.jsx +++ b/frontend/src/HomePage/HomePage.jsx @@ -303,23 +303,28 @@ class HomePageComponent extends React.Component { canUserPerform(user, action, app) { const currentSession = authenticationService.currentSessionValue; + const userPermissions = currentSession.user_permissions; + const appPermission = currentSession.app_group_permissions; + const canUpdateApp = + appPermission && (appPermission.is_all_editable || appPermission.editable_apps_id.includes(app?.id)); + const canReadApp = + (appPermission && canUpdateApp) || + appPermission.is_all_viewable || + appPermission.viewable_apps_id.includes(app?.id); let permissionGrant; switch (action) { case 'create': - permissionGrant = this.canAnyGroupPerformAction('app_create', currentSession.group_permissions); + permissionGrant = currentSession.user_permissions.app_create; break; case 'read': + permissionGrant = this.isUserOwnerOfApp(user, app) || canReadApp; + break; case 'update': - permissionGrant = - this.canAnyGroupPerformActionOnApp(action, currentSession.app_group_permissions, app) || - this.isUserOwnerOfApp(user, app); + permissionGrant = canUpdateApp || this.isUserOwnerOfApp(user, app); break; case 'delete': - permissionGrant = - this.canAnyGroupPerformActionOnApp('delete', currentSession.app_group_permissions, app) || - this.canAnyGroupPerformAction('app_delete', currentSession.group_permissions) || - this.isUserOwnerOfApp(user, app); + permissionGrant = currentSession.user_permissions.app_delete || this.isUserOwnerOfApp(user, app); break; default: permissionGrant = false; @@ -363,15 +368,15 @@ class HomePageComponent extends React.Component { }; canCreateFolder = () => { - return this.canAnyGroupPerformAction('folder_create', authenticationService.currentSessionValue?.group_permissions); + return authenticationService.currentSessionValue?.user_permissions?.folder_c_r_u_d; }; canDeleteFolder = () => { - return this.canAnyGroupPerformAction('folder_delete', authenticationService.currentSessionValue?.group_permissions); + return authenticationService.currentSessionValue?.user_permissions?.folder_c_r_u_d; }; canUpdateFolder = () => { - return this.canAnyGroupPerformAction('folder_update', authenticationService.currentSessionValue?.group_permissions); + return authenticationService.currentSessionValue?.user_permissions?.folder_c_r_u_d; }; cancelDeleteAppDialog = () => { diff --git a/frontend/src/ManageGranularAccess/AddEditResourceModal/AddEditResourcePermissionsModal.jsx b/frontend/src/ManageGranularAccess/AddEditResourceModal/AddEditResourcePermissionsModal.jsx new file mode 100644 index 0000000000..3424779e03 --- /dev/null +++ b/frontend/src/ManageGranularAccess/AddEditResourceModal/AddEditResourcePermissionsModal.jsx @@ -0,0 +1,145 @@ +import React from 'react'; +import '../../ManageGroupPermissionsV2/groupPermissions.theme.scss'; +import ModalBase from '@/_ui/Modal'; +import { AppsSelect } from '@/_ui/Modal/AppsSelect'; +import AppPermissionsActions from './AppPermissionActionContainer'; + +function AddEditResourcePermissionsModal({ + handleClose, + handleConfirm, + updateParentState, + resourceType, + currentState, + show, + title, + confirmBtnProps, + disableBuilderLevelUpdate, + selectedApps, + setSelectedApps, + addableApps, + darkMode, +}) { + const isCustom = currentState?.isCustom; + const newPermissionName = currentState?.newPermissionName; + const initialPermissionState = currentState?.initialPermissionState; + const errors = currentState?.errors; + const isAll = currentState?.isAll; + + return ( + +
+ +
+ { + if (e.target.value?.length < 51) + updateParentState(() => ({ + newPermissionName: e.target.value, + })); + }} + /> + {errors['permissionName']} +
+
+
Permission name must be unique and max 50 characters
+
+
+ {/* Till here */} +
+ + { + updateParentState((prevState) => ({ + initialPermissionState: { + ...prevState.initialPermissionState, + canEdit: !prevState.initialPermissionState.canEdit, + canView: prevState.initialPermissionState.canEdit, + ...(prevState.initialPermissionState.canEdit && { hideFromDashboard: false }), + }, + })); + }} + handleClickView={() => { + updateParentState((prevState) => ({ + initialPermissionState: { + ...prevState.initialPermissionState, + canView: !prevState.initialPermissionState.canView, + canEdit: prevState.initialPermissionState.canView, + ...(prevState.initialPermissionState.canEdit && { hideFromDashboard: false }), + }, + })); + }} + handleHideFromDashboard={() => { + updateParentState((prevState) => ({ + initialPermissionState: { + ...initialPermissionState, + hideFromDashboard: !prevState.initialPermissionState.hideFromDashboard, + }, + })); + }} + disableBuilderLevelUpdate={disableBuilderLevelUpdate} + initialPermissionState={initialPermissionState} + /> +
+ +
+ +
+ + + +
+
+
+ ); +} + +export default AddEditResourcePermissionsModal; diff --git a/frontend/src/ManageGranularAccess/AddEditResourceModal/AppPermissionActionContainer.jsx b/frontend/src/ManageGranularAccess/AddEditResourceModal/AppPermissionActionContainer.jsx new file mode 100644 index 0000000000..cc0ad15c1a --- /dev/null +++ b/frontend/src/ManageGranularAccess/AddEditResourceModal/AppPermissionActionContainer.jsx @@ -0,0 +1,67 @@ +import React from 'react'; +import '../../ManageGroupPermissionsV2/groupPermissions.theme.scss'; + +function AppPermissionsActions({ + handleClickEdit, + handleClickView, + handleHideFromDashboard, + disableBuilderLevelUpdate, + initialPermissionState, +}) { + return ( +
+
+ +
+
+ + +
+
+ ); +} + +export default AppPermissionsActions; diff --git a/frontend/src/ManageGranularAccess/AddResourcePermissionsMenu.jsx b/frontend/src/ManageGranularAccess/AddResourcePermissionsMenu.jsx new file mode 100644 index 0000000000..26c750c67c --- /dev/null +++ b/frontend/src/ManageGranularAccess/AddResourcePermissionsMenu.jsx @@ -0,0 +1,61 @@ +import React from 'react'; +import '../ManageGroupPermissionsV2/groupPermissions.theme.scss'; +import { ButtonSolid } from '@/_ui/AppButton/AppButton'; +import { OverlayTrigger } from 'react-bootstrap'; + +function AddResourcePermissionsMenu({ openAddPermissionModal, resourcesOptions, currentGroupPermission }) { + return resourcesOptions.length > 1 ? ( + + { + openAddPermissionModal(); + }} + > + Apps + + + } + > +
+ + Add permission + +
+
+ ) : ( +
+ { + openAddPermissionModal(); + }} + > + Add apps + +
+ ); +} + +export default AddResourcePermissionsMenu; diff --git a/frontend/src/ManageGranularAccess/AppResourcePermission.jsx b/frontend/src/ManageGranularAccess/AppResourcePermission.jsx new file mode 100644 index 0000000000..0c93d0b70c --- /dev/null +++ b/frontend/src/ManageGranularAccess/AppResourcePermission.jsx @@ -0,0 +1,151 @@ +import React, { useState } from 'react'; +import GroupChipTD from '@/ManageGroupPermissionsV2/ResourceChip'; +import '../ManageGroupPermissionsV2/groupPermissions.theme.scss'; +import SolidIcon from '@/_ui/Icon/SolidIcons'; +import { ButtonSolid } from '@/_ui/AppButton/AppButton'; +import { OverlayTrigger, Tooltip } from 'react-bootstrap'; +function AppResourcePermissions({ + updateOnlyGranularPermissions, + permissions, + currentGroupPermission, + openEditPermissionModal, +}) { + const [onHover, setHover] = useState(false); + const [notClickable, setNotClickable] = useState(false); + const isRoleGroup = currentGroupPermission.name == 'admin'; + const disableEditUpdate = currentGroupPermission.name == 'end-user'; + const appsPermissions = permissions.appsGroupPermissions; + let apps = appsPermissions?.groupApps?.map((app) => { + return app?.app?.name; + }); + if (apps.length == 0 || permissions.isAll) apps = ['All apps']; + + return ( +
{ + setHover(true); + }} + onMouseOut={() => { + setHover(false); + }} + onClick={() => { + !isRoleGroup && !notClickable && openEditPermissionModal(permissions); + }} + > +
+ +
{permissions.name}
+
+
+
+ + + +
+
+
+ +
+
+ { + openEditPermissionModal(permissions); + }} + disabled={isRoleGroup} + /> +
+
+ ); +} + +export default AppResourcePermissions; diff --git a/frontend/src/ManageGranularAccess/index.jsx b/frontend/src/ManageGranularAccess/index.jsx new file mode 100644 index 0000000000..18b6b559e1 --- /dev/null +++ b/frontend/src/ManageGranularAccess/index.jsx @@ -0,0 +1,535 @@ +import { ButtonSolid } from '@/_ui/AppButton/AppButton'; +import SolidIcon from '@/_ui/Icon/SolidIcons'; +import React from 'react'; +import { OverlayTrigger, Tooltip } from 'react-bootstrap'; +import { withTranslation } from 'react-i18next'; +import { groupPermissionV2Service } from '@/_services'; +import { toast } from 'react-hot-toast'; +import '../ManageGroupPermissionsV2/groupPermissions.theme.scss'; +import ChangeRoleModal from '@/ManageGroupPermissionResourcesV2/ChangeRoleModal'; +import AppResourcePermissions from '@/ManageGranularAccess/AppResourcePermission'; +import AddResourcePermissionsMenu from '@/ManageGranularAccess/AddResourcePermissionsMenu'; +import { ConfirmDialog } from '@/_components'; +import AddEditResourcePermissionsModal from '@/ManageGranularAccess/AddEditResourceModal/AddEditResourcePermissionsModal'; + +import { ToolTip } from '@/_components/ToolTip'; +class ManageGranularAccessComponent extends React.Component { + constructor(props) { + super(props); + + this.state = { + isLoading: false, + granularPermissions: [], + showAddPermissionModal: false, + errors: {}, + values: {}, + customSelected: true, + selectedApps: [], + type: null, + newPermissionName: null, + initialPermissionState: { + canEdit: false, + canView: false, + hideFromDashboard: false, + }, + currentEditingPermissions: null, + isAll: true, + isCustom: false, + addableApps: [], + modalType: 'add', + modalTitle: 'Add app permissions', + showAutoRoleChangeModal: false, + autoRoleChangeModalMessage: '', + autoRoleChangeModalList: [], + autoRoleChangeMessageType: '', + updateParam: {}, + updatingPermission: {}, + updateType: '', + deleteConfirmationModal: false, + deletingPermissions: false, + }; + } + + componentDidMount() { + this.fetchAppsCanBeAdded(); + this.fetchGranularPermissions(this.props.groupPermissionId); + } + + fetchAppsCanBeAdded = () => { + groupPermissionV2Service + .fetchAddableApps() + .then((data) => { + const addableApps = data.map((app) => { + return { + name: app.name, + value: app.id, + label: app.name, + }; + }); + this.setState({ + addableApps, + }); + }) + .catch((err) => { + toast.error(err.error); + }); + }; + + fetchGranularPermissions = (groupPermissionId) => { + this.setState({ + isLoading: true, + }); + groupPermissionV2Service.fetchGranularPermissions(groupPermissionId).then((data) => { + this.setState({ + granularPermissions: data, + isLoading: false, + }); + }); + }; + + deleteGranularPermissions = () => { + const { currentEditingPermissions } = this.state; + this.setState({ + deleteGranularPermissions: true, + }); + groupPermissionV2Service + .deleteGranularPermission(currentEditingPermissions.id) + .then(() => { + toast.success('Deleted permission successfully'); + this.fetchGranularPermissions(this.props.groupPermissionId); + this.closeAddPermissionModal(); + }) + .catch((err) => { + toast.error(err.error); + }) + .finally(() => { + this.setState({ + deleteConfirmationModal: false, + deleteGranularPermissions: false, + }); + }); + }; + + createGranularPermissions = () => { + const { initialPermissionState, isAll, newPermissionName, isCustom, selectedApps } = this.state; + if (isCustom && selectedApps.length == 0) { + toast.error('Please select the apps'); + return; + } + const body = { + name: newPermissionName, + type: 'app', + groupId: this.props.groupPermissionId, + isAll: isAll, + createAppsPermissionsObject: { + ...initialPermissionState, + resourcesToAdd: selectedApps.filter((apps) => !apps?.isAllField)?.map((option) => ({ appId: option.value })), + }, + }; + groupPermissionV2Service + .createGranularPermission(body) + .then(() => { + this.fetchGranularPermissions(this.props.groupPermissionId); + this.closeAddPermissionModal(); + }) + .catch(({ error }) => { + this.closeAddPermissionModal(); + if (error?.error) { + this.props.updateParentState({ + showEditRoleErrorModal: true, + errorTitle: error?.title ? error?.title : 'Cannot add granular permissions', + errorMessage: error.error, + errorIconName: 'usergear', + errorListItems: error.data, + }); + } + toast.error(error); + }); + // .then(()) + }; + + openEditPermissionModal = (granularPermission) => { + const currentApps = granularPermission?.appsGroupPermissions?.groupApps; + const appsGroupPermission = granularPermission?.appsGroupPermissions; + this.setState({ + currentEditingPermissions: granularPermission, + modalTitle: 'Edit app permissions', + showAddPermissionModal: true, + modalType: 'edit', + isAll: !!granularPermission.isAll, + isCustom: !granularPermission.isAll, + newPermissionName: granularPermission.name, + initialPermissionState: { + canEdit: appsGroupPermission.canEdit, + canView: appsGroupPermission.canView, + hideFromDashboard: appsGroupPermission.hideFromDashboard, + }, + + selectedApps: + currentApps?.length > 0 + ? currentApps?.map(({ app }) => { + return { + name: app.name, + value: app.id, + label: app.name, + }; + }) + : [], + }); + }; + + updateOnlyGranularPermissions = (permission, actions = {}, allowRoleChange) => { + const body = { + actions: actions, + allowRoleChange, + }; + groupPermissionV2Service + .updateGranularPermission(permission.id, body) + .then(() => { + this.fetchGranularPermissions(this.props.groupPermissionId); + this.closeAddPermissionModal(); + toast.success('Permission updated successfully'); + }) + .catch(({ error }) => { + if (error?.type) { + this.setState({ + showAutoRoleChangeModal: true, + autoRoleChangeModalMessage: error?.error, + autoRoleChangeModalList: error?.data, + autoRoleChangeMessageType: error?.type, + updateParam: actions, + updatingPermission: permission, + updateType: 'ONLY_PERMISSIONS', + }); + return; + } + this.props.updateParentState({ + showEditRoleErrorModal: true, + errorTitle: error?.title ? error?.title : 'Cannot remove last admin', + errorMessage: error.error, + errorIconName: 'usergear', + errorListItems: error.data, + }); + }); + }; + + updateGranularPermissions = (allowRoleChange) => { + const { currentEditingPermissions, selectedApps, newPermissionName, isAll, initialPermissionState } = this.state; + const currentResource = currentEditingPermissions?.appsGroupPermissions?.groupApps?.map((app) => { + return app.app.id; + }); + const selectedResource = selectedApps.filter((apps) => !apps?.isAllField)?.map((resource) => resource.value); + const resourcesToAdd = selectedResource + ?.filter((item) => !currentResource.includes(item)) + .map((id) => { + return { + appId: id, + }; + }); + const appsToDelete = currentResource?.filter((item) => !selectedResource?.includes(item)); + const groupAppsToDelete = currentEditingPermissions?.appsGroupPermissions?.groupApps?.filter((groupApp) => + appsToDelete?.includes(groupApp.appId) + ); + const resourcesToDelete = groupAppsToDelete?.map(({ id }) => { + return { + id: id, + }; + }); + const body = { + name: newPermissionName, + isAll: isAll, + actions: initialPermissionState, + resourcesToAdd, + resourcesToDelete, + allowRoleChange, + }; + + groupPermissionV2Service + .updateGranularPermission(currentEditingPermissions.id, body) + .then(() => { + this.fetchGranularPermissions(this.props.groupPermissionId); + this.closeAddPermissionModal(); + toast.success('Permission updated successfully'); + }) + .catch(({ error }) => { + if (error?.type) { + this.setState({ + showEditRoleErrorModal: false, + showAutoRoleChangeModal: true, + autoRoleChangeModalMessage: error?.error, + autoRoleChangeModalList: error?.data, + autoRoleChangeMessageType: error?.type, + updateType: '', + showAddPermissionModal: false, + }); + return; + } + toast.error(error.error); + this.closeAddPermissionModal(); + }); + }; + showPermissionText = (groupPermission) => { + const text = + groupPermission.name === 'admin' + ? 'Admin has edit access to all apps. These are not editable' + : 'End-user can only have permission to view apps'; + return ( +
+

+ {text} + + read documentation + {' '} + to know more +

+
+ ); + }; + + openAddPermissionModal = () => { + this.setState((prevState) => ({ + showAddPermissionModal: true, + initialPermissionState: { ...prevState.initialPermissionState, canView: true }, + isAll: true, + })); + }; + + closeAddPermissionModal = () => { + this.setState({ + currentEditingPermissions: null, + modalTitle: 'Add app permissions', + showAddPermissionModal: false, + modalType: 'add', + isAll: false, + isCustom: false, + newPermissionName: '', + initialPermissionState: { + canEdit: false, + canView: false, + hideFromDashboard: false, + }, + selectedApps: [], + }); + }; + + setSelectedApps = (values) => { + this.setState({ selectedApps: values }); + }; + + handleAutoRoleChangeModalClose = () => { + this.setState({ + showAutoRoleChangeModal: false, + autoRoleChangeModalMessage: '', + autoRoleChangeModalList: [], + autoRoleChangeMessageType: '', + updateParam: {}, + isLoading: false, + updatingPermission: {}, + updateType: '', + }); + }; + handleConfirmAutoRoleChangeGroupUpdate = () => { + this.updateGranularPermissions(true); + this.handleAutoRoleChangeModalClose(); + }; + + updateState = (stateUpdater) => { + this.setState((prevState) => stateUpdater(prevState)); + }; + + handleConfirmAutoRoleChangeOnlyGroupUpdate = () => { + const { updateParam, updatingPermission } = this.state; + this.updateOnlyGranularPermissions(updatingPermission, updateParam, true); + this.handleAutoRoleChangeModalClose(); + }; + + render() { + const { + isEmpty, + showAddPermissionModal, + errors, + selectedApps, + initialPermissionState, + isAll, + isCustom, + granularPermissions, + isLoading, + addableApps, + modalTitle, + modalType, + newPermissionName, + showAutoRoleChangeModal, + autoRoleChangeModalMessage, + autoRoleChangeModalList, + autoRoleChangeMessageType, + updateParam, + updatingPermission, + updateType, + deleteConfirmationModal, + deletingPermissions, + } = this.state; + + const resourcesOptions = ['Apps']; + const currentGroupPermission = this.props?.groupPermission; + const isRoleGroup = currentGroupPermission.name == 'admin'; + const defaultGroup = currentGroupPermission.type === 'default'; + const showPermissionInfo = currentGroupPermission.name == 'admin' || currentGroupPermission.name == 'end-user'; + const disableEditUpdate = currentGroupPermission.name == 'end-user'; + const addPermissionTooltipMessage = !newPermissionName + ? 'Please input permissions name' + : isCustom && selectedApps.length === 0 + ? 'Please select apps or select all apps option' + : ''; + return ( +
+ this.deleteGranularPermissions()} + onCancel={() => { + this.setState({ deleteConfirmationModal: false, deletingPermissions: false }); + }} + darkMode={this.props.darkMode} + /> + + { + this.updateGranularPermissions(); + } + } + updateParentState={this.updateState} + resourceType="app" + currentState={this.state} + show={showAddPermissionModal} + title={ +
+ + + +
+ {modalTitle} +
+ {modalType === 'edit' && !isRoleGroup && ( +
+ { + this.setState({ + deleteConfirmationModal: true, + showAddPermissionModal: false, + }); + }} + /> +
+ )} +
+ } + confirmBtnProps={{ + title: `${modalType === 'edit' ? 'Update' : 'Add'}`, + iconLeft: 'plus', + disabled: (modalType === 'add' && !newPermissionName) || (isCustom && selectedApps.length === 0), + tooltipMessage: addPermissionTooltipMessage, + }} + disableBuilderLevelUpdate={disableEditUpdate} + selectedApps={selectedApps} + setSelectedApps={this.setSelectedApps} + addableApps={addableApps} + darkMode={this.props.darkMode} + /> + {!granularPermissions.length ? ( +
+
+ +
+

No permissions added yet

+

+ Add assets to configure granular, asset-level permissions for this user group +

+ +
+ ) : ( + <> + {showPermissionInfo && this.showPermissionText(currentGroupPermission)} +
+

+ {'Name'} +

+

+ {'Permission'} +

+

+ {'Resource'} +

+
+
+ {isLoading ? ( + + +
+
+
+ + +
+ + +
+ + + ) : ( + <> + {granularPermissions.map((permissions, index) => ( + + ))} + + )} +
+
+ +
+ + )} +
+ ); + } +} + +export const ManageGranularAccess = withTranslation()(ManageGranularAccessComponent); diff --git a/frontend/src/ManageGroupPermissionResourcesV2/ChangeRoleModal.jsx b/frontend/src/ManageGroupPermissionResourcesV2/ChangeRoleModal.jsx new file mode 100644 index 0000000000..29da3bf6e7 --- /dev/null +++ b/frontend/src/ManageGroupPermissionResourcesV2/ChangeRoleModal.jsx @@ -0,0 +1,75 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import ModalBase from '@/_ui/Modal'; +import '../ManageGroupPermissionsV2/groupPermissions.theme.scss'; + +function ChangeRoleModal({ + showAutoRoleChangeModal, + autoRoleChangeModalList, + autoRoleChangeMessageType, + handleAutoRoleChangeModalClose, + handleConfirmation, + darkMode, + isLoading, +}) { + const { t } = useTranslation(); + + const renderUserChangeMessage = (type) => { + const changePermissionMessage = ( +

+ Granting this permission to the user group will result in a role change for the following user(s) from{' '} + end-users to builders. Are you sure you want to continue? +

+ ); + const addUserMessage = ( +

+ Adding the following user(s) to this group will change their default group from end-users to{' '} + builders. Are you sure you want to continue? +

+ ); + const message = type === 'USER_ROLE_CHANGE_ADD_USERS' ? addUserMessage : changePermissionMessage; + return message; + }; + + const renderUserChangeTitle = (type) => { + const addUserTitle = ( +
+ Add user(s) +
+ ); + const updatePermissionTitile = ( +
+ Change in user role +
+ ); + const message = type === 'USER_ROLE_CHANGE_ADD_USERS' ? addUserTitle : updatePermissionTitile; + return message; + }; + + return ( + + <> + {renderUserChangeMessage(autoRoleChangeMessageType)} +

+
+ {autoRoleChangeModalList.map((item, index) => ( +
+ {`${index + 1}. ${item}`} +
+ ))} +
+ +
+ ); +} + +export default ChangeRoleModal; diff --git a/frontend/src/ManageGroupPermissionResourcesV2/constant.js b/frontend/src/ManageGroupPermissionResourcesV2/constant.js new file mode 100644 index 0000000000..67f43536bf --- /dev/null +++ b/frontend/src/ManageGroupPermissionResourcesV2/constant.js @@ -0,0 +1,70 @@ +import React from 'react'; + +export const EDIT_ROLE_MESSAGE = { + admin: { + builder: () => { + return ( +
+

+ Changing your user group from admin to builder will revoke your access to settings. +

+

Are you sure you want to continue?

+
+ ); + }, + 'end-user': (isPaidPlan) => { + return ( +
+

+ Changing your user group from admin to end-user will revoke your access to settings. + {isPaidPlan && 'This will also affect the count of users covered by your plan.'} +

+

Are you sure you want to continue?

+
+ ); + }, + }, + builder: { + 'end-user': (isPaidPlan) => { + return ( +
+ {isPaidPlan && ( +

+ Changing user default group from builder to end-user will affect the count of users covered by your plan. +

+ )} +

+ This will also remove the user from any custom groups with builder-like permissions. +

+

Are you sure you want to continue?

+
+ ); + }, + }, + 'end-user': { + builder: (isPaidPlan) => { + return ( +
+ {isPaidPlan && ( +

+ Changing user default group from end-user to builder will affect the count of users covered by your plan. +

+ )} +

Are you sure you want to continue?

+
+ ); + }, + admin: (isPaidPlan) => { + return ( +
+ {isPaidPlan && ( +

+ Changing user default group from end-user to admin will affect the count of users covered by your plan. +

+ )} +

Are you sure you want to continue?

+
+ ); + }, + }, +}; diff --git a/frontend/src/ManageGroupPermissionResourcesV2/grpPermissionResc.theme.scss b/frontend/src/ManageGroupPermissionResourcesV2/grpPermissionResc.theme.scss new file mode 100644 index 0000000000..581cc3eced --- /dev/null +++ b/frontend/src/ManageGroupPermissionResourcesV2/grpPermissionResc.theme.scss @@ -0,0 +1,146 @@ +@import '../_styles/colors.scss'; + + +.check-label-disable{ + color: var(--slate10) !important; +} + +.info-container { + display: flex; + width: auto; + height: auto; + padding: 10px 12px 8px 12px; + border: 1px solid var(--slate5); + background: var(--slate2); + border-radius: 6px 6px 6px 6px; + margin-top: 13px; + + + .info-btn { + padding: 6px 0px 0px 0px; + margin-right: 5px; + flex: 0 0 24px; + } + + .message { + margin-left: 5px; + display: inline-block; + word-wrap: break-word; + width: auto; + height: auto; + font-size: 12px; + line-height: 13px; + color: var(--slate11); + + p{ + padding: 0; + margin: 0; + } + + .open-git-btn { + margin-top: 5px; + color: var(--indigo9); + font-size: 10px; + font-weight: 500; + display: inline-block; + + .open-icn { + margin-right: 2px; + } + } + } +} + +.search-user-group-btn { + width: 20px; + margin-left: 2px; + margin-right: 7px; + height: 20px; + padding: 0 0; + background: none !important; + background-color: none !important; + box-shadow: none; + +} + + +.searchbox-custom{ + .tj-common-search-input-user { + width: 600px; + .input-icon-addon { + padding-right: 8px; + padding-left: 8px; + + } + + input { + box-sizing: border-box; + display: flex; + flex-direction: row; + align-items: center; + padding: 4px 8px !important; + gap: 16px; + width: 600px !important; + height: 28px !important; + background: var(--base); + border: 1px solid var(--slate7); + border-radius: 6px; + color: var(--slate12); + padding-left: 33px !important; + + + ::placeholder { + color: var(--slate9); + margin-left: 5px !important; + padding-left: 5px !important; + background-color: red !important; + } + + &:hover { + background: var(--slate2); + border: 1px solid var(--slate8); + } + + &:active { + background: var(--indigo2); + border: 2px solid var(--indigo11); + box-shadow: 0px 0px 0px 2px #C6D4F9; + outline: none; + } + + &:focus-visible { + background: var(--slate2); + border: 1px solid var(--slate8); + border-radius: 6px; + outline: none; + padding-left: 12px !important; + } + + &:disabled { + background: var(--slate3); + border: 1px solid var(--slate7); + } + } + } + +} + + +.edit-role-confirm { + width: 350px; + + .modal-footer { + border-top: 1px solid var(--slate6);; + } + + .form-label{ + color: var(--slate11); + } +} + + +.permission-body { + .tj-text-xxsm{ + color: var(--slate11) + } +} \ No newline at end of file diff --git a/frontend/src/ManageGroupPermissionResourcesV2/index.jsx b/frontend/src/ManageGroupPermissionResourcesV2/index.jsx new file mode 100644 index 0000000000..f996216af8 --- /dev/null +++ b/frontend/src/ManageGroupPermissionResourcesV2/index.jsx @@ -0,0 +1,998 @@ +import React from 'react'; +import cx from 'classnames'; +import { groupPermissionV2Service, authenticationService } from '@/_services'; +import { toast } from 'react-hot-toast'; +import { Link } from 'react-router-dom'; +import { withTranslation } from 'react-i18next'; +import ErrorBoundary from '@/Editor/ErrorBoundary'; +import { Loader } from '../ManageSSO/Loader'; +import SolidIcon from '@/_ui/Icon/solidIcons/index'; +import BulkIcon from '@/_ui/Icon/bulkIcons/index'; +import { FilterPreview, MultiSelectUser } from '@/_components'; + +import { ButtonSolid } from '@/_ui/AppButton/AppButton'; +import ModalBase from '@/_ui/Modal'; +import Select from '@/_ui/Select'; +import { ManageGranularAccess } from '@/ManageGranularAccess'; +import './grpPermissionResc.theme.scss'; +import { EDIT_ROLE_MESSAGE } from './constant'; +import { SearchBox } from '@/_components/SearchBox'; +import EditRoleErrorModal from '@/ManageGroupPermissionsV2/ErrorModal/ErrorModal'; +import ChangeRoleModal from '@/ManageGroupPermissionResourcesV2/ChangeRoleModal'; +import { ToolTip } from '@/_components/ToolTip'; +class ManageGroupPermissionResourcesComponent extends React.Component { + constructor(props) { + super(props); + + this.state = { + isLoadingGroup: true, + isLoadingApps: true, + isAddingApps: false, + isLoadingUsers: true, + isAddingUsers: false, + groupPermission: null, + usersInGroup: [], + appsInGroup: [], + addableApps: [], + usersNotInGroup: [], + appsNotInGroup: [], + selectedAppIds: [], + removeAppIds: [], + currentTab: 'users', + selectedUsers: [], + isChangeRoleModalOpen: false, + updatingUserRole: null, + isAddUsersToRoleModalOpen: false, + isRoleGroup: false, + selectedNewRole: '', + showRoleEditMessage: false, + showUserSearchBox: false, + errorListItems: [], + errorMessage: '', + errorTitle: '', + showEditRoleErrorModal: false, + errorIconName: '', + showAutoRoleChangeModal: false, + autoRoleChangeModalMessage: '', + autoRoleChangeModalList: [], + autoRoleChangeMessageType: '', + updateParam: {}, + }; + } + + componentDidMount() { + if (this.props.groupPermissionId) this.fetchGroupAndResources(this.props.groupPermissionId); + } + + componentDidUpdate(prevProps) { + if (this.props.groupPermissionId && this.props.groupPermissionId !== prevProps.groupPermissionId) { + this.fetchGroupAndResources(this.props.groupPermissionId); + this.setState({ + showUserSearchBox: false, + }); + } + } + + fetchGroupPermission = (groupPermissionId) => { + groupPermissionV2Service.getGroup(groupPermissionId).then(({ group, isBuilderLevel }) => { + this.setState((prevState) => { + return { + isRoleGroup: group.type === 'default', + groupPermission: group, + currentTab: prevState.currentTab, + isLoadingGroup: false, + isBuilderLevel: isBuilderLevel, + }; + }); + }); + }; + + fetchGroupAndResources = (groupPermissionId) => { + this.setState({ isLoadingGroup: true }); + this.fetchGroupPermission(groupPermissionId); + this.fetchUsersInGroup(groupPermissionId); + }; + + userFullName = (user) => { + return `${user?.first_name} ${user?.last_name ?? ''}`; + }; + + searchUsersNotInGroup = async (query, groupPermissionId) => { + return new Promise((resolve, reject) => { + groupPermissionV2Service + .getUsersNotInGroup(query, groupPermissionId) + .then((users) => { + resolve( + users.map((user) => { + return { + name: `${this.userFullName(user)} (${user.email})`, + value: user.id, + first_name: user.firstName, + last_name: user.lastName, + email: user.email, + role: user?.userGroups?.group?.name, + }; + }) + ); + }) + .catch(reject); + }); + }; + + fetchUsersInGroup = (groupPermissionId, searchString = '') => { + groupPermissionV2Service.getUsersInGroup(groupPermissionId, searchString).then((data) => { + this.setState({ + usersInGroup: data, + isLoadingUsers: false, + }); + }); + }; + + clearErrorState = () => { + this.setState({ + errorMessage: '', + showEditRoleErrorModal: false, + errorListItems: [], + errorTitle: '', + errorIconName: '', + selectedUsers: [], + isLoadingUsers: false, + isAddingUsers: false, + }); + }; + + updateGroupPermission = (groupPermissionId, params, allowRoleChange) => { + const currentSession = authenticationService.currentSessionValue; + groupPermissionV2Service + .update(groupPermissionId, { ...params, allowRoleChange }) + .then(() => { + toast.success('Group permissions updated'); + this.fetchGroupPermission(groupPermissionId); + }) + .catch(({ error }) => { + if (error?.type) { + this.setState({ + showAutoRoleChangeModal: true, + autoRoleChangeModalMessage: error?.error, + autoRoleChangeModalList: error?.data, + autoRoleChangeMessageType: error?.type, + }); + return; + } + this.setState({ + errorMessage: error?.error, + showEditRoleErrorModal: true, + errorListItems: error?.data, + errorTitle: error?.title ? error?.title : 'Cannot add this permission to the group', + errorIconName: 'lock', + }); + }); + }; + + setSelectedUsers = (value) => { + this.setState({ + selectedUsers: value, + }); + }; + + setSelectedApps = (value) => { + this.setState({ + selectedAppIds: value, + }); + }; + + addSelectedUsersToGroup = (groupPermissionId, selectedUsers, allowRoleChange) => { + this.setState({ isAddingUsers: true }); + const body = { + userIds: selectedUsers.map((user) => user.value), + groupId: groupPermissionId, + allowRoleChange, + }; + groupPermissionV2Service + .addUsersInGroups(body) + .then(() => { + this.setState({ + selectedUsers: [], + isLoadingUsers: true, + isAddingUsers: false, + }); + toast.success('Users added to the group'); + this.fetchUsersInGroup(groupPermissionId); + }) + .catch(({ error }) => { + if (error?.type) { + this.setState({ + isLoadingUsers: false, + showAutoRoleChangeModal: true, + autoRoleChangeModalMessage: error?.error, + autoRoleChangeModalList: error?.data, + autoRoleChangeMessageType: error?.type, + }); + return; + } + this.setState({ + showEditRoleErrorModal: true, + errorTitle: error?.title, + errorMessage: error?.error, + errorIconName: 'usergear', + isAddingUsers: false, + }); + }); + }; + + removeUserFromGroup = (groupUserId) => { + const { groupPermission } = this.state; + groupPermissionV2Service + .deleteUserFromGroup(groupUserId) + .then(() => { + this.setState({ removeUserIds: [], isLoadingUsers: true }); + this.fetchUsersInGroup(groupPermission.id); + }) + .then(() => { + toast.success('User removed from the group'); + }) + .catch(({ error }) => { + toast.error(error); + }); + }; + + showPermissionText = () => { + const { groupPermission } = this.state; + const text = + groupPermission.name === 'admin' + ? 'Admin has edit access to all apps. These are not editable' + : 'End-user can only have permission to view apps'; + return ( +
+

+ {text} + + read documentation + {' '} + to know more +

+
+ ); + }; + + removeSelection = (selected, value) => { + const updatedData = selected.filter((d) => d.value !== value); + this.setSelectedUsers([...updatedData]); + }; + + setErrorState = (state = {}) => { + this.setState({ + ...state, + }); + }; + + updateUserRole = () => { + const { updatingUserRole, groupPermission, selectedNewRole } = this.state; + const currentSession = authenticationService.currentSessionValue; + const currentUser = currentSession?.current_user; + this.setState({ + isLoadingUsers: true, + }); + const body = { + newRole: selectedNewRole, + userId: updatingUserRole.id, + }; + groupPermissionV2Service + .updateUserRole(body) + .then(() => { + this.fetchUsersInGroup(groupPermission.id); + toast.success('Role updated successfully'); + if (groupPermission?.name === 'admin') window.location.reload(); + if (currentUser.id === updatingUserRole.id) window.location.reload(true); + }) + .catch(({ error }) => { + this.setState({ + showEditRoleErrorModal: true, + errorTitle: error?.title ? error?.title : 'Cannot remove last admin', + errorMessage: error.error, + errorIconName: 'usergear', + errorListItems: error.data, + }); + }) + .finally(() => { + this.closeChangeRoleModal(); + }); + }; + closeChangeRoleModal = () => + this.setState({ + isChangeRoleModalOpen: false, + showRoleEditMessage: false, + updatingUserRole: null, + selectedNewRole: null, + isLoadingUsers: false, + }); + + changeThisComponentState = (state = {}) => { + this.setState(state); + }; + + generateSelection = (selected) => { + return selected?.map((d) => { + return ( +
+ this.removeSelection(selected, d.value)} /> +
+ ); + }); + }; + + openChangeRoleModal = (updatingUser) => + this.setState({ isChangeRoleModalOpen: true, updatingUserRole: updatingUser }); + + showChangeRoleModalMessage = () => { + console.log('called'); + this.setState({ showRoleEditMessage: true }); + }; + + handleUserSearchInGroup = (e) => { + this.fetchUsersInGroup(this.props.groupPermissionId, e?.target?.value); + }; + + toggleUserTabSearchBox = () => { + this.fetchUsersInGroup(this.props.groupPermissionId); + this.setState((prevState) => ({ + showUserSearchBox: !prevState.showUserSearchBox, + })); + }; + + toggleAutoRoleChangeModal = () => { + this.setState((prevState) => ({ + showAutoRoleChangeModal: !prevState.showAutoRoleChangeModal, + })); + }; + handleAutoRoleChangeModalClose = () => { + this.setState({ + showAutoRoleChangeModal: false, + autoRoleChangeModalMessage: '', + autoRoleChangeModalList: [], + autoRoleChangeMessageType: '', + updateParam: {}, + isLoadingGroup: false, + isLoadingUsers: false, + isAddingUsers: false, + }); + }; + + renderUserChangeMessage = (type) => { + const changePermissionMessage = ( +

+ Granting this permission to the user group will result in a role change for the following user(s) from{' '} + end-users to builders. Are you sure you want to continue? +

+ ); + const addUserMessage = ( +

+ Adding the following user(s) to this group will change their default group from end-users to{' '} + builders. Are you sure you want to continue? +

+ ); + const message = type === 'USER_ROLE_CHANGE_ADD_USERS' ? addUserMessage : changePermissionMessage; + return message; + }; + + toggleAddUsersToRoleModal = () => this.setState({ isAddUsersToRoleModalOpen: !this.state.isAddUsersToRoleModalOpen }); + + handleConfirmAutoRoleChangeGroupUpdate = () => { + const { updateParam, groupPermission } = this.state; + this.updateGroupPermission(groupPermission.id, updateParam, true); + this.setState({ + updateParam: {}, + }); + this.handleAutoRoleChangeModalClose(); + }; + + handleConfirmAutoRoleChangeAddUser = () => { + const { groupPermission, selectedUsers } = this.state; + this.addSelectedUsersToGroup(groupPermission?.id, selectedUsers, true); + this.handleAutoRoleChangeModalClose(); + }; + + render() { + if (!this.props.groupPermissionId) return null; + + const { + isLoadingGroup, + isLoadingUsers, + isAddingUsers, + appsNotInGroup, + usersInGroup, + groupPermission, + currentTab, + selectedUsers, + isChangeRoleModalOpen, + isAddUsersToRoleModalOpen, + updatingUserRole, + isRoleGroup, + selectedNewRole, + showRoleEditMessage, + showUserSearchBox, + errorListItems, + errorMessage, + errorTitle, + showEditRoleErrorModal, + errorIconName, + showAutoRoleChangeModal, + autoRoleChangeModalMessage, + autoRoleChangeModalList, + autoRoleChangeMessageType, + } = this.state; + + const isBasicPlan = false; + const isPaidPlan = false; + + const searchSelectClass = this.props.darkMode ? 'select-search-dark' : 'select-search'; + const showPermissionInfo = + isRoleGroup && (groupPermission?.name === 'admin' || groupPermission?.name === 'end-user'); + const disablePermissionUpdate = + isBasicPlan || groupPermission?.name === 'admin' || groupPermission?.name === 'end-user'; + + return ( + + + + Edit user role +
+ {updatingUserRole?.email} +
+ + } + handleConfirm={ + EDIT_ROLE_MESSAGE?.[groupPermission?.name]?.[selectedNewRole] && !showRoleEditMessage + ? this.showChangeRoleModalMessage + : this.updateUserRole + } + show={isChangeRoleModalOpen} + isLoading={isLoadingUsers} + handleClose={this.closeChangeRoleModal} + confirmBtnProps={{ title: 'Continue', disabled: !selectedNewRole }} + darkMode={this.props.darkMode} + className="edit-role-confirm" + > + {selectedNewRole && showRoleEditMessage ? ( +
{EDIT_ROLE_MESSAGE?.[groupPermission?.name]?.[selectedNewRole](isPaidPlan)}
+ ) : ( +
+ + { + this.updateGroupPermission(groupPermission.id, { + appCreate: !groupPermission.appCreate, + }); + this.setState({ + updateParam: { appCreate: !groupPermission.appCreate }, + }); + }} + checked={groupPermission.appCreate} + disabled={disablePermissionUpdate} + data-cy="app-create-checkbox" + /> + + {this.props.t('globals.create', 'Create')} + + + Create apps in this workspace + + + +
+ + + +
+
+ {this.props.t( + 'header.organization.menus.manageGroups.permissionResources.folder', + 'Folder' + )} +
+
+
+ +
+
+
+
+
+ {this.props.t('globals.environmentVar', 'Workspace constant/variable')} +
+
+
+ +
+
+
+ + )} + + + + + + + {/* Granular Access */} + + + + + )} + +
+ ); + } +} + +export const ManageGroupPermissionResourcesV2 = withTranslation()(ManageGroupPermissionResourcesComponent); diff --git a/frontend/src/ManageGroupPermissions/ManageGroupPermissions.jsx b/frontend/src/ManageGroupPermissions/ManageGroupPermissions.jsx index a7fb9ca550..3bed6a747c 100644 --- a/frontend/src/ManageGroupPermissions/ManageGroupPermissions.jsx +++ b/frontend/src/ManageGroupPermissions/ManageGroupPermissions.jsx @@ -455,7 +455,7 @@ class ManageGroupPermissionsComponent extends React.Component { title={ showGroupNameUpdateForm ? this.props.t('header.organization.menus.manageGroups.permissions.updateGroup', 'Update group') - : this.props.t('header.organization.menus.manageGroups.permissions.addNewGroup', 'Add new group') + : this.props.t('header.organization.menus.manageGroups.permissions.addNewGroup', 'Create new group') } >
+ +
+ onClose()} + data-cy="close-button" + iconWidth="20" + /> +
+
+ +
+ {errorTitle} +

{errorMessage}

+
+ +
+ {listItems.map((item, index) => ( +
+ {`${index + 1}. ${item}`} +
+ ))} +
+
+ + ); +} + +export default EditRoleErrorModal; diff --git a/frontend/src/ManageGroupPermissionsV2/ManageGroupPermissionsV2.jsx b/frontend/src/ManageGroupPermissionsV2/ManageGroupPermissionsV2.jsx new file mode 100644 index 0000000000..170e2f8602 --- /dev/null +++ b/frontend/src/ManageGroupPermissionsV2/ManageGroupPermissionsV2.jsx @@ -0,0 +1,788 @@ +import React from 'react'; +import { groupPermissionV2Service } from '@/_services'; +import { Tooltip } from 'react-tooltip'; +import { ConfirmDialog } from '@/_components'; +import { toast } from 'react-hot-toast'; +import { withTranslation } from 'react-i18next'; +import ErrorBoundary from '@/Editor/ErrorBoundary'; +import Modal from '../HomePage/Modal'; +import { ButtonSolid } from '@/_ui/AppButton/AppButton'; +import FolderList from '@/_ui/FolderList/FolderList'; +import { Loader } from '../ManageSSO/Loader'; +import Popover from 'react-bootstrap/Popover'; +import SolidIcon from '@/_ui/Icon/solidIcons/index'; +import ModalBase from '@/_ui/Modal'; +import OverflowTooltip from '@/_components/OverflowTooltip'; +import { ManageGroupPermissionResourcesV2 } from '@/ManageGroupPermissionResourcesV2'; +import './groupPermissions.theme.scss'; +import { SearchBox } from '@/_components/SearchBox'; +class ManageGroupPermissionsComponent extends React.Component { + constructor(props) { + super(props); + + this.state = { + isLoading: true, + groups: [], + defaultGroups: [], + creatingGroup: false, + showNewGroupForm: false, + newGroupName: '', + isDeletingGroup: false, + isUpdatingGroupName: false, + showGroupDeletionConfirmation: false, + showGroupNameUpdateForm: false, + groupToBeUpdated: null, + isSaveBtnDisabled: false, + selectedGroupPermissionId: null, + selectedGroup: 'Admin', + isDuplicatingGroup: false, + selectedGroupObject: null, + groupDuplicateOption: { addPermission: true, addApps: true, addUsers: true }, + showDuplicateGroupModal: false, + groupToDuplicate: '', + showGroupSearchBar: false, + filteredGroup: [], + groupNameMessage: 'Group name must be unique and max 50 characters', + }; + } + + componentDidMount() { + this.fetchGroups(); + } + + findCurrentGroupDetails = (data) => { + let currentUpdatedGroup = data.find((item) => { + return item.name == this.state.newGroupName; + }); + this.setState({ selectedGroup: currentUpdatedGroup.name }); + return currentUpdatedGroup.id; + }; + + duplicateGroup = () => { + const { groupDuplicateOption, groupToDuplicate } = this.state; + this.setState({ isDuplicatingGroup: true, creatingGroup: true }); + groupPermissionV2Service + .duplicate(groupToDuplicate, groupDuplicateOption) + .then((data) => { + this.setState({ + newGroupName: data?.name, + }); + + this.fetchGroups('current', () => { + this.setState({ + newGroupName: '', + creatingGroup: false, + selectedGroupPermissionId: data?.id, + selectedGroup: data?.name, + isDuplicatingGroup: false, + showDuplicateGroupModal: false, + groupDuplicateOption: { addPermission: true, addApps: true, addUsers: true }, + }); + }); + + toast.success('Group duplicated successfully!'); + }) + .catch((err) => { + this.setState({ + isDuplicatingGroup: false, + groupDuplicateOption: { addPermission: true, addApps: true, addUsers: true }, + }); + console.error('Error occured in duplicating: ', err); + toast.error('Could not duplicate group.\nPlease try again!'); + }); + }; + + toggleShowDuplicateModal = () => { + this.setState((prevState) => ({ + showDuplicateGroupModal: !prevState.showDuplicateGroupModal, + groupToDuplicate: '', + groupDuplicateOption: { addPermission: true, addApps: true, addUsers: true }, + })); + }; + + renderPopoverContent = (props, compoParam) => { + const { groupName, id } = compoParam; + const deleteGroup = () => { + this.deleteGroup(id); + }; + + const duplicateGroup = () => { + this.showDuplicateDiologBox(id); + }; + + const isDefaultGroup = groupName == 'end-user' || groupName == 'admin' || groupName == 'builder'; + + return ( +
+ + +
+ + +
+
+
+ {(groupName == 'all_users' || groupName == 'admin' || groupName == 'builder' || groupName == 'end-user') && ( + + )} +
+ ); + }; + sortDefaultGroup = (list) => { + const priority = { + admin: 1, + builder: 2, + 'end-user': 3, + }; + list.sort((a, b) => { + const priorityA = priority[a.name] || 4; // default to 4 if not found + const priorityB = priority[b.name] || 4; // default to 4 if not found + return priorityA - priorityB; + }); + return list; + }; + + fetchGroups = (type = 'admin', callback = () => {}) => { + this.setState({ + isLoading: true, + }); + + groupPermissionV2Service + .getGroups() + .then((data) => { + const groupPermissions = data.groupPermissions; + const defaultGroups = this.sortDefaultGroup(groupPermissions.filter((group) => group.type === 'default')); + const currentGroupId = + type == 'admin' + ? defaultGroups[0].id + : type == 'current' + ? this.findCurrentGroupDetails(groupPermissions) + : groupPermissions.at(-1).id; + this.setState( + { + groups: groupPermissions.filter((group) => group.type === 'custom'), + defaultGroups: defaultGroups, + filteredGroup: groupPermissions.filter((group) => group.type === 'custom'), + isLoading: false, + selectedGroupPermissionId: currentGroupId, + selectedGroupObject: groupPermissions.find((group) => group.id === currentGroupId), + }, + callback + ); + }) + .catch(({ error }) => { + toast.error(error); + this.setState({ + isLoading: false, + }); + }); + }; + + handleGroupSearch = (e) => { + const { groups } = this.state; + let filteredGroup = groups; + const value = e?.target?.value; + if (value) { + filteredGroup = groups.filter((group) => group.name.toLowerCase().includes(value.toLowerCase())); + } + this.setState({ + filteredGroup, + }); + }; + + changeNewGroupName = (value) => { + if (value.length > 50) { + this.setState({ + groupNameMessage: 'Max length has been reached', + isSaveBtnDisabled: true, + newGroupName: value?.slice(0, 50), + }); + return; + } + this.setState({ + newGroupName: value, + isSaveBtnDisabled: false, + groupNameMessage: 'Group name must be unique and max 50 characters', + }); + if ((this.state.groupToBeUpdated && this.state.groupToBeUpdated.name === value) || !value) { + this.setState({ + isSaveBtnDisabled: true, + }); + } + }; + + humanizeifDefaultGroupName = (groupName) => { + switch (groupName) { + case 'end-user': + return 'End-user'; + case 'admin': + return 'Admin'; + case 'builder': + return 'Builder'; + default: + return groupName; + } + }; + + createGroup = () => { + this.setState({ creatingGroup: true }); + groupPermissionV2Service + .create(this.state.newGroupName) + .then(() => { + this.setState({ + creatingGroup: false, + showNewGroupForm: false, + newGroupName: null, + selectedGroup: this.state.newGroupName, + }); + toast.success('Group has been created'); + this.fetchGroups('new'); + }) + .catch(({ error }) => { + toast.error(error, { + style: { + maxWidth: '500px !important', + }, + }); + this.setState({ + creatingGroup: false, + showNewGroupForm: true, + }); + }); + }; + + deleteGroup = (groupPermissionId) => { + this.setState({ + showGroupDeletionConfirmation: true, + groupToBeDeleted: groupPermissionId, + }); + }; + + updateGroupName = (groupPermission) => { + this.setState({ + showGroupNameUpdateForm: true, + groupToBeUpdated: groupPermission, + newGroupName: groupPermission.name, + isSaveBtnDisabled: true, + }); + }; + + cancelDeleteGroupDialog = () => { + this.setState({ + isDeletingGroup: false, + groupToBeDeleted: null, + showGroupDeletionConfirmation: false, + }); + }; + + executeGroupDeletion = () => { + this.setState({ isDeletingGroup: true }); + groupPermissionV2Service + .del(this.state.groupToBeDeleted) + .then(() => { + toast.success('Group deleted successfully'); + this.fetchGroups(); + this.setState({ selectedGroup: 'Admin', isDeletingGroup: false }); + }) + .catch(({ error }) => { + toast.error(error); + }) + .finally(() => { + this.cancelDeleteGroupDialog(); + }); + }; + + handleGroupSearchClose = () => { + this.setState((prevState) => ({ + showGroupSearchBar: false, + filteredGroup: prevState.groups, + })); + }; + + showDuplicateDiologBox = (id) => { + this.setState({ groupToDuplicate: id, showDuplicateGroupModal: true, isDuplicatingGroup: false }); + }; + + executeGroupUpdation = () => { + this.setState({ isUpdatingGroupName: true }); + groupPermissionV2Service + .update(this.state.groupToBeUpdated?.id, { name: this.state.newGroupName }) + .then(() => { + toast.success('Group name updated successfully'); + this.fetchGroups('current'); + this.setState({ + isUpdatingGroupName: false, + groupToBeUpdated: null, + showGroupNameUpdateForm: false, + selectedGroup: this.state.newGroupName, + }); + }) + .catch(({ error }) => { + toast.error(error); + this.setState({ + isUpdatingGroupName: false, + }); + }); + }; + + render() { + const { + isLoading, + showNewGroupForm, + showGroupNameUpdateForm, + creatingGroup, + isUpdatingGroupName, + groups, + isDeletingGroup, + showGroupDeletionConfirmation, + showDuplicateGroupModal, + isDuplicatingGroup, + groupDuplicateOption, + defaultGroups, + filteredGroup, + showGroupSearchBar, + } = this.state; + + const grounNameErrorStyle = + this.state.newGroupName?.length > 50 ? { color: '#ff0000', borderColor: '#ff0000' } : {}; + const { addPermission, addApps, addUsers } = groupDuplicateOption; + const allFalse = [addPermission, addApps, addUsers].every((value) => !value); + + return ( + +
+
+ this.executeGroupDeletion()} + onCancel={() => this.cancelDeleteGroupDialog()} + darkMode={this.props.darkMode} + /> + +
+ Duplicate the following parts of the group +
+
+
+
+ { + this.setState((prevState) => ({ + groupDuplicateOption: { + ...prevState.groupDuplicateOption, + addUsers: !prevState.groupDuplicateOption.addUsers, + }, + })); + }} + data-cy="users-check-input" + /> +
+
+
+ Users +
+
+
+
+
+ { + this.setState((prevState) => ({ + groupDuplicateOption: { + ...prevState.groupDuplicateOption, + addPermission: !prevState.groupDuplicateOption.addPermission, + }, + })); + }} + data-cy="permissions-check-input" + /> +
+
+
+ Permissions +
+
+
+
+
+ { + this.setState((prevState) => ({ + groupDuplicateOption: { + ...prevState.groupDuplicateOption, + addApps: !prevState.groupDuplicateOption.addApps, + }, + })); + }} + data-cy="apps-check-input" + /> +
+
+
+ Apps +
+
+
+
+
+
+

+ {groups?.length} Groups +

+ {!showNewGroupForm && !showGroupNameUpdateForm && ( + { + e.preventDefault(); + this.setState({ newGroupName: '', showNewGroupForm: true, isSaveBtnDisabled: true }); + }} + data-cy="create-new-group-button" + leftIcon="plus" + isLoading={isLoading} + iconWidth="16" + fill={'#FDFDFE'} + > + {this.props.t( + 'header.organization.menus.manageGroups.permissions.createNewGroup', + 'Create new group' + )} + + )} +
+ + + this.setState({ + showNewGroupForm: false, + showGroupNameUpdateForm: false, + newGroupName: null, + }) + } + title={ + showGroupNameUpdateForm + ? this.props.t('header.organization.menus.manageGroups.permissions.updateGroup', 'Update group') + : this.props.t('header.organization.menus.manageGroups.permissions.addNewGroup', 'Create new group') + } + > + { + e.preventDefault(); + if (showNewGroupForm) { + this.createGroup(); + } else { + this.executeGroupUpdation(); + } + }} + > +
+
+
+ = 50 ? 'custom-input-error' : ''}`} + placeholder={this.props.t( + 'header.organization.menus.manageGroups.permissions.enterName', + 'Enter group name' + )} + onChange={(e) => { + this.changeNewGroupName(e.target.value); + }} + value={this.state.newGroupName} + data-cy="group-name-input" + autoFocus + /> + + {this.state.groupNameMessage} + +
+
+
+
+ + this.setState({ + showNewGroupForm: false, + showGroupNameUpdateForm: false, + newGroupName: null, + }) + } + disabled={creatingGroup} + data-cy="cancel-button" + variant="tertiary" + > + {this.props.t('globals.cancel', 'Cancel')} + + + {showGroupNameUpdateForm + ? this.props.t('globals.save', 'Save') + : this.props.t('header.organization.menus.manageGroups.permissions.createGroup', 'Create Group')} + +
+ +
+ + {!showNewGroupForm && !showGroupNameUpdateForm && ( +
+
+
+
+ + USER ROLE +
+ {defaultGroups.map((permissionGroup) => { + return ( + { + this.setState({ + selectedGroupPermissionId: permissionGroup.id, + selectedGroup: this.humanizeifDefaultGroupName(permissionGroup.name), + selectedGroupObject: permissionGroup, + }); + }} + toolTipText={this.humanizeifDefaultGroupName(permissionGroup.name)} + overLayComponent={this.renderPopoverContent} + className="groups-folder-list" + dataCy={this.humanizeifDefaultGroupName(permissionGroup.name) + .toLowerCase() + .replace(/\s+/g, '-')} + > + + {this.humanizeifDefaultGroupName(permissionGroup.name)} + + + ); + })} +
+
+ {!showGroupSearchBar ? ( +
+ + CUSTOM GROUPS +
+ { + e.preventDefault(); + this.setState({ showGroupSearchBar: true }); + }} + size="xsm" + rightIcon="search" + iconWidth="15" + fill="#889096" + className="create-group-custom" + /> + { + e.preventDefault(); + this.setState({ newGroupName: null, showNewGroupForm: true, isSaveBtnDisabled: true }); + }} + size="sm" + fill="#889096" + rightIcon="plus" + iconWidth="20" + className="create-group-custom" + /> +
+
+ ) : ( +
+ +
+ )} + + {groups.length ? ( + filteredGroup.map((permissionGroup) => { + return ( + { + this.setState({ + selectedGroupPermissionId: permissionGroup.id, + selectedGroup: this.humanizeifDefaultGroupName(permissionGroup.name), + selectedGroupObject: permissionGroup, + }); + }} + toolTipText={this.humanizeifDefaultGroupName(permissionGroup.name)} + overLayComponent={this.renderPopoverContent} + className="groups-folder-list" + dataCy={this.humanizeifDefaultGroupName(permissionGroup.name) + .toLowerCase() + .replace(/\s+/g, '-')} + > + + {this.humanizeifDefaultGroupName(permissionGroup.name)} + + + ); + }) + ) : ( +
+ + No custom groups added +
+ )} +
+
+ +
+ {isLoading ? ( + + ) : ( + { + return { + name: this.humanizeifDefaultGroupName(group.name), + value: group.name, + }; + })} + /> + )} +
+
+ )} +
+
+
+ ); + } +} + +export const ManageGroupPermissionsV2 = withTranslation()(ManageGroupPermissionsComponent); + +const Field = ({ + text, + onClick, + customClass, + leftIcon, + leftIconWidth, + leftIconHeight = '18', + leftIconClassName, + buttonDisable = false, + tooltipContent = '', + tooltipId = '', + darkMode = false, +}) => { + return ( +
+ +
+ {leftIcon && ( + + )} +
+
{text}
+
+
+ ); +}; diff --git a/frontend/src/ManageGroupPermissionsV2/ResourceChip.jsx b/frontend/src/ManageGroupPermissionsV2/ResourceChip.jsx new file mode 100644 index 0000000000..afb4d71a51 --- /dev/null +++ b/frontend/src/ManageGroupPermissionsV2/ResourceChip.jsx @@ -0,0 +1,103 @@ +import React, { useEffect, useState, useRef } from 'react'; +import cx from 'classnames'; // Assuming you're using the classnames package +import { humanizeifDefaultGroupName } from '@/_helpers/utils'; +import './groupPermissions.theme.scss'; + +const GroupChipTD = ({ groups = [] }) => { + const [showAllGroups, setShowAllGroups] = useState(false); + const groupsListRef = useRef(); + + useEffect(() => { + const onCloseHandler = (e) => { + if (groupsListRef.current && !groupsListRef.current.contains(e.target)) { + setShowAllGroups(false); + } + }; + + window.addEventListener('click', onCloseHandler); + return () => { + window.removeEventListener('click', onCloseHandler); + }; + }, [showAllGroups]); + + function moveValuesToLast(arr, valuesToMove) { + const validValuesToMove = valuesToMove.filter((value) => arr.includes(value)); + + validValuesToMove.forEach((value) => { + const index = arr.indexOf(value); + if (index !== -1) { + const removedItem = arr.splice(index, 1); + arr.push(removedItem[0]); + } + }); + + return arr; + } + + const orderedArray = groups; + + const toggleAllGroupsList = (e) => { + setShowAllGroups(!showAllGroups); + }; + + const renderGroupChip = (group, index) => ( + + {humanizeifDefaultGroupName(group)} + + ); + + return ( +
{ + orderedArray.length > 2 && toggleAllGroupsList(e); + }} + className={cx('text-muted resource-name-cell', { 'groups-hover': orderedArray.length > 2 })} + > +
+ {orderedArray.length === 0 ? ( + renderGroupChip('-', null) + ) : ( + <> +
+ {orderedArray.slice(0, 2).map((group, index) => { + return renderGroupChip(group, index); + })} +
+
+ {orderedArray.slice(2, 4).map((group, index) => { + return renderGroupChip(group, index); + })} +
+ {orderedArray.length > 4 && ( + +
+ +{orderedArray.length - 4} more +
+ {showAllGroups && ( +
+ {orderedArray.slice(4).map((group, index) => renderGroupChip(group, index))} +
+ )} +
+ )} + + {/* orderedArray.slice(0, 2).map((group, index) => { + if (orderedArray.length <= 2) { + return renderGroupChip(group, index); + } + + if (orderedArray.length > 2 && index === 1) { + + } + + return renderGroupChip(group, index); + }) */} + + )} +
+
+ ); +}; + +export default GroupChipTD; diff --git a/frontend/src/ManageGroupPermissionsV2/groupPermissions.theme.scss b/frontend/src/ManageGroupPermissionsV2/groupPermissions.theme.scss new file mode 100644 index 0000000000..0ad36a1d01 --- /dev/null +++ b/frontend/src/ManageGroupPermissionsV2/groupPermissions.theme.scss @@ -0,0 +1,458 @@ +@import "../_styles/colors.scss"; + +.default-group-list-container { + margin-bottom: 20px; +} + +.empty-custom-group-info{ + margin-top: 15px; + border-radius: 6px; + border: 1px dashed var(--slate8) !important; + width: 100%; + height: 32px; + + padding: 3px 12px; + display: flex; + flex-direction: row; + + .info-icon{ + margin-top: 3px; + } + + .info-label{ + margin-left: 4px; + color: var(--slate9); + } +} + +.group-title { + font-weight: 500; + color: var(--slate11); + font-size: 12px; + margin-left: 5px; + } + +.create-group-cont { + margin-left:10px ; + margin-right: auto; + display: flex; + flex-direction: row; + .create-group-custom { + width: 20px; + margin-left: 2px; + margin-right: 2px; + height: 20px; + padding: 0 0; + background: none !important; + background-color: none !important; + box-shadow: none; + + } + } + + +.searchbox-custom{ + margin-bottom: 10px; + .tj-common-search-input-group { + .input-icon-addon { + padding-right: 8px; + padding-left: 8px; + + } + + input { + box-sizing: border-box; + display: flex; + flex-direction: row; + align-items: center; + padding: 4px 8px !important; + gap: 16px; + width: 190px !important; + height: 28px !important; + background: var(--base); + border: 1px solid var(--slate7); + border-radius: 6px; + color: var(--slate12); + padding-left: 33px !important; + + + ::placeholder { + color: var(--slate9); + margin-left: 5px !important; + padding-left: 5px !important; + background-color: red !important; + } + + &:hover { + background: var(--slate2); + border: 1px solid var(--slate8); + } + + &:active { + background: var(--indigo2); + border: 2px solid var(--indigo11); + box-shadow: 0px 0px 0px 2px #C6D4F9; + outline: none; + } + + &:focus-visible { + background: var(--slate2); + border: 1px solid var(--slate8); + border-radius: 6px; + outline: none; + padding-left: 12px !important; + } + + &:disabled { + background: var(--slate3); + border: 1px solid var(--slate7); + } + } + } + +} + +.edit-role-modal { + font-family: 'IBM Plex Sans'; + + .modal-dialog { + width: 320px; + } + + .modal-content { + background: linear-gradient(0deg, #FFFFFF, #FFFFFF), + linear-gradient(0deg, #DFE3E6, #DFE3E6); + } + + .modal-header { + justify-content: center !important; + flex-direction: column; + padding: 30px 32px 20px 32px; + border: none; + + .remove-icon-container{ + display: flex; + justify-content: flex-end; + margin-right: 10px; + + .close-btn { + + width: 20px; + margin: 5px 5px 5px 5px; + height: 20px; + padding: 0 0; + background: none !important; + background-color: none !important; + box-shadow: none; + } + } + .icon-class { + display: flex; /* Enable flexbox */ + justify-content: center; /* Center items horizontally */ + align-items: center; /* Center items vertically */ + justify-content: center; + width: 64px; + height: 64px; + background-color: var(--tomato3); + border-radius: 6px; + } + + .header-text { + font-style: normal; + font-weight: 600; + font-size: 16px; + text-align: center; + // line-height: 36px; + margin: 12px 0 5px 0; + } + + p { + font-style: normal; + font-weight: 400; + font-size: 14px; + color: #687076; + text-align: Center; + margin-bottom: 0px; + } + } + + .modal-body { + border: none; + padding: 12px 12px 22px 27px; + + .item-list { + display: flex; + gap: 5px; + flex-direction: column; + max-height: 100px; /* Set a fixed height or max-height */ + overflow-y: scroll; /* Enable vertical scrolling */ + } + } +} + +.edit-role-modal.dark-mode { + + .modal-footer, + .modal-header { + border-color: #232e3c !important; + + p { + color: rgba(255, 255, 255, 0.5) !important; + } + } + + .modal-body, + .modal-footer, + .modal-header, + .modal-content { + color: white; + background-color: #2b394a; + } + + .modal-content { + border: none; + } +} + + +.resource-name-cell { + transition: 0.3s all; + border-radius: 6px; + max-width: 170px; + position: relative !important; + overflow: visible !important; + + .groups-name-container { + display: flex; + flex-direction: column; + row-gap: 8px; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + max-height: 200px; + max-width: 170px; + + + .empty-text{ + display: flex; + justify-content: center; + margin-left: 20px; + } + + .groups-name-row { + display: flex; + column-gap: 8px; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + max-width: 170px; + } + } + + .group-chip { + padding: 2px 8px; + margin: 0; + border-radius: 6px; + background-color: var(--slate3); + color: var(--slate11); + min-height: 24px; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + max-width: 100px; + font-size: 12px; + } + + .all-groups-list { + position: absolute; + width: 100%; + top: 59px; + display: flex; + flex-direction: column; + background: var(--slate1); + align-items: flex-start; + border-radius: 6px; + border: 1px solid var(--slate1); + box-shadow: 0px 4px 6px -2px rgba(16, 24, 40, 0.03), 0px 12px 16px -4px rgba(16, 24, 40, 0.08); + padding: 9px 10px; + gap: 10px; + cursor: default; + max-height: 240px; + overflow: auto; + left: 0px; + z-index: 1; + + + .group-chip { + padding: 2px 8px; + margin: 0; + border-radius: 6px; + background-color: var(--slate3); + color: var(--slate11); + min-height: 24px; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + max-width: 200px; + } + } +} + +.groups-name-cell[data-active="true"] { + display: flex; + background: var(--gray5) !important; + justify-content: center; + + .groups-name-container { + padding-left: 6px; + justify-content: center; + } + + .group-chip { + max-width: unset !important; + } +} +.role-name-cell { + transition: 0.3s all; + border-radius: 6px; + width: 120px !important; + position: relative !important; + overflow: visible !important; + + .groups-name-container { + display: flex; + column-gap: 8px; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + max-width: 185px; + } + + .group-chip { + padding: 2px 8px; + margin: 0; + border-radius: 6px; + background-color: var(--slate3); + // color: var(--slate11); + min-height: 24px; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + max-width: 95px; + } + + .all-groups-list { + position: absolute; + width: 100%; + top: 41px; + display: flex; + flex-direction: column; + background: var(--slate1); + align-items: flex-start; + border-radius: 6px; + border: 1px solid var(--slate1); + box-shadow: 0px 4px 6px -2px rgba(16, 24, 40, 0.03), 0px 12px 16px -4px rgba(16, 24, 40, 0.08); + padding: 9px 10px; + gap: 10px; + cursor: default; + max-height: 240px; + overflow: auto; + left: 0px; + z-index: 1; + } +} + +.role-name-cell[data-active="true"] { + display: flex; + background: var(--gray5) !important; + justify-content: center; + + .groups-name-container { + padding-left: 6px; + justify-content: center; + } + + .group-chip { + max-width: unset !important; + } +} + + +.edit-icon-container{ + max-width: 10px; + display: flex; + justify-content: flex-end; + align-items: flex-start; + padding: 0; + height: 100%; + margin-bottom: auto; +} + + +.edit-permission-custom { + width: 20px; + margin-left: 2px; + margin-right: 2px; + height: 20px; + padding: 0 0; + background: none !important; + background-color: none !important; + box-shadow: none; +} + +.manage-resource-permission{ + + + .tj-text-xxsm{ + color: var(--slate11) + } + + + transition: background-color 0.3s ease; + border-bottom: 1px solid var(--slate5); + display: flex; + align-items:baseline; + padding: 12px; + gap: 10px; + + div { + width: 206px; + + + .resource-name { + display: flex; + flex-direction: row !important; + gap: 10px; + + + .resource-icon { + margin-right: 200px; + } + .resource-text { + margin-left: 10px; + } + } + } + + &:hover { + background-color: var(--slate3); + } + + +} + +.manage-resource-body { + padding: 24px; + font-size: 12px; + overflow-y: auto; + height: calc(100vh - 300px); + +} + +.error-text{ + color: red; +} +.form-control, +.error-input { + border-color: red !important; +} \ No newline at end of file diff --git a/frontend/src/ManageOrgConstants/ManageOrgConstants.jsx b/frontend/src/ManageOrgConstants/ManageOrgConstants.jsx index 77e7bc3a7d..bc897ba952 100644 --- a/frontend/src/ManageOrgConstants/ManageOrgConstants.jsx +++ b/frontend/src/ManageOrgConstants/ManageOrgConstants.jsx @@ -126,24 +126,15 @@ const ManageOrgConstantsComponent = ({ darkMode }) => { }; const canCreateVariable = () => { - return canAnyGroupPerformAction( - 'org_environment_variable_create', - authenticationService.currentSessionValue.group_permissions - ); + return authenticationService.currentSessionValue.user_permissions.org_constant_c_r_u_d; }; const canUpdateVariable = () => { - return canAnyGroupPerformAction( - 'org_environment_variable_update', - authenticationService.currentSessionValue.group_permissions - ); + return authenticationService.currentSessionValue.user_permissions.org_constant_c_r_u_d; }; const canDeleteVariable = () => { - return canAnyGroupPerformAction( - 'org_environment_variable_delete', - authenticationService.currentSessionValue.group_permissions - ); + return authenticationService.currentSessionValue.user_permissions.org_constant_c_r_u_d; }; const fetchEnvironments = () => { diff --git a/frontend/src/ManageOrgUsers/InviteUsersForm.jsx b/frontend/src/ManageOrgUsers/InviteUsersForm.jsx index 8fe5c71c20..ccbb0cec99 100644 --- a/frontend/src/ManageOrgUsers/InviteUsersForm.jsx +++ b/frontend/src/ManageOrgUsers/InviteUsersForm.jsx @@ -8,6 +8,8 @@ import { toast } from 'react-hot-toast'; import { FileDropzone } from './FileDropzone'; import { USER_DRAWER_MODES } from '@/_helpers/utils'; import { UserGroupsSelect } from './UserGroupsSelect'; +import { EDIT_ROLE_MESSAGE } from '@/ManageGroupPermissionResourcesV2/constant'; +import ModalBase from '@/_ui/Modal'; function InviteUsersForm({ onClose, @@ -27,26 +29,54 @@ function InviteUsersForm({ }) { const { t } = useTranslation(); const [activeTab, setActiveTab] = useState(1); - const [selectedGroups, setSelectedGroups] = useState([]); const [existingGroups, setExistingGroups] = useState([]); + const [newRole, setNewRole] = useState(null); + + const customGroups = groups.filter((group) => group.groupType === 'custom'); + const roleGroups = groups + .filter((group) => group.groupType === 'default') + .sort((a, b) => { + const sortOrder = ['admin', 'builder', 'end-user']; + const indexA = sortOrder.indexOf(a.value); + const indexB = sortOrder.indexOf(b.value); + + return indexA - indexB; + }); + const [isChangeRoleModalOpen, setIsChangeRoleModalOpen] = useState(false); + const groupedOptions = [ + { + label: 'default', + options: roleGroups, + }, + { + label: 'custom', + options: customGroups, + }, + ]; + const [selectedGroups, setSelectedGroups] = useState([]); const hiddenFileInput = useRef(null); useEffect(() => { if (currentEditingUser && groups.length) { - const { first_name, last_name, email, groups: addedToGroups } = currentEditingUser; + const { first_name, last_name, email, groups: addedToCustomGroups, role_group } = currentEditingUser; + const addedToGroups = [...addedToCustomGroups, ...role_group]; setUserValues({ fullName: `${first_name}${last_name && ` ${last_name}`}`, email: email, }); const preSelectedGroups = groups - .filter((group) => addedToGroups.includes(group.value)) + .filter((group) => addedToGroups.map((group) => group.name).includes(group.value)) .map((filteredGroup) => ({ ...filteredGroup, label: filteredGroup.name, })); - setExistingGroups(groups.filter((group) => addedToGroups.includes(group.value)).map((g) => g.value)); + setExistingGroups( + groups.filter((group) => addedToCustomGroups.map((gp) => gp.name).includes(group.value)).map((g) => g.id) + ); onChangeHandler(preSelectedGroups); + } else { + onChangeHandler(roleGroups.filter((group) => group.value === 'end-user')); } }, [currentEditingUser, groups]); @@ -65,44 +95,91 @@ function InviteUsersForm({ }; const onChangeHandler = (items) => { - setSelectedGroups(items); + let finalGroup = items; + const roleGroups = items.filter((group) => group.groupType === 'default'); + const currentRole = selectedGroups.find((group) => group.groupType === 'default'); + if (roleGroups.length == 2) { + finalGroup = items.filter((group) => group.value !== currentRole.value); + } + if (roleGroups.length === 0) return; + if (currentEditingUser) { + const role = finalGroup.find( + (group) => + group.groupType === 'default' && !currentEditingUser.role_group.map((role) => role.name).includes(group.value) + ); + setNewRole(role); + } + setSelectedGroups(finalGroup); }; const handleCreateUser = (e) => { e.preventDefault(); - const selectedGroupsIds = selectedGroups.map((group) => group.value); - manageUser(currentEditingUser?.id, selectedGroupsIds); + const role = selectedGroups.find((group) => group.groupType === 'default').value; + const selectedGroupsIds = selectedGroups.filter((group) => group.groupType !== 'default').map((group) => group.id); + manageUser(currentEditingUser?.id, selectedGroupsIds, role); }; const handleEditUser = (e) => { e.preventDefault(); - const selectedGroupsIds = selectedGroups.map((group) => group.value); - const newGroupsToAdd = selectedGroupsIds.filter((selectedGroupId) => !existingGroups.includes(selectedGroupId)); - const groupsToRemove = existingGroups.filter((existingGroup) => !selectedGroupsIds.includes(existingGroup)); - manageUser(currentEditingUser.id, selectedGroupsIds, newGroupsToAdd, groupsToRemove); + if (newRole) setIsChangeRoleModalOpen(true); + else { + editUser(); + } + }; + + const editUser = () => { + const { newGroupsToAdd, groupsToRemove, selectedGroupsIds, role } = getEditedGroups(); + manageUser(currentEditingUser.id, selectedGroupsIds, role, newGroupsToAdd, groupsToRemove); }; const getEditedGroups = () => { - const selectedGroupsIds = selectedGroups.map((group) => group.value); + const selectedGroupsIds = selectedGroups.filter((group) => group.groupType !== 'default').map((group) => group.id); const newGroupsToAdd = selectedGroupsIds.filter((selectedGroupId) => !existingGroups.includes(selectedGroupId)); const groupsToRemove = existingGroups.filter((existingGroup) => !selectedGroupsIds.includes(existingGroup)); - return { newGroupsToAdd, groupsToRemove }; + return { newGroupsToAdd, groupsToRemove, selectedGroupsIds }; }; + const validUserDetail = fields['fullName']?.length > 0 && fields['email']?.length > 0; + const isEdited = () => { const { newGroupsToAdd, groupsToRemove } = getEditedGroups(); const { first_name, last_name } = currentEditingUser || {}; return isEditing ? fields['fullName'] !== `${first_name}${last_name && ` ${last_name}`}` || groupsToRemove.length || + newRole || newGroupsToAdd.length : true; }; const isEditing = userDrawerMode === USER_DRAWER_MODES.EDIT; + const containRoleGroup = + selectedGroups.filter((item) => ['admin', 'end-user', 'builder'].includes(item.value)).length > 0; return (
+ {isChangeRoleModalOpen && ( + + Edit user role +
+ {currentEditingUser?.email} +
+
+ } + handleConfirm={editUser} + show={isChangeRoleModalOpen} + handleClose={() => { + setIsChangeRoleModalOpen(false); + onCancel(); + onClose(); + }} + confirmBtnProps={{ title: 'Continue' }} + > +
{EDIT_ROLE_MESSAGE?.[currentEditingUser?.role_group?.[0]?.name]?.[newRole?.value]()}
+ + )}
@@ -216,7 +293,7 @@ function InviteUsersForm({ ? 'User groups' : t('header.organization.menus.manageUsers.selectGroup', 'Select Group')} - +
@@ -274,7 +351,13 @@ function InviteUsersForm({ form={activeTab == 1 ? 'inviteByEmail' : 'inviteBulkUsers'} type="submit" variant="primary" - disabled={uploadingUsers || creatingUser || !isEdited()} + disabled={ + uploadingUsers || + creatingUser || + !isEdited() || + (!isEditing && !containRoleGroup && uploadingUsers) || + (!isEditing && !validUserDetail && uploadingUsers) + } data-cy={activeTab == 1 ? 'button-invite-users' : 'button-upload-users'} leftIcon={activeTab == 1 ? 'sent' : 'fileupload'} width="20" diff --git a/frontend/src/ManageOrgUsers/ManageOrgUsers.jsx b/frontend/src/ManageOrgUsers/ManageOrgUsers.jsx index 2d7d02e4ef..b16d83b27a 100644 --- a/frontend/src/ManageOrgUsers/ManageOrgUsers.jsx +++ b/frontend/src/ManageOrgUsers/ManageOrgUsers.jsx @@ -11,6 +11,7 @@ import { ButtonSolid } from '@/_ui/AppButton/AppButton'; import ManageOrgUsersDrawer from './ManageOrgUsersDrawer'; import { USER_DRAWER_MODES } from '@/_helpers/utils'; import { getQueryParams } from '@/_helpers/routes'; +import EditRoleErrorModal from '@/ManageGroupPermissionsV2/ErrorModal/ErrorModal'; class ManageOrgUsersComponent extends React.Component { constructor(props) { @@ -36,6 +37,11 @@ class ManageOrgUsersComponent extends React.Component { userDrawerMode: USER_DRAWER_MODES.CREATE, newSelectedGroups: [], existingGroupsToRemove: [], + showErrorModal: false, + errorModalMessage: '', + errorItemList: [], + errorTitle: '', + errorIconName: 'usergear', }; } @@ -73,7 +79,6 @@ class ManageOrgUsersComponent extends React.Component { if (!this.state.file) { errors['file'] = 'This field is required'; } - this.setState({ errors: errors }); return Object.keys(errors).length === 0; } @@ -96,11 +101,22 @@ class ManageOrgUsersComponent extends React.Component { changeNewUserOption = (name, e) => { let fields = this.state.fields; + let errors = {}; fields[name] = e.target.value; this.setState({ fields, }); + + if (name === 'email') { + if (!this.validateEmail(fields['email'])) { + errors['email'] = 'Email is not valid'; + this.setState({ errors }); + } else { + errors['email'] = ''; + this.setState({ errors }); + } + } }; archiveOrgUser = (id) => { @@ -135,6 +151,7 @@ class ManageOrgUsersComponent extends React.Component { }); }; + //Need to work on that inviteBulkUsers = (event) => { event.preventDefault(); if (this.handleFileValidation()) { @@ -178,7 +195,7 @@ class ManageOrgUsersComponent extends React.Component { }); }; - manageUser = (currentOrgUserId, selectedGroups, groupsToAdd, groupsToRemove) => { + manageUser = (currentOrgUserId, selectedGroups, role, groupsToAdd, groupsToRemove) => { const isEditing = this.state.userDrawerMode === USER_DRAWER_MODES.EDIT; if (this.handleValidation()) { if (!this.state.fields.fullName?.trim()) { @@ -201,11 +218,13 @@ class ManageOrgUsersComponent extends React.Component { last_name: this.state.fields.lastName, email: this.state.fields.email, groups: selectedGroups, + role: role, }; const updateUserBody = { addGroups: groupsToAdd, removeGroups: groupsToRemove, + role: role, }; service(currentOrgUserId, isEditing ? updateUserBody : createUserBody) .then(() => { @@ -220,7 +239,13 @@ class ManageOrgUsersComponent extends React.Component { }); }) .catch(({ error }) => { - toast.error(error); + this.setState({ + showErrorModal: true, + errorModalMessage: error.error, + errorTitle: error?.title || 'Conflicting Permissions', + errorItemList: error?.data, + errorIconName: 'usergear', + }); this.setState({ creatingUser: false }); }); } else { @@ -251,6 +276,16 @@ class ManageOrgUsersComponent extends React.Component { toast.success('Invitation URL copied'); }; + clearErrorState = () => { + this.setState({ + showErrorModal: false, + errorModalMessage: '', + errorItemList: [], + errorTitle: '', + errorIconName: '', + }); + }; + pageChanged = (page) => { this.fetchUsers(page, this.state.options); }; @@ -290,10 +325,24 @@ class ManageOrgUsersComponent extends React.Component { meta, currentEditingUser, userDrawerMode, + showErrorModal, + errorModalMessage, + errorItemList, + errorTitle, + errorIconName, } = this.state; return (
+ {this.state.isInviteUsersDrawerOpen && ( { switch (groupName) { - case 'all_users': - return 'All users'; + case 'end-user': + return 'End user'; case 'admin': return 'Admin'; + case 'builder': + return 'Builder'; default: return groupName; @@ -41,19 +43,17 @@ const ManageOrgUsersDrawer = ({ const fetchOrganizations = () => { const { current_organization_id } = authenticationService.currentSessionValue; - groupPermissionService + groupPermissionV2Service .getGroups() - .then(({ group_permissions }) => { - const orgGroups = group_permissions - .filter((group) => group.organization_id === current_organization_id) - .map(({ group }) => ({ - label: - group === 'all_users' && isEditing - ? `${humanizeifDefaultGroupName(group)} (Default group)` - : humanizeifDefaultGroupName(group), - name: humanizeifDefaultGroupName(group), - value: group, - ...(group === 'all_users' && isEditing && { isDisabled: true, isFixed: true }), + .then(({ groupPermissions }) => { + const orgGroups = groupPermissions + .filter((group) => group.organizationId === current_organization_id) + .map(({ name, type, id }) => ({ + label: humanizeifDefaultGroupName(name), + name: humanizeifDefaultGroupName(name), + value: name, + groupType: type, + id: id, })); setGroups(orgGroups); }) diff --git a/frontend/src/ManageOrgUsers/UserGroupsSelect.jsx b/frontend/src/ManageOrgUsers/UserGroupsSelect.jsx index 99a67c1e1e..6270a42eb5 100644 --- a/frontend/src/ManageOrgUsers/UserGroupsSelect.jsx +++ b/frontend/src/ManageOrgUsers/UserGroupsSelect.jsx @@ -3,6 +3,7 @@ import { ButtonSolid } from '@/_ui/AppButton/AppButton'; import React, { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import Select, { components } from 'react-select'; +import SolidIcon from '@/_ui/Icon/solidIcons/index'; export function UserGroupsSelect(props) { const navigate = useNavigate(); @@ -30,12 +31,22 @@ export function UserGroupsSelect(props) { ); }; - const InputOption = ({ getStyles, Icon, isDisabled, isFocused, isSelected, children, innerProps, ...rest }) => { + const formatGroupLabel = (data) => { + const type = data.label; + return ( +
+ + {type === 'default' ? 'USER ROLE' : 'GROUPS'} + {type === 'default' && *} +
+ ); + }; + + const InputOption = ({ getStyles, Icon, isDisabled, isFocused, isSelected, children, data, innerProps, ...rest }) => { const [isActive, setIsActive] = useState(false); const onMouseDown = () => setIsActive(true); const onMouseUp = () => setIsActive(false); const onMouseLeave = () => setIsActive(false); - const style = { alignItems: 'center', backgroundColor: 'transparent', @@ -50,7 +61,6 @@ export function UserGroupsSelect(props) { onMouseLeave, style, }; - return ( ); }; + const MultiValueRemove = (props) => { + // Conditionally render the close icon + if (props.data.groupType === 'default') { + return null; // Do not render the close icon + } + return ; + }; const MultiValue = (props) => ( @@ -80,6 +101,11 @@ export function UserGroupsSelect(props) { ); const selectStyles = { + placeholder: (base) => ({ + ...base, + fontSize: '12px', + color: '#A0A0A0', + }), indicatorSeparator: (base) => ({ ...base, display: 'none', @@ -122,6 +148,7 @@ export function UserGroupsSelect(props) { border: '1px solid var(--slate7)', boxShadow: 'none', borderRadius: '6px', + background: 'unset', '&:hover': { border: '1px solid var(--slate8)', @@ -155,10 +182,11 @@ export function UserGroupsSelect(props) { closeMenuOnSelect={false} hideSelectedOptions={false} className={darkMode && 'theme-dark dark-theme'} - components={{ Option: InputOption, MultiValue, IndicatorSeparator: null }} + formatGroupLabel={formatGroupLabel} + components={{ Option: InputOption, MultiValue, MultiValueRemove, IndicatorSeparator: null }} {...props} styles={selectStyles} - placeholder="Select groups to add for this user" + placeholder="Select user groups and role .." noOptionsMessage={() => 'No groups found'} /> ); diff --git a/frontend/src/ManageOrgVars/ManageOrgVars.jsx b/frontend/src/ManageOrgVars/ManageOrgVars.jsx index 808214446e..3cc0adf66f 100644 --- a/frontend/src/ManageOrgVars/ManageOrgVars.jsx +++ b/frontend/src/ManageOrgVars/ManageOrgVars.jsx @@ -244,10 +244,7 @@ class RawManageOrgVarsComponent extends React.Component { } canDeleteVariable = () => { - return this.canAnyGroupPerformAction( - 'org_environment_variable_delete', - authenticationService.currentSessionValue.group_permissions - ); + return authenticationService.currentSessionValue.org_constant_c_r_u_d; }; setIsManageVarDrawerOpen = (val) => { this.setState({ isManageVarDrawerOpen: val }); diff --git a/frontend/src/TooljetDatabase/Forms/styles.scss b/frontend/src/TooljetDatabase/Forms/styles.scss index e30a8e22ed..150c7a6e8c 100644 --- a/frontend/src/TooljetDatabase/Forms/styles.scss +++ b/frontend/src/TooljetDatabase/Forms/styles.scss @@ -177,7 +177,7 @@ input.form-control:disabled { gap: 16px !important; - background: #f4f6fa !important; + background: var(--base) !important; border: 1px solid var(--slate7) !important; border-radius: 6px !important; margin-bottom: 4px !important; diff --git a/frontend/src/WorkspaceConstants/index.jsx b/frontend/src/WorkspaceConstants/index.jsx index 7a3821aa2d..851badf43c 100644 --- a/frontend/src/WorkspaceConstants/index.jsx +++ b/frontend/src/WorkspaceConstants/index.jsx @@ -17,10 +17,7 @@ export default function WorkspaceConstants({ darkMode, switchDarkMode }) { }; const canCreateVariableOrConstant = () => { - return canAnyGroupPerformAction( - 'org_environment_variable_create', - authenticationService.currentSessionValue.group_permissions - ); + return authenticationService.currentSessionValue.user_permissions.org_constant_c_r_u_d; }; useEffect(() => { diff --git a/frontend/src/_components/LogoNavDropdown.jsx b/frontend/src/_components/LogoNavDropdown.jsx index c5c2e6adcd..f512e41750 100644 --- a/frontend/src/_components/LogoNavDropdown.jsx +++ b/frontend/src/_components/LogoNavDropdown.jsx @@ -38,16 +38,17 @@ export default function LogoNavDropdown({ darkMode }) { Database )} - - - Data sources - - + {admin && ( + + + Data sources + + )} { - setOptions(filterOptions(listOfOptions.current)); + setOptions(listOfOptions.current); // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedValues, listOfOptions.current]); @@ -35,14 +35,19 @@ function MultiSelectUser({ ); function renderCustom(props, option) { + const valuePresent = selectedValues.some((item) => item.value === option.value); return (
{ - onSelect([...selectedValues, option]); + if (!valuePresent) { + onSelect([...selectedValues, option]); + } else { + onSelect([...selectedValues.filter((item) => item.value !== option.value)]); + } }} />
@@ -76,7 +81,7 @@ function MultiSelectUser({ closeOnSelect={false} search={true} multiple - value={{ name: '' }} + value={selectedValues} onChange={(id, value) => onSelect([...selectedValues, ...value])} placeholder={placeholder} debounce={onSearch ? 300 : undefined} diff --git a/frontend/src/_components/OrganizationLogin/OrganizationLogin.jsx b/frontend/src/_components/OrganizationLogin/OrganizationLogin.jsx index e82cfddf22..3c2eb1981f 100644 --- a/frontend/src/_components/OrganizationLogin/OrganizationLogin.jsx +++ b/frontend/src/_components/OrganizationLogin/OrganizationLogin.jsx @@ -319,7 +319,7 @@ class OrganizationLogin extends React.Component {
- Users will be able to sign up without being invited + Users will be able to sign up as end-users without being invited
diff --git a/frontend/src/_components/SearchBox.jsx b/frontend/src/_components/SearchBox.jsx index 4ee201d226..ec52e37fa1 100644 --- a/frontend/src/_components/SearchBox.jsx +++ b/frontend/src/_components/SearchBox.jsx @@ -38,12 +38,24 @@ export const SearchBox = forwardRef( onClearCallback?.(); }; + const handleClickOutside = (event) => { + if (ref.current && !ref.current.contains(event.target)) { + clearSearchText(); + // Your function to be triggered + } + }; + const mounted = useMounted(); useEffect(() => { + document.addEventListener('mousedown', handleClickOutside); if (mounted) { onSubmit?.(debouncedSearchTerm); } + return () => { + // Cleanup event listener on component unmount + document.removeEventListener('mousedown', handleClickOutside); + }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [debouncedSearchTerm, onSubmit]); diff --git a/frontend/src/_helpers/utils.js b/frontend/src/_helpers/utils.js index 1e44d3373f..52300348e8 100644 --- a/frontend/src/_helpers/utils.js +++ b/frontend/src/_helpers/utils.js @@ -1227,11 +1227,13 @@ export const USER_DRAWER_MODES = { export const humanizeifDefaultGroupName = (groupName) => { switch (groupName) { - case 'all_users': - return 'All users'; + case 'end-user': + return 'End user'; case 'admin': return 'Admin'; + case 'builder': + return 'Builder'; default: return groupName; diff --git a/frontend/src/_services/authentication.service.js b/frontend/src/_services/authentication.service.js index 191221229d..09114aad0d 100644 --- a/frontend/src/_services/authentication.service.js +++ b/frontend/src/_services/authentication.service.js @@ -17,8 +17,10 @@ const currentSessionSubject = new BehaviorSubject({ current_organization_name: null, super_admin: null, admin: null, + user_permissions: null, group_permissions: null, app_group_permissions: null, + role: null, organizations: [], isUserLoggingIn: false, authentication_status: null, diff --git a/frontend/src/_services/groupPermission.v2.service.js b/frontend/src/_services/groupPermission.v2.service.js new file mode 100644 index 0000000000..8ac8da2338 --- /dev/null +++ b/frontend/src/_services/groupPermission.v2.service.js @@ -0,0 +1,190 @@ +import config from 'config'; +import { authHeader, handleResponse } from '@/_helpers'; + +export const groupPermissionV2Service = { + create, + update, + del, + getGroup, + getGroups, + fetchAddableApps, + getUsersInGroup, + getUsersNotInGroup, + updateUserRole, + addUsersInGroups, + deleteUserFromGroup, + createGranularPermission, + fetchGranularPermissions, + deleteGranularPermission, + updateGranularPermission, + duplicate, +}; + +function create(name) { + const body = { + name, + }; + + const requestOptions = { + method: 'POST', + headers: authHeader(), + credentials: 'include', + body: JSON.stringify(body), + }; + return fetch(`${config.apiUrl}/v2/group_permissions`, requestOptions).then(handleResponse); +} + +function update(groupPermissionId, body) { + const requestOptions = { + method: 'PUT', + headers: authHeader(), + credentials: 'include', + body: JSON.stringify(body), + }; + return fetch(`${config.apiUrl}/v2/group_permissions/${groupPermissionId}`, requestOptions).then(handleResponse); +} + +function del(groupPermissionId) { + const requestOptions = { + method: 'DELETE', + headers: authHeader(), + credentials: 'include', + }; + return fetch(`${config.apiUrl}/v2/group_permissions/${groupPermissionId}`, requestOptions).then(handleResponse); +} + +function getGroup(groupPermissionId) { + const requestOptions = { + method: 'GET', + headers: authHeader(), + credentials: 'include', + }; + return fetch(`${config.apiUrl}/v2/group_permissions/${groupPermissionId}`, requestOptions).then(handleResponse); +} + +function fetchAddableApps() { + const requestOptions = { + method: 'GET', + headers: authHeader(), + credentials: 'include', + }; + return fetch(`${config.apiUrl}/v2/group_permissions/granular-permissions/addable-apps`, requestOptions).then( + handleResponse + ); +} + +function getGroups() { + const requestOptions = { + method: 'GET', + headers: authHeader(), + credentials: 'include', + }; + return fetch(`${config.apiUrl}/v2/group_permissions`, requestOptions).then(handleResponse); +} + +function addUsersInGroups(body) { + const requestOptions = { + method: 'POST', + headers: authHeader(), + credentials: 'include', + body: JSON.stringify(body), + }; + return fetch(`${config.apiUrl}/v2/group_permissions/group-user`, requestOptions).then(handleResponse); +} + +function deleteUserFromGroup(id) { + const requestOptions = { + method: 'DELETE', + headers: authHeader(), + credentials: 'include', + }; + return fetch(`${config.apiUrl}/v2/group_permissions/group-user/${id}`, requestOptions).then(handleResponse); +} + +function createGranularPermission(body) { + const requestOptions = { + method: 'POST', + headers: authHeader(), + credentials: 'include', + body: JSON.stringify(body), + }; + return fetch(`${config.apiUrl}/v2/group_permissions/granular-permissions`, requestOptions).then(handleResponse); +} + +function updateGranularPermission(id, body) { + const requestOptions = { + method: 'PUT', + headers: authHeader(), + credentials: 'include', + body: JSON.stringify(body), + }; + return fetch(`${config.apiUrl}/v2/group_permissions/granular-permissions/update/${id}`, requestOptions).then( + handleResponse + ); +} + +function deleteGranularPermission(id) { + const requestOptions = { + method: 'DELETE', + headers: authHeader(), + credentials: 'include', + }; + return fetch(`${config.apiUrl}/v2/group_permissions/granular-permissions/${id}`, requestOptions).then(handleResponse); +} + +function fetchGranularPermissions(groupPermissionId) { + const requestOptions = { + method: 'GET', + headers: authHeader(), + credentials: 'include', + }; + return fetch(`${config.apiUrl}/v2/group_permissions/${groupPermissionId}/granular-permissions`, requestOptions).then( + handleResponse + ); +} + +function updateUserRole(body) { + const requestOptions = { + method: 'PUT', + headers: authHeader(), + credentials: 'include', + body: JSON.stringify(body), + }; + return fetch(`${config.apiUrl}/v2/group_permissions/user-role/edit`, requestOptions).then(handleResponse); +} + +function getUsersInGroup(groupPermissionId, searchInput = '') { + const requestOptions = { + method: 'GET', + headers: authHeader(), + credentials: 'include', + }; + return fetch( + `${config.apiUrl}/v2/group_permissions/${groupPermissionId}/group-user?input=${searchInput && searchInput?.trim()}`, + requestOptions + ).then(handleResponse); +} + +function getUsersNotInGroup(searchInput, groupPermissionId) { + const requestOptions = { + method: 'GET', + headers: authHeader(), + credentials: 'include', + }; + return fetch( + `${config.apiUrl}/v2/group_permissions/${groupPermissionId}/group-user/addable-users?input=${searchInput.trim()}`, + requestOptions + ).then(handleResponse); +} + +function duplicate(groupPermissionId, body) { + const requestOptions = { + method: 'POST', + headers: authHeader(), + credentials: 'include', + body: JSON.stringify(body), + }; + return fetch(`${config.apiUrl}/v2/group_permissions/${groupPermissionId}/duplicate`, requestOptions).then( + handleResponse + ); +} diff --git a/frontend/src/_services/index.js b/frontend/src/_services/index.js index ea9a1ab1e5..9925c306ec 100644 --- a/frontend/src/_services/index.js +++ b/frontend/src/_services/index.js @@ -22,3 +22,4 @@ export * from './globalDatasource.service'; export * from './app_environment.service'; export * from './copilot.service'; export * from './organization_constants.service'; +export * from './groupPermission.v2.service'; diff --git a/frontend/src/_styles/groups-permissions.scss b/frontend/src/_styles/groups-permissions.scss new file mode 100644 index 0000000000..456857bfd0 --- /dev/null +++ b/frontend/src/_styles/groups-permissions.scss @@ -0,0 +1,146 @@ +@import "./typography.scss"; +@import "./designtheme.scss"; + +.granular-access-container { + display: flex; + flex-direction: row; + height: 470px; + gap: 0; + + .manage-granular-permissions-info { + display: flex; + height: 48px; + width: 612px; + border-radius: 6px; + padding: 12px 24px 12px 24px; + background: var(--slate3); + border: 1px solid var(--slate5); + border-radius: 6px; + margin-bottom: 16px; + + p { + color: var(--slate12); + // gap: 14px; + display: flex; + align-items: center; + + } + } + + + .manage-granular-permission-header { + border-bottom: 1px solid var(--slate5); + display: flex; + p { + padding: 8px 12px; + // gap: 10px; + width: 230px; + height: 36px; + font-weight: 500; + color: var(--slate11) !important; + } + } + + .empty-container { + flex-shrink: 0; /* Prevent shrinking */ + height: 100%; /* Take full height of the parent container */ + display: flex; + align-items: center; /* Center items vertically */ + justify-content: center; /* Center items horizontally */ + text-align: center; + flex-direction: column; + width: 330px; + + .icon-container { + width: 55px; + height: 55px; + background: var(--indigo4); + border-radius: 6px; + + svg { + width: 45px; + height: 45px; + path { + fill: var(--indigo9); + } + } + } + + .add-permission-btn { + width: 135px; + } + } + + .permission-body-one { + flex-grow: 1; /* Allow this to grow and fill available space */ + overflow-y: auto; + border-bottom: 1px solid var(--slate5); + margin: 0; /* Ensure no margin */ + padding: 0; /* Ensure no padding */ + min-height: calc(100% - 48px - 16px - 55px - 70px); + max-height: calc(100% - 48px - 16px - 55px - 70px); + } + + .permission-body-two { + flex-grow: 1; /* Allow this to grow and fill available space */ + overflow-y: auto; + border-bottom: 1px solid var(--slate5); + margin: 0; /* Ensure no margin */ + padding: 0; /* Ensure no padding */ + min-height: calc(100% - 16px - 55px - 54px); + max-height: calc(100% - 16px - 55px - 54px); + } + + .side-button-cont { + display: flex; + justify-content: flex-end; + align-items: center; /* Ensure the content is centered vertically */ + height: 50px; + flex-shrink: 0; /* Prevent shrinking */ + margin: 0; /* Ensure no margin */ + padding: 12px; /* Ensure no padding */ + margin-bottom: 15px; + + .add-icon { + width: 135px; + height: 30px; + } + } +} + + +.permission-type { + border: 0px !important; + width: 100% !important; + justify-content: flex-start; + padding-left: 1rem; +} + +.permission-manager-modal { + .permission-manager-title { + display: flex; + align-items: center; + gap: 5px; + } + + .type-container { + display: flex; + justify-content: space-between; + + .right-container { + display: flex; + flex-direction: column; + } + } +} + + +.delete-icon-cont { + margin-left: 200px; + + .icon-class{ + border: none !important; + background-color: none !important; + + } +} \ No newline at end of file diff --git a/frontend/src/_styles/theme.scss b/frontend/src/_styles/theme.scss index 50a458d6df..e22a996df1 100644 --- a/frontend/src/_styles/theme.scss +++ b/frontend/src/_styles/theme.scss @@ -12,6 +12,7 @@ @import "./ui-operations.scss"; @import 'react-loading-skeleton/dist/skeleton.css'; @import './table-component.scss'; +@import './groups-permissions.scss'; @import 'tailwindcss/base'; @import 'tailwindcss/components'; @import 'tailwindcss/utilities'; @@ -8924,25 +8925,38 @@ tbody { padding: 16px; tbody { - - tr>td>span, - tr>td>a { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - max-width: 140px; + tr{ + td { + border-bottom-width: 0px !important; + display: flex; + align-items: center; + flex: 9%; + padding-left: 0px !important; + padding-right: 0px !important; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + &[data-name="role-header"] { + max-width: 98px !important; + } + span,a { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 140px; + } + } } } thead { tr { - padding: 0px 6px; + padding: 6px 0px 0px 6px; gap: 8px; width: 848px; height: 40px; display: flex; align-items: center; - margin-top: 6px; } tr>th { @@ -8950,6 +8964,10 @@ tbody { border-bottom: none !important; padding: 0 !important; width: 282px; + + &[data-name="role-header"] { + width:120px !important; + } } } @@ -8963,17 +8981,21 @@ tbody { gap: 8px; } - tr>td { - border-bottom-width: 0px !important; - display: flex; - align-items: center; - flex: 9%; - padding-left: 0px !important; - padding-right: 0px !important; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } + // tr>td { + // border-bottom-width: 0px !important; + // display: flex; + // align-items: center; + // flex: 9%; + // padding-left: 0px !important; + // padding-right: 0px !important; + // white-space: nowrap; + // overflow: hidden; + // text-overflow: ellipsis; + + // &[data-name="role-header"] { + // width:120px !important; + // } + // } } .user-actions-button { @@ -9440,15 +9462,21 @@ tbody { } .manage-groups-body { - padding: 24px; + padding: 12px 12px 10px 12px; font-size: 12px; - overflow-y: auto; + // overflow-y: auto; height: calc(100vh - 300px); + .group-users-list-container{ + height: calc(100vh - 300px - 100px); /* Set a fixed height */ + overflow-y: auto; /* Enable vertical scrolling */ + border-bottom: 1px solid var(--slate6) !important; + } + } .groups-sub-header-wrap { - width: 612px; + // width: 612px; height: 36px; border-bottom: 1px solid var(--slate5) !important; @@ -9618,14 +9646,15 @@ tbody { } .apps-permission-wrap { - height: 72px; + height: auto; justify-content: center; + width: auto; gap: 12px; } .apps-folder-permission-wrap, .apps--variable-permission-wrap { - height: 44px; + height: auto; } .manage-group-permision-header { @@ -9635,7 +9664,7 @@ tbody { p { padding: 8px 12px; gap: 10px; - width: 206px; + width: 230px; height: 36px; font-weight: 500; color: var(--slate11) !important; @@ -9682,13 +9711,19 @@ tbody { .default-group-wrap { gap: 10px; - width: 119px; - height: 28px; + width: 130px; + height: 32px; display: flex; align-items: center; justify-content: center; - background: var(--grass3); + background: var(--indigo3); border-radius: 100px; + border: 2px solid var(--indigo7); + color: var(--indigo9); + + path { + fill: var(--indigo9); + } } .sso-icon-wrapper { @@ -9759,6 +9794,9 @@ tbody { text-transform: capitalize; } + + + .manage-group-users-row { display: flex; flex-direction: row; @@ -9769,13 +9807,13 @@ tbody { border-bottom: 1px solid var(--slate5); p { - width: 272px; + width: 262px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; span { - max-width: 150px; + max-width: 140px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -9785,8 +9823,17 @@ tbody { &:hover .apps-remove-btn { display: flex; } + + .edit-role-btn{ + margin-left: auto; + margin-right: 20px; + display: flex; + align-items: center; + padding-top: 5px; + } } + .manage-group-app-table-body { width: 602px !important; @@ -9853,7 +9900,7 @@ tbody { border-bottom: 1px solid var(--slate5); width: 612px; height: 36px; - padding: 8px 12px; + padding: 8px 12px 8px 2px; align-items: center; @@ -9863,6 +9910,15 @@ tbody { font-weight: 500; } + .edit-role-btn{ + margin-left: auto; + margin-right: 50px; + display: flex; + width: 20px; + align-items: center; + padding-top: 5px; + } + } .manage-groups-permission-apps, @@ -9888,12 +9944,12 @@ tbody { .apps-variable-permission-wrap, .apps-constant-permission-wrap { gap: 10px; - height: 72px; + height: auto; } .apps-folder-permission-wrap, .apps-variable-permission-wrap { - height: 44px; + height: auto; border-bottom: 1px solid var(--slate5); } @@ -10800,6 +10856,12 @@ tbody { border-bottom-left-radius: 6px; border-bottom-right-radius: 6px; + + .user-detail{ + display: flex; + flex-direction: column; + } + } .user-filter-search { @@ -12784,7 +12846,7 @@ tbody { text-overflow: ellipsis; overflow: hidden; white-space: nowrap; - max-width: 185px; + max-width: 210px; } .group-chip { @@ -13170,6 +13232,27 @@ tbody { } } +.modal-base { + .modal-footer { + padding: 1rem; + + .tj-btn-left-icon { + svg { + width: 20px; + height: 20px; + + path { + fill: var(--indigo1); + } + } + } + + .tj-large-btn { + font-weight: 500; + font-size: 14px; + } + } +} .component-spinner { animation: l13 1s infinite linear; position: absolute; @@ -13183,7 +13266,6 @@ tbody { .widget-version-identifier { position: absolute; - right: 0px; top: 0px; border-radius: 0px 8px 0px 8px; height: 16px; diff --git a/frontend/src/_styles/widgets/multi-select.scss b/frontend/src/_styles/widgets/multi-select.scss index caa63dce8f..86ede8e00a 100644 --- a/frontend/src/_styles/widgets/multi-select.scss +++ b/frontend/src/_styles/widgets/multi-select.scss @@ -181,6 +181,10 @@ } } +.tj-ms-usergroup{ + width: auto; +} + .tj-ms-count { border-radius: 2px; display: flex; diff --git a/frontend/src/_ui/AppButton/AppButton.scss b/frontend/src/_ui/AppButton/AppButton.scss index 2fee7d996e..15320ad44e 100644 --- a/frontend/src/_ui/AppButton/AppButton.scss +++ b/frontend/src/_ui/AppButton/AppButton.scss @@ -1,7 +1,7 @@ @import "../../_styles/designtheme.scss"; .tj-base-btn { - box-sizing: border-box; + // box-sizing: border-box; display: flex; flex-direction: row; justify-content: center; diff --git a/frontend/src/_ui/Icon/solidIcons/GranularAccess.jsx b/frontend/src/_ui/Icon/solidIcons/GranularAccess.jsx new file mode 100644 index 0000000000..0c5a3b309b --- /dev/null +++ b/frontend/src/_ui/Icon/solidIcons/GranularAccess.jsx @@ -0,0 +1,19 @@ +import React from 'react'; + +const GranularAccess = ({ fill = '#C1C8CD', width = '17', className = '', viewBox = '0 0 17 17' }) => ( + + + +); + +export default GranularAccess; diff --git a/frontend/src/_ui/Icon/solidIcons/UserGear.jsx b/frontend/src/_ui/Icon/solidIcons/UserGear.jsx new file mode 100644 index 0000000000..e1a024cb5c --- /dev/null +++ b/frontend/src/_ui/Icon/solidIcons/UserGear.jsx @@ -0,0 +1,19 @@ +import React from 'react'; + +const UserGear = ({ fill = '#889096', width = '17', className = '', viewBox = '0 0 17 17' }) => ( + + + +); + +export default UserGear; diff --git a/frontend/src/_ui/Icon/solidIcons/UserGroups.jsx b/frontend/src/_ui/Icon/solidIcons/UserGroups.jsx new file mode 100644 index 0000000000..3b22d5ebba --- /dev/null +++ b/frontend/src/_ui/Icon/solidIcons/UserGroups.jsx @@ -0,0 +1,39 @@ +import React from 'react'; + +const UserGroup = ({ fill = '#889096', width = '15', className = '', viewBox = '0 0 15 15' }) => ( + + + + + + + + +); + +export default UserGroup; diff --git a/frontend/src/_ui/Icon/solidIcons/index.js b/frontend/src/_ui/Icon/solidIcons/index.js index 97a08d1e3a..45a5d1e040 100644 --- a/frontend/src/_ui/Icon/solidIcons/index.js +++ b/frontend/src/_ui/Icon/solidIcons/index.js @@ -167,6 +167,8 @@ import Open from './Open.jsx'; import TooljetIcon from './TooljetIcon.jsx'; import TriangleUpCenter from './TriangleUpCenter.jsx'; import TriangleDownCenter from './TriangleDownCenter.jsx'; +import UserGear from './UserGear.jsx'; +import GranularAccess from './GranularAccess.jsx'; import Search01 from './Search01.jsx'; import ShiftButtonIcon from './ShiftButtonIcon.jsx'; import Unpin01 from './Unpin01.jsx'; @@ -293,6 +295,8 @@ const Icon = (props) => { return ; case 'grid': return ; + case 'granularaccess': + return ; case 'helppolygon': return ; case 'home': @@ -445,6 +449,8 @@ const Icon = (props) => { return ; case 'usergroup': return ; + case 'usergear': + return ; case 'userremove': return ; case 'uturn': diff --git a/frontend/src/_ui/Layout/index.jsx b/frontend/src/_ui/Layout/index.jsx index 2acbaa32c3..6cc104f39c 100644 --- a/frontend/src/_ui/Layout/index.jsx +++ b/frontend/src/_ui/Layout/index.jsx @@ -47,10 +47,7 @@ function Layout({ }; const canCreateVariableOrConstant = () => { - return canAnyGroupPerformAction( - 'org_environment_variable_create', - authenticationService.currentSessionValue.group_permissions - ); + return authenticationService.currentSessionValue.user_permissions?.org_constant_c_r_u_d; }; return ( diff --git a/frontend/src/_ui/Modal/AppsSelect.jsx b/frontend/src/_ui/Modal/AppsSelect.jsx new file mode 100644 index 0000000000..4427a7298c --- /dev/null +++ b/frontend/src/_ui/Modal/AppsSelect.jsx @@ -0,0 +1,204 @@ +import { getWorkspaceId } from '@/_helpers/utils'; +import { ButtonSolid } from '@/_ui/AppButton/AppButton'; +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import Select, { components } from 'react-select'; +import { FilterPreview } from '@/_components'; +import './appSelect.theme.scss'; + +export function AppsSelect(props) { + const navigate = useNavigate(); + const workspaceId = getWorkspaceId(); + const darkMode = localStorage.getItem('darkMode') === 'true'; + + //Will be used when workspace routing settings have been merged + const Menu = (props) => { + return ( + + {props.children} +
+ navigate(`/${workspaceId}/workspace-settings`)} + iconCustomClass="rectangle-add-icon" + className="create-group" + fill="var(--indigo9)" + variant="secondary" + leftIcon="addrectangle" + > + Create new group + +
+
+ ); + }; + + const InputOption = ({ getStyles, Icon, isDisabled, isFocused, isSelected, children, innerProps, ...rest }) => { + const [isActive, setIsActive] = useState(false); + const onMouseDown = () => setIsActive(true); + const onMouseUp = () => setIsActive(false); + const onMouseLeave = () => setIsActive(false); + + const style = { + alignItems: 'center', + backgroundColor: 'transparent', + color: 'inherit', + display: 'flex ', + }; + + const props = { + ...innerProps, + onMouseDown, + onMouseUp, + onMouseLeave, + style, + }; + + return ( + + +
{children}
+
+ ); + }; + + const MultiValue = (props) => { + // Check if props.data exists and is not "all" + if (!props.data?.isAllField) { + return ( + +
{props.data.name}
+
+ ); + } + }; + + const selectStyles = { + indicatorSeparator: (base) => ({ + ...base, + display: 'none', + }), + option: (base) => ({ + ...base, + '.select-option': { + margin: '0px 10px', + }, + }), + multiValue: (base) => ({ + ...base, + borderRadius: '6px', + backgroundColor: 'var(--slate3)', + color: 'var(--slate11)', + '.selected-value': { + padding: '0px 6px 1px 3px', + color: 'var(--slate11)', + }, + }), + multiValueRemove: (base, state) => ({ + ...base, + '&:hover': { + backgroundColor: 'var(--tomato3)', + color: 'var(--tomato9)', + }, + paddingLeft: '0px', + ...(state.data.isFixed && { display: 'none' }), + }), + input: (base) => ({ + ...base, + input: { + height: '25px !important', + color: 'var(--slate11) !important', + }, + }), + control: (base) => ({ + ...base, + outline: 'none', + border: '1px solid var(--slate7)', + boxShadow: 'none', + borderRadius: '6px', + background: 'unset', + '&:hover': { + border: '1px solid var(--slate8)', + }, + }), + menuList: (base) => ({ + ...base, + maxHeight: '200px', + }), + menu: (base) => ({ + ...base, + background: 'var(--slate1)', + '.add-group-btn': { + display: 'flex', + justifyContent: 'flex-end', + padding: '8px', + borderTop: '1px solid var(--slate5)', + '.create-group': { + background: 'none !important', + '.rectangle-add-icon': { + width: '20px', + height: '20px', + }, + }, + }, + }), + }; + + return ( +