mirror of
https://github.com/ToolJet/ToolJet
synced 2026-05-23 08:58:26 +00:00
Merge pull request #10579 from ToolJet/ce-release-group-cve
Release [CVE + Group Permissions]
This commit is contained in:
commit
0b1ec0d7cd
486 changed files with 63488 additions and 87649 deletions
11
.env.example
11
.env.example
|
|
@ -48,6 +48,8 @@ GOOGLE_CLIENT_SECRET=
|
|||
|
||||
# EMAIL CONFIGURATION
|
||||
DEFAULT_FROM_EMAIL=hello@tooljet.io
|
||||
# Set this value true to get preview email on cevelopment env
|
||||
SMTP_DISABLED=
|
||||
SMTP_USERNAME=
|
||||
SMTP_PASSWORD=
|
||||
SMTP_DOMAIN=
|
||||
|
|
@ -81,3 +83,12 @@ ENABLE_ONBOARDING_QUESTIONS_FOR_ALL_SIGN_UPS=
|
|||
|
||||
#session expiry in minutes
|
||||
USER_SESSION_EXPIRY=
|
||||
|
||||
#Disable app embed feature, if true then private and public app embed is not allowed
|
||||
DISABLE_APP_EMBED=
|
||||
|
||||
# if true then private app embed is allowed
|
||||
ENABLE_PRIVATE_APP_EMBED=
|
||||
|
||||
#Enable cors else restricted to TOOLJET_HOST. Set the value true if you are serving front end from diffrent host
|
||||
ENABLE_CORS=
|
||||
|
|
|
|||
2
.version
2
.version
|
|
@ -1 +1 @@
|
|||
2.66.2
|
||||
2.67.0
|
||||
|
|
|
|||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
|
|
@ -23,5 +23,6 @@
|
|||
],
|
||||
"url": "https://raw.githubusercontent.com/ToolJet/ToolJet/develop/plugins/schemas/manifest.schema.json"
|
||||
}
|
||||
]
|
||||
],
|
||||
"CodeGPT.apiKey": "CodeGPT Plus Beta"
|
||||
}
|
||||
22
CODEOWNERS
Normal file
22
CODEOWNERS
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# Code owners for specific package.json and package-lock.json files
|
||||
/server/package.json @shah21 @gsmithun4 @adishm98
|
||||
/server/package-lock.json @shah21 @gsmithun4 @adishm98
|
||||
|
||||
/frontend/package.json @shah21 @gsmithun4 @adishm98
|
||||
/frontend/package-lock.json @shah21 @gsmithun4 @adishm98
|
||||
|
||||
/marketplace/package.json @shah21 @gsmithun4 @adishm98
|
||||
/marketplace/package-lock.json @shah21 @gsmithun4 @adishm98
|
||||
|
||||
/cypress/package.json @shah21 @gsmithun4 @adishm98
|
||||
/cypress/package-lock.json @shah21 @gsmithun4 @adishm98
|
||||
|
||||
/plugins/package.json @shah21 @gsmithun4 @adishm98
|
||||
/plugins/package-lock.json @shah21 @gsmithun4 @adishm98
|
||||
|
||||
/package.json @shah21 @gsmithun4 @adishm98
|
||||
/package-lock.json @shah21 @gsmithun4 @adishm98
|
||||
|
||||
# Server service files
|
||||
/server/src/services/email.service.ts @shah21 @gsmithun4
|
||||
/server/src/mails @shah21 @gsmithun4
|
||||
9621
cli/package-lock.json
generated
9621
cli/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -31,7 +31,7 @@
|
|||
"eslint-plugin-prettier": "^3.4.1",
|
||||
"globby": "^11",
|
||||
"mocha": "^9",
|
||||
"oclif": "^2.0.0-main.10",
|
||||
"oclif": "^4.14.14",
|
||||
"shx": "^0.3.3",
|
||||
"ts-node": "^10.2.1",
|
||||
"tslib": "^2.3.1",
|
||||
|
|
@ -79,4 +79,4 @@
|
|||
"version": "oclif readme && git add README.md"
|
||||
},
|
||||
"types": "dist/index.d.ts"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ module.exports = defineConfig({
|
|||
experimentalModfyObstructiveThirdPartyCode: true,
|
||||
experimentalRunAllSpecs: true,
|
||||
baseUrl: "http://localhost:8082",
|
||||
specPattern: "cypress/e2e/**/*.cy.js",
|
||||
specPattern: "cypress/e2e/happyPath/platform/commonTestcases/**/*.cy.js",
|
||||
downloadsFolder: "cypress/downloads",
|
||||
numTestsKeptInMemory: 0,
|
||||
redirectionLimit: 10,
|
||||
|
|
@ -93,5 +93,6 @@ module.exports = defineConfig({
|
|||
codeCoverageTasksRegistered: true,
|
||||
video: false,
|
||||
videoUploadOnPasses: false,
|
||||
experimentalStudio: true,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -189,6 +189,7 @@ Cypress.Commands.add("userInviteApi", (userName, userEmail) => {
|
|||
first_name: userName,
|
||||
email: userEmail,
|
||||
groups: [],
|
||||
role: "end-user",
|
||||
},
|
||||
},
|
||||
{ log: false }
|
||||
|
|
|
|||
|
|
@ -373,9 +373,9 @@ export const commonWidgetSelector = {
|
|||
modalCloseButton: '[data-cy="modal-close-button"]',
|
||||
iframeLinkLabel: '[data-cy="iframe-link-label"]',
|
||||
ifameLinkCopyButton: '[data-cy="iframe-link-copy-button"]',
|
||||
appSlugLabel: '[data-cy="app-slug-label"]',
|
||||
appSlugLabel: '[data-cy="input-field-label"]',
|
||||
appSlugInput: '[data-cy="app-slug-input-field"]',
|
||||
appSlugInfoLabel: '[data-cy="app-slug-info-label"]',
|
||||
appSlugInfoLabel: '[data-cy="helper-text"]',
|
||||
appLinkLabel: '[data-cy="app-link-label"]',
|
||||
appLinkField: '[data-cy="app-link-field"]',
|
||||
appSlugErrorLabel: '[data-cy="app-slug-error-label"]',
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ export const groupsSelector = {
|
|||
createNewGroupButton: "[data-cy=create-new-group-button]",
|
||||
tableHeader: "[data-cy=table-header]",
|
||||
groupName: "[data-cy=group-name]",
|
||||
addNewGroupModalTitle: '[data-cy="add-new-group-title"]',
|
||||
addNewGroupModalTitle: '[data-cy="create-new-group-title"]',
|
||||
groupNameInput: "[data-cy=group-name-input]",
|
||||
cancelButton: "[data-cy=cancel-button]",
|
||||
createGroupButton: "[data-cy=create-group-button]",
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ export const usersSelector = {
|
|||
buttonAddUsers: "[data-cy=button-invite-new-user]",
|
||||
usersElements: {
|
||||
usersTableNameColumnHeader: '[data-cy="users-table-name-column-header"]',
|
||||
usersTableEmailColumnHeader: '[data-cy="users-table-email-column-header"]',
|
||||
usersTableRolesColumnHeader: '[data-cy="users-table-roles-column-header"]',
|
||||
usersTableGroupsColumnHeader: '[data-cy="users-table-groups-column-header"]',
|
||||
usersTableStatusColumnHeader:
|
||||
'[data-cy="users-table-status-column-header"]',
|
||||
usersFilterLabel: '[data-cy="users-filter-label"]',
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
export const usersText = {
|
||||
usersElements: {
|
||||
usersTableNameColumnHeader: "NAME",
|
||||
usersTableEmailColumnHeader: "EMAIL",
|
||||
usersTableRolesColumnHeader: "User role",
|
||||
usersTableGroupsColumnHeader: "Custom groups",
|
||||
usersTableStatusColumnHeader: "STATUS",
|
||||
usersFilterLabel: "Showing",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -50,20 +50,22 @@ describe("App slug", () => {
|
|||
"have.text",
|
||||
"App slug can't be empty"
|
||||
);
|
||||
|
||||
cy.clearAndType(commonWidgetSelector.appSlugInput, "_2#");
|
||||
cy.wait(500)
|
||||
cy.get(commonWidgetSelector.appSlugErrorLabel).verifyVisibleElement(
|
||||
"have.text",
|
||||
"Special characters are not accepted."
|
||||
);
|
||||
|
||||
cy.clearAndType(commonWidgetSelector.appSlugInput, "t ");
|
||||
cy.wait(500)
|
||||
cy.get(commonWidgetSelector.appSlugErrorLabel).verifyVisibleElement(
|
||||
"have.text",
|
||||
"Cannot contain spaces"
|
||||
);
|
||||
|
||||
cy.clearAndType(commonWidgetSelector.appSlugInput, "T");
|
||||
cy.wait(500)
|
||||
cy.get(commonWidgetSelector.appSlugErrorLabel).verifyVisibleElement(
|
||||
"have.text",
|
||||
"Only lowercase letters are accepted."
|
||||
|
|
@ -71,7 +73,8 @@ describe("App slug", () => {
|
|||
|
||||
cy.get(commonWidgetSelector.appSlugInput).clear();
|
||||
cy.clearAndType(commonWidgetSelector.appSlugInput, data.slug);
|
||||
cy.get(commonWidgetSelector.appSlugErrorLabel).verifyVisibleElement(
|
||||
cy.wait(500)
|
||||
cy.get('[data-cy="app-slug-accepted-label"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Slug accepted!"
|
||||
);
|
||||
|
|
@ -112,6 +115,7 @@ describe("App slug", () => {
|
|||
cy.get(commonSelectors.leftSideBarSettingsButton).click();
|
||||
cy.get(commonWidgetSelector.appSlugInput).clear();
|
||||
cy.clearAndType(commonWidgetSelector.appSlugInput, data.slug);
|
||||
cy.wait(500)
|
||||
cy.get(commonWidgetSelector.appSlugErrorLabel).verifyVisibleElement(
|
||||
"have.text",
|
||||
"This app slug is already taken."
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ describe(
|
|||
const data = {};
|
||||
beforeEach(() => {
|
||||
cy.defaultWorkspaceLogin();
|
||||
cy.removeAssignedApps();
|
||||
// cy.removeAssignedApps();
|
||||
cy.skipWalkthrough();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -12,10 +12,8 @@ const newGroupname = `New ${groupName}`;
|
|||
describe("Manage Groups", () => {
|
||||
beforeEach(() => {
|
||||
cy.defaultWorkspaceLogin();
|
||||
permissions.reset("all_users");
|
||||
});
|
||||
it("Should verify the elements and functionalities on manage groups page", () => {
|
||||
cy.removeAssignedApps();
|
||||
common.navigateToManageGroups();
|
||||
cy.get(commonSelectors.breadcrumbTitle).should(($el) => {
|
||||
expect($el.contents().first().text().trim()).to.eq("Workspace settings");
|
||||
|
|
@ -26,6 +24,21 @@ describe("Manage Groups", () => {
|
|||
);
|
||||
|
||||
groups.manageGroupsElements();
|
||||
cy.get(groupsSelector.createNewGroupButton).should("be.visible").click();
|
||||
cy.get(groupsSelector.addNewGroupModalTitle).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.cardTitle
|
||||
);
|
||||
cy.get(groupsSelector.groupNameInput).should("be.visible");
|
||||
cy.get(groupsSelector.cancelButton).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.cancelButton
|
||||
);
|
||||
cy.get(groupsSelector.createGroupButton).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.createGroupButton
|
||||
);
|
||||
cy.get(groupsSelector.cancelButton).click();
|
||||
|
||||
cy.get(groupsSelector.createNewGroupButton).click();
|
||||
cy.clearAndType(groupsSelector.groupNameInput, groupsText.admin);
|
||||
|
|
@ -53,24 +66,16 @@ describe("Manage Groups", () => {
|
|||
|
||||
cy.get(groupsSelector.groupPageTitle(groupName)).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupName
|
||||
`${groupName} (0)`
|
||||
);
|
||||
|
||||
cy.get('[data-cy="group-name-update-link"]').should("be.visible");
|
||||
groups.OpenGroupCardOption(groupName);
|
||||
cy.get(groupsSelector.updateGroupNameLink(groupName)).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.editGroupNameButton
|
||||
);
|
||||
|
||||
cy.get(groupsSelector.deleteGroupOption).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.deleteGroupButton
|
||||
);
|
||||
|
||||
cy.get(groupsSelector.appsLink).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.appsLink
|
||||
);
|
||||
cy.get(groupsSelector.usersLink).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.usersLink
|
||||
|
|
@ -79,30 +84,10 @@ describe("Manage Groups", () => {
|
|||
"have.text",
|
||||
groupsText.permissionsLink
|
||||
);
|
||||
|
||||
cy.get(groupsSelector.appsLink).click();
|
||||
|
||||
cy.get(groupsSelector.searchBox).should("be.visible");
|
||||
cy.get(groupsSelector.selectAddButton).verifyVisibleElement(
|
||||
cy.get('[data-cy="granular-access-link"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.addButton
|
||||
"Granular access"
|
||||
);
|
||||
|
||||
cy.get(groupsSelector.nameTableHeader).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.textAppName
|
||||
);
|
||||
|
||||
cy.get(groupsSelector.permissionstableHedaer).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.permissionstableHedaer
|
||||
);
|
||||
cy.get(groupsSelector.helperTextNoAppsAdded)
|
||||
.eq(0)
|
||||
.verifyVisibleElement("have.text", groupsText.helperTextNoAppsAdded);
|
||||
|
||||
cy.get(groupsSelector.searchBox).should("be.visible");
|
||||
|
||||
cy.get(groupsSelector.usersLink).click();
|
||||
cy.get(groupsSelector.nameTableHeader).verifyVisibleElement(
|
||||
"have.text",
|
||||
|
|
@ -112,6 +97,15 @@ describe("Manage Groups", () => {
|
|||
"have.text",
|
||||
groupsText.emailTableHeader
|
||||
);
|
||||
cy.get('[data-cy="user-empty-page-icon"]').should("be.visible");
|
||||
cy.get('[data-cy="user-empty-page"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"No users added yet"
|
||||
);
|
||||
cy.get('[data-cy="user-empty-page-info-text"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Add users to this group to configure permissions for them!"
|
||||
);
|
||||
|
||||
cy.get(groupsSelector.permissionsLink).click();
|
||||
cy.get(groupsSelector.resourcesApps).verifyVisibleElement(
|
||||
|
|
@ -127,7 +121,13 @@ describe("Manage Groups", () => {
|
|||
"have.text",
|
||||
groupsText.resourcesApps
|
||||
);
|
||||
cy.get(groupsSelector.appsCreateCheck).should("be.visible").check();
|
||||
cy.get(groupsSelector.appsCreateCheck).should("be.visible");
|
||||
cy.get(groupsSelector.appsCreateCheck).check();
|
||||
cy.verifyToastMessage(
|
||||
commonSelectors.toastMessage,
|
||||
groupsText.permissionUpdatedToast
|
||||
);
|
||||
cy.get(groupsSelector.appsCreateCheck).uncheck();
|
||||
cy.verifyToastMessage(
|
||||
commonSelectors.toastMessage,
|
||||
groupsText.permissionUpdatedToast
|
||||
|
|
@ -136,37 +136,277 @@ describe("Manage Groups", () => {
|
|||
"have.text",
|
||||
groupsText.createLabel
|
||||
);
|
||||
cy.get(groupsSelector.appsCreateCheck).uncheck();
|
||||
cy.get(groupsSelector.appsDeleteCheck).should("be.visible").check();
|
||||
cy.get('[data-cy="app-create-helper-text"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Create apps in this workspace"
|
||||
);
|
||||
cy.get(groupsSelector.appsDeleteCheck).should("be.visible");
|
||||
cy.get(groupsSelector.appsDeleteLabel).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.deleteLabel
|
||||
);
|
||||
cy.get('[data-cy="app-delete-helper-text"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Delete any app in this workspace"
|
||||
);
|
||||
|
||||
cy.get(groupsSelector.appsDeleteCheck).check();
|
||||
cy.verifyToastMessage(
|
||||
commonSelectors.toastMessage,
|
||||
groupsText.permissionUpdatedToast
|
||||
);
|
||||
cy.get(groupsSelector.appsDeleteCheck).uncheck();
|
||||
cy.verifyToastMessage(
|
||||
commonSelectors.toastMessage,
|
||||
groupsText.permissionUpdatedToast
|
||||
);
|
||||
|
||||
cy.get(groupsSelector.resourcesFolders).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.resourcesFolders
|
||||
);
|
||||
cy.get(groupsSelector.foldersCreateCheck).should("be.visible").check();
|
||||
cy.get(groupsSelector.foldersCreateCheck).should("be.visible");
|
||||
|
||||
cy.get(groupsSelector.foldersCreateLabel).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.folderCreateLabel
|
||||
);
|
||||
cy.get('[data-cy="folder-helper-text"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"All operations on folders"
|
||||
);
|
||||
cy.get(groupsSelector.foldersCreateCheck).check();
|
||||
cy.verifyToastMessage(
|
||||
commonSelectors.toastMessage,
|
||||
groupsText.permissionUpdatedToast
|
||||
);
|
||||
cy.get(groupsSelector.foldersCreateCheck).uncheck();
|
||||
cy.verifyToastMessage(
|
||||
commonSelectors.toastMessage,
|
||||
groupsText.permissionUpdatedToast
|
||||
);
|
||||
|
||||
cy.get(groupsSelector.resourcesWorkspaceVar).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.resourcesWorkspaceVar
|
||||
);
|
||||
cy.get(groupsSelector.workspaceVarCheckbox).should("be.visible");
|
||||
cy.get('[data-cy="workspace-constants-helper-text"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"All operations on workspace constants"
|
||||
);
|
||||
cy.get(groupsSelector.workspaceVarCheckbox).check();
|
||||
cy.verifyToastMessage(
|
||||
commonSelectors.toastMessage,
|
||||
groupsText.permissionUpdatedToast
|
||||
);
|
||||
cy.get(groupsSelector.workspaceVarCheckbox).uncheck();
|
||||
cy.verifyToastMessage(
|
||||
commonSelectors.toastMessage,
|
||||
groupsText.permissionUpdatedToast
|
||||
);
|
||||
|
||||
cy.get(groupsSelector.updateGroupNameLink(groupName)).click();
|
||||
cy.get('[data-cy="granular-access-link"]').click();
|
||||
|
||||
cy.get('[data-cy="add-apps-buton"]').click();
|
||||
|
||||
cy.get('[data-cy="modal-title"]:eq(2)').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Add app permissions"
|
||||
);
|
||||
cy.get('[data-cy="modal-close-button"]').should("be.visible").click();
|
||||
cy.get('[data-cy="add-apps-buton"]').click();
|
||||
cy.get('[data-cy="modal-title"]:eq(2)').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Add app permissions"
|
||||
);
|
||||
|
||||
cy.get('[data-cy="permission-name-label"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Permission name"
|
||||
);
|
||||
cy.get('[data-cy="permission-name-input"]')
|
||||
.should("be.visible")
|
||||
.and("have.attr", "placeholder", "Eg. Product analytics apps");
|
||||
cy.get('[data-cy="permission-name-help-text"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Permission name must be unique and max 50 characters"
|
||||
);
|
||||
|
||||
cy.get('[data-cy="permission-label"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Permission"
|
||||
);
|
||||
cy.get('[data-cy="edit-permission-radio"]').should("be.visible");
|
||||
cy.get('[data-cy="edit-permission-label"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Edit"
|
||||
);
|
||||
cy.get('[data-cy="edit-permission-info-text"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Access to app builder"
|
||||
);
|
||||
|
||||
cy.get('[data-cy="view-permission-radio"]')
|
||||
.should("be.visible")
|
||||
.and("be.checked");
|
||||
cy.get('[data-cy="view-permission-label"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"View"
|
||||
);
|
||||
cy.get('[data-cy="view-permission-info-text"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Only access released version of apps"
|
||||
);
|
||||
|
||||
cy.get('[data-cy="hide-from-dashboard-permission-input"]').should(
|
||||
"be.visible"
|
||||
);
|
||||
cy.get(
|
||||
'[data-cy="hide-from-dashboard-permission-label"]'
|
||||
).verifyVisibleElement("have.text", "Hide from dashboard");
|
||||
cy.get(
|
||||
'[data-cy="hide-from-dashboard-permission-info-text"]'
|
||||
).verifyVisibleElement("have.text", "App will be accessible by URL only");
|
||||
|
||||
cy.get('[data-cy="resource-label"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Resources"
|
||||
);
|
||||
cy.get('[data-cy="all-apps-radio"]').should("be.visible").and("be.checked");
|
||||
cy.get('[data-cy="all-apps-label"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"All apps"
|
||||
);
|
||||
cy.get('[data-cy="all-apps-info-text"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"This will select all apps in the workspace including any new apps created"
|
||||
);
|
||||
|
||||
cy.get('[ data-cy="custom-radio"]').should("be.visible");
|
||||
cy.get('[data-cy="custom-label"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Custom"
|
||||
);
|
||||
cy.get('[data-cy="custom-info-text"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Select specific applications you want to add to the group"
|
||||
);
|
||||
cy.get('[data-cy="resources-container"]>>>>').should("be.visible");
|
||||
cy.get('[data-cy="confim-button"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Add"
|
||||
);
|
||||
cy.get('[data-cy="confim-button"]').should('be.disabled')
|
||||
cy.get('[data-cy="cancel-button"]')
|
||||
.verifyVisibleElement("have.text", "Cancel")
|
||||
.click();
|
||||
|
||||
cy.get('[data-cy="add-apps-buton"]').click();
|
||||
|
||||
cy.clearAndType('[data-cy="permission-name-input"]', groupName)
|
||||
cy.get('[data-cy="confim-button"]').click()
|
||||
cy.get(`[data-cy="${groupName.toLowerCase()}-text"]`).click();
|
||||
|
||||
cy.get('[data-cy="modal-title"]:eq(2)').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Edit app permissions"
|
||||
);
|
||||
cy.get('[data-cy="delete-button"]').should('be.visible');
|
||||
cy.get('[data-cy="modal-close-button"]').should("be.visible").click();
|
||||
cy.get(`[data-cy="${groupName.toLowerCase()}-text"]`).click();
|
||||
cy.get('[data-cy="modal-title"]:eq(2)').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Edit app permissions"
|
||||
);
|
||||
|
||||
cy.get('[data-cy="permission-name-label"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Permission name"
|
||||
);
|
||||
cy.get('[data-cy="permission-name-input"]')
|
||||
.should("be.visible")
|
||||
.and("have.value", groupName);
|
||||
cy.get('[data-cy="permission-name-help-text"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Permission name must be unique and max 50 characters"
|
||||
);
|
||||
|
||||
cy.get('[data-cy="permission-label"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Permission"
|
||||
);
|
||||
cy.get('[data-cy="edit-permission-radio"]').should("be.visible").check();
|
||||
cy.get('[data-cy="edit-permission-label"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Edit"
|
||||
);
|
||||
cy.get('[data-cy="edit-permission-info-text"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Access to app builder"
|
||||
);
|
||||
|
||||
cy.get('[data-cy="view-permission-radio"]')
|
||||
.should("be.visible")
|
||||
.and("not.be.checked");
|
||||
cy.get('[data-cy="view-permission-label"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"View"
|
||||
);
|
||||
cy.get('[data-cy="view-permission-info-text"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Only access released version of apps"
|
||||
);
|
||||
|
||||
cy.get('[data-cy="hide-from-dashboard-permission-input"]').should(
|
||||
"be.visible"
|
||||
);
|
||||
cy.get(
|
||||
'[data-cy="hide-from-dashboard-permission-label"]'
|
||||
).verifyVisibleElement("have.text", "Hide from dashboard");
|
||||
cy.get(
|
||||
'[data-cy="hide-from-dashboard-permission-info-text"]'
|
||||
).verifyVisibleElement("have.text", "App will be accessible by URL only");
|
||||
|
||||
cy.get('[data-cy="resource-label"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Resources"
|
||||
);
|
||||
cy.get('[data-cy="all-apps-radio"]').should("be.visible").and("be.checked");
|
||||
cy.get('[data-cy="all-apps-label"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"All apps"
|
||||
);
|
||||
cy.get('[data-cy="all-apps-info-text"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"This will select all apps in the workspace including any new apps created"
|
||||
);
|
||||
|
||||
cy.get('[ data-cy="custom-radio"]').should("be.visible");
|
||||
cy.get('[data-cy="custom-label"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Custom"
|
||||
);
|
||||
cy.get('[data-cy="custom-info-text"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Select specific applications you want to add to the group"
|
||||
);
|
||||
cy.get('[data-cy="resources-container"]>>>>').should("be.visible");
|
||||
cy.get('[data-cy="confim-button"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Update"
|
||||
);
|
||||
cy.get('[data-cy="confim-button"]').should('be.enabled')
|
||||
cy.get('[data-cy="cancel-button"]')
|
||||
.verifyVisibleElement("have.text", "Cancel")
|
||||
.click();
|
||||
|
||||
cy.get(`[data-cy="${groupName.toLowerCase()}-text"]`).click();
|
||||
cy.clearAndType('[data-cy="permission-name-input"]', groupName)
|
||||
cy.get('[data-cy="edit-permission-radio"]').check();
|
||||
cy.get('[data-cy="confim-button"]').click()
|
||||
|
||||
cy.get('[data-cy="group-name-update-link"]').click();
|
||||
cy.get(groupsSelector.updateGroupNameModalTitle).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.updateGroupNameModalTitle
|
||||
|
|
@ -182,7 +422,7 @@ describe("Manage Groups", () => {
|
|||
);
|
||||
cy.get(groupsSelector.cancelButton).click();
|
||||
|
||||
cy.get(groupsSelector.updateGroupNameLink(groupName)).click();
|
||||
cy.get('[data-cy="group-name-update-link"]').click();
|
||||
|
||||
cy.clearAndType(groupsSelector.groupNameInput, newGroupname);
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
confirmInviteElements,
|
||||
selectUserGroup,
|
||||
inviteUserWithUserGroups,
|
||||
inviteUserWithUserRole,
|
||||
fetchAndVisitInviteLink,
|
||||
} from "Support/utils/manageUsers";
|
||||
import {
|
||||
|
|
@ -23,7 +24,6 @@ import { addNewUser, visitWorkspaceInvitation } from "Support/utils/onboarding";
|
|||
import { commonText } from "Texts/common";
|
||||
|
||||
const data = {};
|
||||
data.groupName = fake.firstName.replaceAll("[^A-Za-z]", "");
|
||||
|
||||
describe("Manage Users", () => {
|
||||
beforeEach(() => {
|
||||
|
|
@ -45,31 +45,17 @@ describe("Manage Users", () => {
|
|||
cy.get(usersSelector.usersPageTitle).should("be.visible");
|
||||
cy.get(usersSelector.buttonAddUsers).click();
|
||||
|
||||
cy.get(usersSelector.buttonInviteUsers).click();
|
||||
cy.get(usersSelector.fullNameError).verifyVisibleElement(
|
||||
"have.text",
|
||||
usersText.errorTextFieldRequired
|
||||
);
|
||||
cy.get(usersSelector.emailError).verifyVisibleElement(
|
||||
"have.text",
|
||||
usersText.errorTextFieldRequired
|
||||
);
|
||||
cy.get(usersSelector.buttonInviteUsers).should('be.disabled');
|
||||
|
||||
|
||||
cy.clearAndType(commonSelectors.inputFieldFullName, data.firstName);
|
||||
cy.clearAndType(commonSelectors.inputFieldEmailAddress, data.email);
|
||||
cy.get(commonSelectors.inputFieldEmailAddress).clear();
|
||||
cy.get(usersSelector.buttonInviteUsers).click();
|
||||
cy.get(usersSelector.emailError).verifyVisibleElement(
|
||||
"have.text",
|
||||
usersText.errorTextFieldRequired
|
||||
);
|
||||
|
||||
cy.get(commonSelectors.inputFieldFullName).clear();
|
||||
cy.clearAndType(commonSelectors.inputFieldEmailAddress, data.email);
|
||||
cy.get(usersSelector.buttonInviteUsers).click();
|
||||
cy.get(usersSelector.fullNameError).verifyVisibleElement(
|
||||
"have.text",
|
||||
usersText.errorTextFieldRequired
|
||||
"Email is not valid"
|
||||
);
|
||||
cy.get(usersSelector.buttonInviteUsers).should('be.disabled');
|
||||
|
||||
cy.clearAndType(commonSelectors.inputFieldFullName, data.firstName);
|
||||
cy.clearAndType(
|
||||
|
|
@ -78,16 +64,18 @@ describe("Manage Users", () => {
|
|||
);
|
||||
cy.get(usersSelector.buttonInviteUsers).click();
|
||||
|
||||
cy.get(commonSelectors.newToastMessage).should(
|
||||
"have.text",
|
||||
usersText.exsitingEmail
|
||||
);
|
||||
cy.get('[data-cy="modal-icon"]').should('be.visible')
|
||||
cy.get('[data-cy="modal-header"]').verifyVisibleElement("have.text", "Duplicate email");
|
||||
cy.get(commonSelectors.modalMessage).verifyVisibleElement("have.text", "Duplicate email found. Please provide a unique email address.")
|
||||
cy.get('[data-cy="close-button"]:eq(1)').should('be.visible').click();
|
||||
cy.get(commonSelectors.inputFieldEmailAddress).should("have.value", usersText.adminUserEmail)
|
||||
|
||||
});
|
||||
|
||||
it("Should verify the confirm invite page and new user account", () => {
|
||||
data.firstName = fake.firstName;
|
||||
data.email = fake.email.toLowerCase().replaceAll("[^A-Za-z]", "");
|
||||
cy.removeAssignedApps();
|
||||
// cy.removeAssignedApps();
|
||||
|
||||
navigateToManageUsers();
|
||||
fillUserInviteForm(data.firstName, data.email);
|
||||
|
|
@ -208,6 +196,10 @@ describe("Manage Users", () => {
|
|||
it("Should verify the user onboarding with groups", () => {
|
||||
data.firstName = fake.firstName;
|
||||
data.email = fake.email.toLowerCase().replaceAll("[^A-Za-z]", "");
|
||||
data.groupName1 = fake.firstName.replaceAll("[^A-Za-z]", "");
|
||||
data.groupName2 = fake.firstName.replaceAll("[^A-Za-z]", "");
|
||||
|
||||
|
||||
const groupNames = ["All users", "Admin"];
|
||||
|
||||
navigateToManageUsers();
|
||||
|
|
@ -233,13 +225,10 @@ describe("Manage Users", () => {
|
|||
cy.get(commonSelectors.cancelButton).click();
|
||||
|
||||
cy.get(usersSelector.buttonAddUsers).click();
|
||||
cy.get(".css-1jqq78o-placeholder").should(
|
||||
"have.text",
|
||||
"Select groups to add for this user"
|
||||
);
|
||||
cy.get('.selected-value').should('have.text', "End-user")
|
||||
cy.get(commonSelectors.cancelButton).click();
|
||||
|
||||
inviteUserWithUserGroups(data.firstName, data.email, "All users", "Admin");
|
||||
inviteUserWithUserRole(data.firstName, data.email, "Admin");
|
||||
|
||||
navigateToManageGroups();
|
||||
cy.get(groupsSelector.groupLink("Admin")).click();
|
||||
|
|
@ -250,25 +239,31 @@ describe("Manage Users", () => {
|
|||
data.email = fake.email.toLowerCase().replaceAll("[^A-Za-z]", "");
|
||||
|
||||
cy.get(groupsSelector.createNewGroupButton).click();
|
||||
cy.clearAndType(groupsSelector.groupNameInput, data.groupName);
|
||||
cy.clearAndType(groupsSelector.groupNameInput, data.groupName1);
|
||||
cy.get(groupsSelector.createGroupButton).click();
|
||||
cy.verifyToastMessage(
|
||||
commonSelectors.toastMessage,
|
||||
groupsText.groupCreatedToast
|
||||
);
|
||||
cy.get(groupsSelector.createNewGroupButton).click();
|
||||
cy.clearAndType(groupsSelector.groupNameInput, data.groupName2);
|
||||
cy.get(groupsSelector.createGroupButton).click();
|
||||
|
||||
navigateToManageUsers();
|
||||
inviteUserWithUserGroups(
|
||||
data.firstName,
|
||||
data.email,
|
||||
"All users",
|
||||
data.groupName
|
||||
data.groupName1,
|
||||
data.groupName2
|
||||
);
|
||||
logout();
|
||||
|
||||
cy.defaultWorkspaceLogin();
|
||||
navigateToManageGroups();
|
||||
cy.get(groupsSelector.groupLink(data.groupName)).click();
|
||||
cy.get(groupsSelector.groupLink(data.groupName1)).click();
|
||||
cy.get(groupsSelector.usersLink).click();
|
||||
cy.get(groupsSelector.userRow(data.email)).should("be.visible");
|
||||
cy.get(groupsSelector.groupLink(data.groupName2)).click();
|
||||
cy.get(groupsSelector.usersLink).click();
|
||||
cy.get(groupsSelector.userRow(data.email)).should("be.visible");
|
||||
});
|
||||
|
|
@ -341,13 +336,55 @@ describe("Manage Users", () => {
|
|||
cy.get('[data-cy="group-check-input"]').eq(0).check();
|
||||
|
||||
cy.get(usersSelector.buttonInviteUsers).click();
|
||||
|
||||
cy.get('[data-cy="modal-title"] > .tj-text-md').verifyVisibleElement("have.text", "Edit user role")
|
||||
cy.get('[data-cy="user-email"]').verifyVisibleElement("have.text", data.email);
|
||||
cy.get('[data-cy="modal-body"]>').verifyVisibleElement("have.text", "Are you sure you want to continue?");
|
||||
cy.get('.modal-footer > [data-cy="cancel-button"]').verifyVisibleElement("have.text", "Cancel");
|
||||
cy.get('[data-cy="confim-button"]').verifyVisibleElement("have.text", "Continue");
|
||||
cy.get('[data-cy="modal-close-button"]').should('be.visible').click();
|
||||
|
||||
cy.get(usersSelector.userActionButton).click();
|
||||
cy.get(usersSelector.editUserDetailsButton).click();
|
||||
cy.get('[data-cy="user-group-select"]>>>>>').eq(0).type("Admin");
|
||||
cy.wait(1000);
|
||||
cy.get('[data-cy="group-check-input"]').eq(0).check();
|
||||
|
||||
cy.get(commonSelectors.cancelButton).click();
|
||||
|
||||
cy.get(usersSelector.userActionButton).click();
|
||||
cy.get(usersSelector.editUserDetailsButton).click();
|
||||
cy.get('[data-cy="user-group-select"]>>>>>').eq(0).type("Admin");
|
||||
cy.wait(1000);
|
||||
cy.get('[data-cy="group-check-input"]').eq(0).check();
|
||||
|
||||
cy.get(usersSelector.buttonInviteUsers).click();
|
||||
cy.get('.modal-footer > [data-cy="cancel-button"]').click()
|
||||
|
||||
cy.get(usersSelector.userActionButton).click();
|
||||
cy.get(usersSelector.editUserDetailsButton).click();
|
||||
cy.get('[data-cy="user-group-select"]>>>>>').eq(0).type("Admin");
|
||||
cy.wait(1000);
|
||||
cy.get('[data-cy="group-check-input"]').eq(0).check();
|
||||
|
||||
cy.get(commonSelectors.cancelButton).click();
|
||||
|
||||
cy.get(usersSelector.userActionButton).click();
|
||||
cy.get(usersSelector.editUserDetailsButton).click();
|
||||
cy.get('[data-cy="user-group-select"]>>>>>').eq(0).type("Admin");
|
||||
cy.wait(1000);
|
||||
cy.get('[data-cy="group-check-input"]').eq(0).check();
|
||||
|
||||
cy.get(usersSelector.buttonInviteUsers).click();
|
||||
cy.get('[data-cy="confim-button"]').click()
|
||||
|
||||
cy.verifyToastMessage(
|
||||
commonSelectors.toastMessage,
|
||||
"User has been updated"
|
||||
);
|
||||
|
||||
searchUser(data.email);
|
||||
cy.get(usersSelector.groupChip).eq(1).should("have.text", "Admin");
|
||||
cy.get('[data-name="role-header"] [data-cy="group-chip"]').should("have.text", "Admin");
|
||||
});
|
||||
|
||||
it("Should verify exisiting user invite flow", () => {
|
||||
|
|
|
|||
|
|
@ -2,34 +2,39 @@ import { groupsSelector } from "Selectors/manageGroups";
|
|||
import { groupsText } from "Texts/manageGroups";
|
||||
import { commonSelectors } from "Selectors/common";
|
||||
import { commonText } from "Texts/common";
|
||||
import { navigateToAllUserGroup, createGroup, navigateToManageGroups } from "Support/utils/common";
|
||||
import { cyParamName } from "../../constants/selectors/common";
|
||||
import {
|
||||
navigateToAllUserGroup,
|
||||
createGroup,
|
||||
navigateToManageGroups,
|
||||
} from "Support/utils/common";
|
||||
import { cyParamName } from "Selectors/common";
|
||||
|
||||
export const manageGroupsElements = () => {
|
||||
cy.get(groupsSelector.groupLink("All users")).verifyVisibleElement(
|
||||
cy.get('[data-cy="page-title"]').should(($el) => {
|
||||
expect($el.contents().last().text().trim()).to.eq("Groups");
|
||||
});
|
||||
|
||||
cy.get('[data-cy="admin-list-item"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.allUsers
|
||||
"Admin"
|
||||
);
|
||||
cy.get('[data-cy="user-role-title"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"USER ROLE"
|
||||
);
|
||||
cy.get('[data-cy="admin-title"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Admin (1)"
|
||||
);
|
||||
|
||||
cy.get(groupsSelector.groupLink("Admin")).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.admin
|
||||
);
|
||||
|
||||
navigateToAllUserGroup();
|
||||
|
||||
cy.get(groupsSelector.groupPageTitle("All Users")).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.allUsers
|
||||
);
|
||||
cy.get(groupsSelector.createNewGroupButton).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.createNewGroupButton
|
||||
);
|
||||
|
||||
cy.get(groupsSelector.appsLink).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.appsLink
|
||||
);
|
||||
cy.get(groupsSelector.usersLink).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.usersLink
|
||||
|
|
@ -38,66 +43,183 @@ export const manageGroupsElements = () => {
|
|||
"have.text",
|
||||
groupsText.permissionsLink
|
||||
);
|
||||
cy.get('[data-cy="granular-access-link"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Granular access"
|
||||
);
|
||||
|
||||
cy.get(groupsSelector.appsLink).click();
|
||||
// cy.get(groupsSelector.appsLink).click();
|
||||
|
||||
cy.get(groupsSelector.textDefaultGroup).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.textDefaultGroup
|
||||
);
|
||||
|
||||
cy.get(groupsSelector.searchBox).should("be.visible");
|
||||
cy.get(groupsSelector.selectAddButton).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.addButton
|
||||
);
|
||||
|
||||
cy.get(groupsSelector.nameTableHeader).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.textAppName
|
||||
groupsText.userNameTableHeader
|
||||
);
|
||||
cy.get(groupsSelector.emailTableHeader).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.emailTableHeader
|
||||
);
|
||||
|
||||
cy.get(groupsSelector.permissionsLink).click();
|
||||
cy.get('[data-cy="helper-text-admin-app-access"]')
|
||||
.eq(0)
|
||||
.verifyVisibleElement(
|
||||
"have.text",
|
||||
" Admin has edit access to all apps. These are not editableread documentation to know more !"
|
||||
);
|
||||
cy.get(groupsSelector.resourcesApps).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.resourcesApps
|
||||
);
|
||||
cy.get(groupsSelector.permissionstableHedaer).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.permissionstableHedaer
|
||||
);
|
||||
|
||||
cy.get("body").then(($title) => {
|
||||
if ($title.find(groupsSelector.helperTextNoAppsAdded).length > 0) {
|
||||
cy.get(groupsSelector.helperTextNoAppsAdded)
|
||||
.eq(0)
|
||||
.verifyVisibleElement("have.text", groupsText.helperTextNoAppsAdded);
|
||||
cy.get(groupsSelector.helperTextPermissions)
|
||||
.eq(0)
|
||||
.verifyVisibleElement("have.text", groupsText.helperTextPermissions);
|
||||
}
|
||||
});
|
||||
|
||||
cy.get(groupsSelector.createNewGroupButton).should("be.visible").click();
|
||||
cy.get(groupsSelector.addNewGroupModalTitle).verifyVisibleElement(
|
||||
cy.get(groupsSelector.resourcesApps).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.cardTitle
|
||||
);
|
||||
cy.get(groupsSelector.groupNameInput).should("be.visible");
|
||||
cy.get(groupsSelector.cancelButton).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.cancelButton
|
||||
);
|
||||
cy.get(groupsSelector.createGroupButton).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.createGroupButton
|
||||
);
|
||||
cy.get(groupsSelector.cancelButton).click();
|
||||
cy.get(groupsSelector.helperTextAllUsersIncluded).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.helperTextAllUsersIncluded
|
||||
groupsText.resourcesApps
|
||||
);
|
||||
|
||||
// cy.get(groupsSelector.usersLink).click();
|
||||
// cy.get(groupsSelector.helperTextAllUsersIncluded).verifyVisibleElement(
|
||||
// "have.text",
|
||||
// groupsText.helperTextAllUsersIncluded
|
||||
// );
|
||||
cy.get(groupsSelector.appsCreateCheck)
|
||||
.should("be.visible")
|
||||
.and("have.attr", "disabled");
|
||||
|
||||
cy.get(groupsSelector.appsCreateLabel).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.createLabel
|
||||
);
|
||||
cy.get('[data-cy="app-create-helper-text"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Create apps in this workspace"
|
||||
);
|
||||
cy.get(groupsSelector.appsDeleteCheck)
|
||||
.should("be.visible")
|
||||
.and("have.attr", "disabled");
|
||||
cy.get(groupsSelector.appsDeleteLabel).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.deleteLabel
|
||||
);
|
||||
cy.get('[data-cy="app-delete-helper-text"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Delete any app in this workspace"
|
||||
);
|
||||
|
||||
cy.get(groupsSelector.resourcesFolders).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.resourcesFolders
|
||||
);
|
||||
cy.get(groupsSelector.foldersCreateCheck)
|
||||
.should("be.visible")
|
||||
.and("have.attr", "disabled");
|
||||
cy.get(groupsSelector.foldersCreateLabel).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.folderCreateLabel
|
||||
);
|
||||
cy.get('[data-cy="folder-helper-text"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"All operations on folders"
|
||||
);
|
||||
|
||||
cy.get(groupsSelector.resourcesWorkspaceVar).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.resourcesWorkspaceVar
|
||||
);
|
||||
cy.get(groupsSelector.workspaceVarCheckbox)
|
||||
.should("be.visible")
|
||||
.and("have.attr", "disabled");
|
||||
cy.get('[data-cy="workspace-constants-helper-text"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"All operations on workspace constants"
|
||||
);
|
||||
|
||||
cy.get('[data-cy="granular-access-link"]').click();
|
||||
cy.get(groupsSelector.nameTableHeader).verifyVisibleElement(
|
||||
"have.text",
|
||||
"Name"
|
||||
);
|
||||
|
||||
cy.get(groupsSelector.permissionstableHedaer).verifyVisibleElement(
|
||||
"have.text",
|
||||
"Permission"
|
||||
);
|
||||
cy.get('[data-cy="resource-header"]:eq(1)').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Resource"
|
||||
);
|
||||
cy.get('[data-cy="apps-text"]').verifyVisibleElement("have.text", " Apps");
|
||||
cy.get('[data-cy="app-edit-radio"]')
|
||||
.should("be.visible")
|
||||
.and("have.attr", "disabled");
|
||||
cy.get('[data-cy="app-edit-radio"]').should("be.checked");
|
||||
cy.get('[data-cy="app-edit-label"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Edit"
|
||||
);
|
||||
cy.get('[data-cy="app-edit-helper-text"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Access to app builder"
|
||||
);
|
||||
|
||||
cy.get('[data-cy="app-view-radio"]')
|
||||
.should("be.visible")
|
||||
.and("have.attr", "disabled");
|
||||
cy.get('[data-cy="app-view-label"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"View"
|
||||
);
|
||||
cy.get('[data-cy="app-view-helper-text"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Only access released version of apps"
|
||||
);
|
||||
cy.get('[data-cy="app-hide-from-dashboard-radio"]')
|
||||
.should("be.visible")
|
||||
.and("have.attr", "disabled");
|
||||
|
||||
cy.get(
|
||||
'[data-cy="app-hide-from-dashboard-helper-text"]'
|
||||
).verifyVisibleElement("have.text", "App will be accessible by URL only");
|
||||
cy.get('[data-cy="group-chip"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"All apps"
|
||||
);
|
||||
cy.get('[data-cy="add-apps-buton"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Add apps"
|
||||
);
|
||||
|
||||
cy.get(groupsSelector.groupLink("Builder")).click();
|
||||
cy.get(groupsSelector.groupLink("Builder")).verifyVisibleElement(
|
||||
"have.text",
|
||||
"Builder"
|
||||
);
|
||||
|
||||
cy.get('[data-cy="builder-title"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Builder (1)"
|
||||
);
|
||||
|
||||
cy.get(groupsSelector.createNewGroupButton).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.createNewGroupButton
|
||||
);
|
||||
cy.get(groupsSelector.usersLink).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.usersLink
|
||||
);
|
||||
cy.get(groupsSelector.permissionsLink).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.permissionsLink
|
||||
);
|
||||
cy.get('[data-cy="granular-access-link"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Granular access"
|
||||
);
|
||||
cy.get(groupsSelector.usersLink).click();
|
||||
cy.get(groupsSelector.nameTableHeader).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.userNameTableHeader
|
||||
|
|
@ -121,7 +243,13 @@ export const manageGroupsElements = () => {
|
|||
"have.text",
|
||||
groupsText.resourcesApps
|
||||
);
|
||||
cy.get(groupsSelector.appsCreateCheck).should("be.visible").check();
|
||||
cy.get(groupsSelector.appsCreateCheck).should("be.visible").and("be.checked");
|
||||
cy.get(groupsSelector.appsCreateCheck).uncheck();
|
||||
cy.verifyToastMessage(
|
||||
commonSelectors.toastMessage,
|
||||
groupsText.permissionUpdatedToast
|
||||
);
|
||||
cy.get(groupsSelector.appsCreateCheck).check();
|
||||
cy.verifyToastMessage(
|
||||
commonSelectors.toastMessage,
|
||||
groupsText.permissionUpdatedToast
|
||||
|
|
@ -130,76 +258,155 @@ export const manageGroupsElements = () => {
|
|||
"have.text",
|
||||
groupsText.createLabel
|
||||
);
|
||||
cy.get(groupsSelector.appsCreateCheck).uncheck();
|
||||
cy.get(groupsSelector.appsDeleteCheck).should("be.visible").check();
|
||||
cy.get('[data-cy="app-create-helper-text"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Create apps in this workspace"
|
||||
);
|
||||
cy.get(groupsSelector.appsDeleteCheck).should("be.visible").and("be.checked");
|
||||
cy.get(groupsSelector.appsDeleteLabel).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.deleteLabel
|
||||
);
|
||||
cy.get('[data-cy="app-delete-helper-text"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Delete any app in this workspace"
|
||||
);
|
||||
|
||||
cy.get(groupsSelector.appsDeleteCheck).uncheck();
|
||||
cy.verifyToastMessage(
|
||||
commonSelectors.toastMessage,
|
||||
groupsText.permissionUpdatedToast
|
||||
);
|
||||
cy.get(groupsSelector.appsDeleteCheck).check();
|
||||
cy.verifyToastMessage(
|
||||
commonSelectors.toastMessage,
|
||||
groupsText.permissionUpdatedToast
|
||||
);
|
||||
|
||||
cy.get(groupsSelector.resourcesFolders).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.resourcesFolders
|
||||
);
|
||||
cy.get(groupsSelector.foldersCreateCheck).should("be.visible").check();
|
||||
cy.get(groupsSelector.foldersCreateCheck)
|
||||
.should("be.visible")
|
||||
.and("be.checked");
|
||||
cy.get(groupsSelector.foldersCreateLabel).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.folderCreateLabel
|
||||
);
|
||||
cy.get('[data-cy="folder-helper-text"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"All operations on folders"
|
||||
);
|
||||
cy.get(groupsSelector.foldersCreateCheck).uncheck();
|
||||
cy.verifyToastMessage(
|
||||
commonSelectors.toastMessage,
|
||||
groupsText.permissionUpdatedToast
|
||||
);
|
||||
cy.get(groupsSelector.foldersCreateCheck).check();
|
||||
cy.verifyToastMessage(
|
||||
commonSelectors.toastMessage,
|
||||
groupsText.permissionUpdatedToast
|
||||
);
|
||||
|
||||
cy.get(groupsSelector.resourcesWorkspaceVar).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.resourcesWorkspaceVar
|
||||
);
|
||||
cy.get(groupsSelector.workspaceVarCheckbox)
|
||||
.should("be.visible")
|
||||
.and("be.checked");
|
||||
cy.get('[data-cy="workspace-constants-helper-text"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"All operations on workspace constants"
|
||||
);
|
||||
cy.get(groupsSelector.workspaceVarCheckbox).uncheck();
|
||||
cy.verifyToastMessage(
|
||||
commonSelectors.toastMessage,
|
||||
groupsText.permissionUpdatedToast
|
||||
);
|
||||
cy.get(groupsSelector.workspaceVarCheckbox).check();
|
||||
cy.verifyToastMessage(
|
||||
commonSelectors.toastMessage,
|
||||
groupsText.permissionUpdatedToast
|
||||
);
|
||||
cy.get(groupsSelector.workspaceVarCheckbox).uncheck();
|
||||
|
||||
navigateToAllUserGroup();
|
||||
cy.get(groupsSelector.groupLink("Admin")).click();
|
||||
cy.get(groupsSelector.groupLink("Admin")).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.admin
|
||||
);
|
||||
|
||||
cy.get(groupsSelector.appsLink).click();
|
||||
cy.get(groupsSelector.textDefaultGroup).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.textDefaultGroup
|
||||
);
|
||||
|
||||
cy.get('[data-cy="granular-access-link"]').click();
|
||||
cy.get(groupsSelector.nameTableHeader).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.textAppName
|
||||
"Name"
|
||||
);
|
||||
|
||||
cy.get(groupsSelector.permissionstableHedaer).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.permissionstableHedaer
|
||||
"Permission"
|
||||
);
|
||||
|
||||
cy.get("body").then(($title) => {
|
||||
if ($title.find(groupsSelector.helperTextNoAppsAdded).length > 0) {
|
||||
cy.get(groupsSelector.helperTextNoAppsAdded)
|
||||
.eq(0)
|
||||
.verifyVisibleElement("have.text", groupsText.helperTextNoAppsAdded);
|
||||
cy.get(groupsSelector.helperTextPermissions)
|
||||
.eq(0)
|
||||
.verifyVisibleElement("have.text", groupsText.helperTextPermissions);
|
||||
}
|
||||
});
|
||||
|
||||
cy.get(groupsSelector.usersLink).click();
|
||||
cy.get(groupsSelector.multiSelectSearch).should("be.visible");
|
||||
cy.get(groupsSelector.mutiSelectAddButton("Admin")).verifyVisibleElement(
|
||||
cy.get('[data-cy="resource-header"]:eq(1)').verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.addUsersButton
|
||||
"Resource"
|
||||
);
|
||||
cy.get('[data-cy="apps-text"]').verifyVisibleElement("have.text", " Apps");
|
||||
cy.get('[data-cy="app-edit-radio"]').should("be.visible").and("be.checked");
|
||||
cy.get('[data-cy="app-edit-label"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Edit"
|
||||
);
|
||||
cy.get('[data-cy="app-edit-helper-text"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Access to app builder"
|
||||
);
|
||||
|
||||
cy.get('[data-cy="app-view-radio"]').should("be.visible");
|
||||
cy.get('[data-cy="app-view-label"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"View"
|
||||
);
|
||||
cy.get('[data-cy="app-view-helper-text"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Only access released version of apps"
|
||||
);
|
||||
cy.get('[data-cy="app-hide-from-dashboard-radio"]').should("be.visible");
|
||||
|
||||
cy.get(
|
||||
'[data-cy="app-hide-from-dashboard-helper-text"]'
|
||||
).verifyVisibleElement("have.text", "App will be accessible by URL only");
|
||||
cy.get('[data-cy="group-chip"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"All apps"
|
||||
);
|
||||
cy.get('[data-cy="add-apps-buton"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Add apps"
|
||||
);
|
||||
|
||||
cy.get(groupsSelector.groupLink("End-user")).click();
|
||||
cy.get(groupsSelector.groupLink("End-user")).verifyVisibleElement(
|
||||
"have.text",
|
||||
"End-user"
|
||||
);
|
||||
|
||||
cy.get('[data-cy="end-user-title"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"End-user (0)"
|
||||
);
|
||||
|
||||
cy.get(groupsSelector.createNewGroupButton).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.createNewGroupButton
|
||||
);
|
||||
cy.get(groupsSelector.usersLink).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.usersLink
|
||||
);
|
||||
cy.get(groupsSelector.permissionsLink).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.permissionsLink
|
||||
);
|
||||
cy.get('[data-cy="granular-access-link"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Granular access"
|
||||
);
|
||||
cy.get(groupsSelector.usersLink).click();
|
||||
cy.get(groupsSelector.nameTableHeader).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.userNameTableHeader
|
||||
|
|
@ -223,15 +430,109 @@ export const manageGroupsElements = () => {
|
|||
"have.text",
|
||||
groupsText.resourcesApps
|
||||
);
|
||||
cy.get(groupsSelector.appsCreateCheck).verifyVisibleElement("be.disabled");
|
||||
cy.get(groupsSelector.appsDeleteCheck).verifyVisibleElement("be.disabled");
|
||||
cy.get(groupsSelector.appsCreateCheck)
|
||||
.should("be.visible")
|
||||
.and("have.attr", "disabled");
|
||||
|
||||
cy.get(groupsSelector.appsCreateLabel).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.createLabel
|
||||
);
|
||||
cy.get('[data-cy="app-create-helper-text"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Create apps in this workspace"
|
||||
);
|
||||
cy.get(groupsSelector.appsDeleteCheck)
|
||||
.should("be.visible")
|
||||
.and("have.attr", "disabled");
|
||||
cy.get(groupsSelector.appsDeleteLabel).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.deleteLabel
|
||||
);
|
||||
cy.get('[data-cy="app-delete-helper-text"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Delete any app in this workspace"
|
||||
);
|
||||
|
||||
cy.get(groupsSelector.resourcesFolders).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.resourcesFolders
|
||||
);
|
||||
cy.get(groupsSelector.foldersCreateCheck)
|
||||
.should("be.visible")
|
||||
.and("have.attr", "disabled");
|
||||
cy.get(groupsSelector.foldersCreateLabel).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.folderCreateLabel
|
||||
);
|
||||
cy.get(groupsSelector.foldersCreateCheck).verifyVisibleElement("be.disabled");
|
||||
cy.get(groupsSelector.workspaceVarCheckbox).verifyVisibleElement(
|
||||
"be.disabled"
|
||||
cy.get('[data-cy="folder-helper-text"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"All operations on folders"
|
||||
);
|
||||
|
||||
cy.get(groupsSelector.resourcesWorkspaceVar).verifyVisibleElement(
|
||||
"have.text",
|
||||
groupsText.resourcesWorkspaceVar
|
||||
);
|
||||
cy.get(groupsSelector.workspaceVarCheckbox)
|
||||
.should("be.visible")
|
||||
.and("have.attr", "disabled");
|
||||
cy.get('[data-cy="workspace-constants-helper-text"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"All operations on workspace constants"
|
||||
);
|
||||
|
||||
cy.get('[data-cy="granular-access-link"]').click();
|
||||
cy.get(groupsSelector.nameTableHeader).verifyVisibleElement(
|
||||
"have.text",
|
||||
"Name"
|
||||
);
|
||||
|
||||
cy.get(groupsSelector.permissionstableHedaer).verifyVisibleElement(
|
||||
"have.text",
|
||||
"Permission"
|
||||
);
|
||||
cy.get('[data-cy="resource-header"]:eq(1)').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Resource"
|
||||
);
|
||||
cy.get('[data-cy="apps-text"]').verifyVisibleElement("have.text", " Apps");
|
||||
cy.get('[data-cy="app-edit-radio"]')
|
||||
.should("be.visible")
|
||||
.and("have.attr", "disabled");
|
||||
cy.get('[data-cy="app-edit-label"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Edit"
|
||||
);
|
||||
cy.get('[data-cy="app-edit-helper-text"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Access to app builder"
|
||||
);
|
||||
|
||||
cy.get('[data-cy="app-view-radio"]')
|
||||
.should("be.visible")
|
||||
.and("have.attr", "disabled");
|
||||
cy.get('[data-cy="app-view-radio"]').should("be.checked");
|
||||
cy.get('[data-cy="app-view-label"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"View"
|
||||
);
|
||||
cy.get('[data-cy="app-view-helper-text"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Only access released version of apps"
|
||||
);
|
||||
cy.get('[data-cy="app-hide-from-dashboard-radio"]').should("be.visible");
|
||||
|
||||
cy.get(
|
||||
'[data-cy="app-hide-from-dashboard-helper-text"]'
|
||||
).verifyVisibleElement("have.text", "App will be accessible by URL only");
|
||||
cy.get('[data-cy="group-chip"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"All apps"
|
||||
);
|
||||
cy.get('[data-cy="add-apps-buton"]').verifyVisibleElement(
|
||||
"have.text",
|
||||
"Add apps"
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -341,9 +642,13 @@ export const verifyGroupCardOptions = (groupName) => {
|
|||
);
|
||||
};
|
||||
|
||||
export const groupPermission = (fieldsToCheckOrUncheck, groupName = "All users", shouldCheck = false,) => {
|
||||
export const groupPermission = (
|
||||
fieldsToCheckOrUncheck,
|
||||
groupName = "All users",
|
||||
shouldCheck = false
|
||||
) => {
|
||||
navigateToManageGroups();
|
||||
cy.get(groupsSelector.groupLink(groupName))
|
||||
cy.get(groupsSelector.groupLink(groupName));
|
||||
cy.get(groupsSelector.permissionsLink).click();
|
||||
|
||||
fieldsToCheckOrUncheck.forEach((field) => {
|
||||
|
|
@ -362,5 +667,5 @@ export const groupPermission = (fieldsToCheckOrUncheck, groupName = "All users",
|
|||
|
||||
export const duplicateGroup = () => {
|
||||
OpenGroupCardOption(groupName);
|
||||
cy.get(groupsSelector.duplicateOption).click()
|
||||
}
|
||||
cy.get(groupsSelector.duplicateOption).click();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -122,8 +122,8 @@ export const manageUsersElements = () => {
|
|||
"have.text",
|
||||
usersText.buttonDownloadTemplate
|
||||
);
|
||||
cy.wait(3000)
|
||||
cy.exec("cd ./cypress/downloads/ && rm -rf *");
|
||||
cy.wait(3000)
|
||||
cy.get(usersSelector.buttonDownloadTemplate).click();
|
||||
cy.wait(4000)
|
||||
cy.exec("ls ./cypress/downloads/").then((result) => {
|
||||
|
|
@ -340,3 +340,40 @@ export const fetchAndVisitInviteLink = (email) => {
|
|||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
export const inviteUserWithUserRole = (
|
||||
firstName,
|
||||
email,
|
||||
role,
|
||||
|
||||
) => {
|
||||
fillUserInviteForm(firstName, email);
|
||||
|
||||
cy.wait(2000);
|
||||
|
||||
cy.get("body").then(($body) => {
|
||||
const selectDropdown = $body.find('[data-cy="user-group-select"]>>>>>');
|
||||
|
||||
if (selectDropdown.length === 0) {
|
||||
cy.get('[data-cy="user-group-select"]>>>>>').click();
|
||||
}
|
||||
cy.get('[data-cy="user-group-select"]>>>>>').eq(0).type(role);
|
||||
cy.wait(1000);
|
||||
cy.get('[data-cy="group-check-input"]').eq(0).check()
|
||||
cy.wait(1000);
|
||||
});
|
||||
|
||||
cy.get(usersSelector.buttonInviteUsers).click();
|
||||
cy.verifyToastMessage(
|
||||
commonSelectors.toastMessage,
|
||||
usersText.userCreatedToast
|
||||
);
|
||||
|
||||
cy.wait(1000);
|
||||
fetchAndVisitInviteLink(email);
|
||||
cy.clearAndType(commonSelectors.passwordInputField, "password");
|
||||
cy.get(commonSelectors.signUpButton).click();
|
||||
cy.wait(2000);
|
||||
cy.get(commonSelectors.acceptInviteButton).click();
|
||||
};
|
||||
2172
cypress-tests/package-lock.json
generated
2172
cypress-tests/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -15,7 +15,7 @@
|
|||
"dependencies": {
|
||||
"cypress-real-events": "^1.7.6",
|
||||
"moment": "^2.29.4",
|
||||
"node-xlsx": "^0.21.0",
|
||||
"node-xlsx": "^0.4.0",
|
||||
"pdf-parse": "^1.1.1",
|
||||
"pg": "^8.8.0"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
2.66.2
|
||||
2.67.0
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
First Name,Last Name,Email,Group
|
||||
test,user,test@gmail.com,For multiple groups separate using pipe (|) operator e.g. All Users|Admin
|
||||
First Name,Last Name,Email,User Role,Group
|
||||
test,user,test@gmail.com,"Assign each user a role: Admin, Builder or End User. User role value should be exact same","For multiple groups separate using pipe (|) operator e.g. Groups1|Group2 or leave blank if no group assign"
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
"search": "Search",
|
||||
"update": "Update",
|
||||
"delete": "Delete",
|
||||
"remove": "Remove",
|
||||
"add": "Add",
|
||||
"view": "View",
|
||||
"create": "Create",
|
||||
|
|
@ -104,7 +105,7 @@
|
|||
"passwordConfirmation": "Password Confirmation",
|
||||
"newToTooljet": "New to ToolJet?",
|
||||
"newToWorkspace": "New to this workspace?",
|
||||
"enterWorkEmail": "Enter your email",
|
||||
"enterWorkEmail": "Enter your work email",
|
||||
"enterPassword": "Enter password",
|
||||
"forgot": "Forgot?",
|
||||
"workEmail": "Email",
|
||||
|
|
@ -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"
|
||||
|
|
@ -418,6 +419,7 @@
|
|||
},
|
||||
"noApplicationFound": "No Applications found",
|
||||
"thisFolderIsEmpty": "This folder is empty",
|
||||
"nonAccessibleFolderApps": "You do not have access to any applications in this folder.",
|
||||
"deleteAppAndData": "The app {{appName}} and the associated data will be permanently deleted, do you want to continue?",
|
||||
"removeAppFromFolder": "The app will be removed from this folder, do you want to continue?",
|
||||
"change": "Change",
|
||||
|
|
@ -960,4 +962,4 @@
|
|||
"tip": "Back to Home"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ const userStatusOptions = [
|
|||
{ name: 'Archived', value: 'archived' },
|
||||
];
|
||||
|
||||
const UsersFilter = ({ filterList }) => {
|
||||
const UsersFilter = ({ filterList, resetSearch }) => {
|
||||
const [options, setOptions] = useState({ searchText: '', status: '' });
|
||||
const [statusVal, setStatusVal] = useState('');
|
||||
const [queryVal, setQueryVal] = useState();
|
||||
|
|
@ -44,6 +44,12 @@ const UsersFilter = ({ filterList }) => {
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [options.searchText, options.status]);
|
||||
|
||||
useEffect(() => {
|
||||
setOptions({ searchText: '', status: '' });
|
||||
setStatusVal('');
|
||||
setQueryVal('');
|
||||
}, [resetSearch]);
|
||||
|
||||
return (
|
||||
<div className="workspace-settings-table-wrap workspace-settings-filter-wrap">
|
||||
<div className="row workspace-settings-filters">
|
||||
|
|
@ -79,6 +85,7 @@ const UsersFilter = ({ filterList }) => {
|
|||
setQueryVal(e.target.value);
|
||||
queryValuesChanged(e);
|
||||
}}
|
||||
value={options.searchText}
|
||||
data-cy="input-field-user-filter-search"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ 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';
|
||||
|
||||
import Spinner from 'react-bootstrap/Spinner';
|
||||
const UsersTable = ({
|
||||
isLoading,
|
||||
|
|
@ -33,10 +35,10 @@ const UsersTable = ({
|
|||
<th data-cy="users-table-name-column-header">
|
||||
{translator('header.organization.menus.manageUsers.name', 'Name')}
|
||||
</th>
|
||||
<th data-cy="users-table-email-column-header">
|
||||
{translator('header.organization.menus.manageUsers.email', 'Email')}
|
||||
<th data-cy="users-table-roles-column-header" data-name="role-header">
|
||||
User role
|
||||
</th>
|
||||
<th data-cy="users-table-groups-column-header">Groups</th>
|
||||
<th data-cy="users-table-groups-column-header">Custom groups</th>
|
||||
{users && users[0]?.status ? (
|
||||
<th data-cy="users-table-status-column-header">
|
||||
{translator('header.organization.menus.manageUsers.status', 'Status')}
|
||||
|
|
@ -65,7 +67,7 @@ const UsersTable = ({
|
|||
{Array.isArray(users) &&
|
||||
users.length > 0 &&
|
||||
users.map((user) => (
|
||||
<tr key={user.id}>
|
||||
<tr key={user.id} data-cy={`${user.name.toLowerCase().replace(/\s+/g, '-')}-user-row`}>
|
||||
<td>
|
||||
<Avatar
|
||||
avatarId={user.avatar_id}
|
||||
|
|
@ -73,22 +75,24 @@ const UsersTable = ({
|
|||
user.last_name ? user.last_name[0] : ''
|
||||
}`}
|
||||
/>
|
||||
<span
|
||||
className="mx-3 tj-text tj-text-sm"
|
||||
data-cy={`${user.name.toLowerCase().replace(/\s+/g, '-')}-user-name`}
|
||||
>
|
||||
{decodeEntities(user.name)}
|
||||
</span>
|
||||
<div className="user-detail">
|
||||
<span
|
||||
className="mx-3 tj-text tj-text-sm"
|
||||
data-cy={`${user.name.toLowerCase().replace(/\s+/g, '-')}-user-name`}
|
||||
>
|
||||
{decodeEntities(user.name)}
|
||||
</span>
|
||||
<span
|
||||
style={{ color: '#687076' }}
|
||||
className="user-email mx-3 tj-text-xsm"
|
||||
data-cy={`${user.name.toLowerCase().replace(/\s+/g, '-')}-user-email`}
|
||||
>
|
||||
{user.email}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="text-muted">
|
||||
<a
|
||||
className="text-reset user-email tj-text-sm"
|
||||
data-cy={`${user.name.toLowerCase().replace(/\s+/g, '-')}-user-email`}
|
||||
>
|
||||
{user.email}
|
||||
</a>
|
||||
</td>
|
||||
<GroupChipTD groups={user.groups} />
|
||||
<GroupChipTD groups={user.role_group.map((group) => group.name)} isRole={true} />
|
||||
<GroupChipTD groups={user.groups.map((group) => group.name)} />
|
||||
{user.status && (
|
||||
<td className="text-muted">
|
||||
<span
|
||||
|
|
@ -165,7 +169,7 @@ const UsersTable = ({
|
|||
|
||||
export default UsersTable;
|
||||
|
||||
const GroupChipTD = ({ groups = [] }) => {
|
||||
const GroupChipTD = ({ groups = [], isRole = false }) => {
|
||||
const [showAllGroups, setShowAllGroups] = useState(false);
|
||||
const groupsListRef = useRef();
|
||||
|
||||
|
|
@ -196,20 +200,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) => (
|
||||
<span className="group-chip" key={index} data-cy="group-chip">
|
||||
{humanizeifDefaultGroupName(group)}
|
||||
</span>
|
||||
<ToolTip message={group}>
|
||||
<span className="group-chip" key={index} data-cy="group-chip">
|
||||
{humanizeifDefaultGroupName(group)}
|
||||
</span>
|
||||
</ToolTip>
|
||||
);
|
||||
|
||||
return (
|
||||
<td
|
||||
data-name={isRole ? 'role-header' : ''}
|
||||
data-active={showAllGroups}
|
||||
ref={groupsListRef}
|
||||
onClick={(e) => {
|
||||
|
|
@ -218,28 +225,31 @@ const GroupChipTD = ({ groups = [] }) => {
|
|||
className={cx('text-muted groups-name-cell', { 'groups-hover': orderedArray.length > 2 })}
|
||||
>
|
||||
<div className="groups-name-container tj-text-sm font-weight-500">
|
||||
{orderedArray.slice(0, 2).map((group, index) => {
|
||||
if (orderedArray.length <= 2) {
|
||||
return renderGroupChip(group, index);
|
||||
}
|
||||
{orderedArray.length === 0 ? (
|
||||
<div className="empty-text">-</div>
|
||||
) : (
|
||||
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 (
|
||||
<>
|
||||
<span className="group-chip" key={index}>
|
||||
{' '}
|
||||
+{orderedArray.length - 1} more
|
||||
</span>
|
||||
<React.Fragment key={index}>
|
||||
{renderGroupChip(group, index)}
|
||||
<span className="group-chip">+{orderedArray.length - 2} more</span>
|
||||
{showAllGroups && (
|
||||
<div className="all-groups-list">{groups.map((group, index) => renderGroupChip(group, index))}</div>
|
||||
<div className="all-groups-list">
|
||||
{orderedArray.slice(2).map((group, index) => renderGroupChip(group, index))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return renderGroupChip(group, index);
|
||||
}
|
||||
})}
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
|
|
|
|||
38653
frontend/package-lock.json
generated
38653
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -180,7 +180,7 @@
|
|||
"terser-webpack-plugin": "^5.3.6",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-cli": "^5.0.1",
|
||||
"webpack-dev-server": "^4.11.1"
|
||||
"webpack-dev-server": "4.11.1"
|
||||
},
|
||||
"overrides": {
|
||||
"react-dates": {
|
||||
|
|
|
|||
|
|
@ -6,17 +6,12 @@ import { authenticationService, tooljetService } from '@/_services';
|
|||
import { withRouter } from '@/_hoc/withRouter';
|
||||
import { PrivateRoute, AdminRoute, AppsRoute, SwitchWorkspaceRoute, OrganizationInviteRoute } from '@/Routes';
|
||||
import { HomePage } from '@/HomePage';
|
||||
import { LoginPage } from '@/LoginPage';
|
||||
import { SignupPage } from '@/SignupPage';
|
||||
import { TooljetDatabase } from '@/TooljetDatabase';
|
||||
import { OrganizationInvitationPage } from '@/ConfirmationPage';
|
||||
import { Authorize } from '@/Oauth2';
|
||||
import { Authorize as Oauth } from '@/Oauth';
|
||||
import { Viewer } from '@/Editor';
|
||||
import { OrganizationSettings } from '@/OrganizationSettingsPage';
|
||||
import { SettingsPage } from '../SettingsPage/SettingsPage';
|
||||
import { ForgotPassword } from '@/ForgotPassword';
|
||||
import { ResetPassword } from '@/ResetPassword';
|
||||
import { MarketplacePage } from '@/MarketplacePage';
|
||||
import SwitchWorkspacePage from '@/HomePage/SwitchWorkspacePage';
|
||||
import { GlobalDatasources } from '@/GlobalDatasources';
|
||||
|
|
@ -25,21 +20,20 @@ import Toast from '@/_ui/Toast';
|
|||
import { VerificationSuccessInfoScreen } from '@/SuccessInfoScreen';
|
||||
import '@/_styles/theme.scss';
|
||||
import { AppLoader } from '@/AppLoader';
|
||||
import SetupScreenSelfHost from '../SuccessInfoScreen/SetupScreenSelfHost';
|
||||
export const BreadCrumbContext = React.createContext({});
|
||||
import 'react-tooltip/dist/react-tooltip.css';
|
||||
import { getWorkspaceIdOrSlugFromURL } from '@/_helpers/routes';
|
||||
import ErrorPage from '@/_components/ErrorComponents/ErrorPage';
|
||||
import WorkspaceConstants from '@/WorkspaceConstants';
|
||||
import { AuthRoute } from '@/Routes/AuthRoute';
|
||||
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';
|
||||
import { onboarding, auth } from '@/modules';
|
||||
|
||||
const AppWrapper = (props) => {
|
||||
const { isAppDarkMode } = useAppDarkMode();
|
||||
|
|
@ -165,49 +159,10 @@ class AppComponent extends React.Component {
|
|||
)}
|
||||
<BreadCrumbContext.Provider value={{ sidebarNav, updateSidebarNAV }}>
|
||||
<Routes>
|
||||
<Route
|
||||
path="/login/:organizationId"
|
||||
exact
|
||||
element={
|
||||
<AuthRoute {...this.props}>
|
||||
<LoginPage {...this.props} />
|
||||
</AuthRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/login"
|
||||
exact
|
||||
element={
|
||||
<AuthRoute {...this.props}>
|
||||
<LoginPage {...this.props} />
|
||||
</AuthRoute>
|
||||
}
|
||||
/>
|
||||
<Route path="/setup" exact element={<SetupScreenSelfHost {...this.props} darkMode={darkMode} />} />
|
||||
{onboarding(this.props)}
|
||||
{auth(this.props)}
|
||||
<Route path="/sso/:origin/:configId" exact element={<Oauth {...this.props} />} />
|
||||
<Route path="/sso/:origin" exact element={<Oauth {...this.props} />} />
|
||||
<Route
|
||||
path="/signup/:organizationId"
|
||||
exact
|
||||
element={
|
||||
<AuthRoute {...this.props}>
|
||||
<SignupPage {...this.props} />
|
||||
</AuthRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/signup"
|
||||
exact
|
||||
element={
|
||||
<AuthRoute {...this.props}>
|
||||
<SignupPage {...this.props} />
|
||||
</AuthRoute>
|
||||
}
|
||||
/>
|
||||
<Route path="/forgot-password" element={<ForgotPassword />} />
|
||||
<Route path="/reset-password/:token" element={<ResetPassword />} />
|
||||
<Route path="/reset-password" element={<ResetPassword />} />
|
||||
<Route path="/invitations/:token" element={<VerificationSuccessInfoScreen />} />
|
||||
<Route
|
||||
path="/invitations/:token/workspaces/:organizationToken"
|
||||
element={
|
||||
|
|
@ -216,14 +171,6 @@ class AppComponent extends React.Component {
|
|||
</OrganizationInviteRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/organization-invitations/:token"
|
||||
element={
|
||||
<OrganizationInviteRoute {...this.props} isOrgazanizationOnlyInvite={true}>
|
||||
<OrganizationInvitationPage {...this.props} darkMode={darkMode} />
|
||||
</OrganizationInviteRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/:workspaceId/apps/:slug/:pageHandle?/*"
|
||||
|
|
@ -310,7 +257,7 @@ class AppComponent extends React.Component {
|
|||
path="groups"
|
||||
element={
|
||||
<AdminRoute>
|
||||
<ManageGroupPermissions switchDarkMode={this.switchDarkMode} darkMode={darkMode} />
|
||||
<ManageGroupPermissionsV2 switchDarkMode={this.switchDarkMode} darkMode={darkMode} />
|
||||
</AdminRoute>
|
||||
}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -4,12 +4,14 @@ import CodeHinter from '.';
|
|||
import { copyToClipboard } from '@/_helpers/appUtils';
|
||||
import { Alert } from '@/_ui/Alert/Alert';
|
||||
import _, { isEmpty } from 'lodash';
|
||||
import { handleCircularStructureToJSON, hasCircularDependency } from '@/_helpers/utils';
|
||||
import { handleCircularStructureToJSON, hasCircularDependency, verifyConstant } from '@/_helpers/utils';
|
||||
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
|
||||
import Popover from 'react-bootstrap/Popover';
|
||||
import Card from 'react-bootstrap/Card';
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { JsonViewer } from '@textea/json-viewer';
|
||||
import { useCurrentStateStore } from '@/_stores/currentStateStore';
|
||||
import { useDataQueriesStore } from '@/_stores/dataQueriesStore';
|
||||
import { reservedKeywordReplacer } from '@/_lib/reserved-keyword-replacer';
|
||||
|
||||
const sanitizeLargeDataset = (data, callback) => {
|
||||
|
|
@ -111,6 +113,19 @@ export const PreviewBox = ({
|
|||
|
||||
let previewType = getCurrentNodeType(resolvedValue);
|
||||
let previewContent = resolvedValue;
|
||||
let isGlobalConstant = currentValue && currentValue.includes('{{constants.');
|
||||
let isSecretConstant = currentValue && currentValue.includes('{{secrets.');
|
||||
let invalidConstants = null;
|
||||
let undefinedError = null;
|
||||
if (isGlobalConstant || isSecretConstant) {
|
||||
const secrets = useDataQueriesStore.getState().secrets;
|
||||
const globals = useCurrentStateStore.getState().constants;
|
||||
|
||||
invalidConstants = verifyConstant(currentValue, globals, secrets);
|
||||
}
|
||||
if (invalidConstants?.length) {
|
||||
undefinedError = { type: 'Invalid constants' };
|
||||
}
|
||||
|
||||
const ifCoersionErrorHasCircularDependency = (value) => {
|
||||
if (hasCircularDependency(value)) {
|
||||
|
|
@ -133,7 +148,17 @@ export const PreviewBox = ({
|
|||
}, [error]);
|
||||
|
||||
useEffect(() => {
|
||||
const [valid, _error, newValue, resolvedValue] = resolveReferences(currentValue, validationSchema, customVariables);
|
||||
const [valid, _error, rawNewValue, rawResolvedValue] = resolveReferences(
|
||||
currentValue,
|
||||
validationSchema,
|
||||
customVariables
|
||||
);
|
||||
|
||||
const resolvedValue = typeof rawResolvedValue === 'function' ? undefined : rawResolvedValue;
|
||||
|
||||
const newValue = typeof rawNewValue === 'function' ? undefined : rawNewValue;
|
||||
const isSecretError =
|
||||
currentValue?.includes('secrets.') || _error?.includes('ReferenceError: secrets is not defined');
|
||||
|
||||
if (isWorkspaceVariable || !validationSchema || isEmpty(validationSchema)) {
|
||||
return setResolvedValue(newValue);
|
||||
|
|
@ -142,7 +167,7 @@ export const PreviewBox = ({
|
|||
// we dont need to add or update the resolved value if the value has deep children
|
||||
const _resolveValue = sanitizeLargeDataset(resolvedValue, setLargeDataset);
|
||||
|
||||
if (valid) {
|
||||
if (valid && !isSecretError) {
|
||||
const [coercionPreview, typeAfterCoercion, typeBeforeCoercion] = computeCoercion(resolvedValue, newValue);
|
||||
|
||||
setResolvedValue(_resolveValue);
|
||||
|
|
@ -153,11 +178,13 @@ export const PreviewBox = ({
|
|||
typeBeforeCoercion,
|
||||
});
|
||||
setError(null);
|
||||
} else if (!valid && !newValue && !resolvedValue) {
|
||||
} else if (!valid && !newValue && !resolvedValue && !isSecretError) {
|
||||
const err = !error ? `Invalid value for ${validationSchema?.schema?.type}` : `${_error}`;
|
||||
setError({ message: err, value: resolvedValue, type: 'Invalid' });
|
||||
} else {
|
||||
const jsErrorType = _error?.includes('ReferenceError')
|
||||
const jsErrorType = isSecretError
|
||||
? 'Error'
|
||||
: _error?.includes('ReferenceError')
|
||||
? 'ReferenceError'
|
||||
: _error?.includes('TypeError')
|
||||
? 'TypeError'
|
||||
|
|
@ -168,9 +195,9 @@ export const PreviewBox = ({
|
|||
const errValue = ifCoersionErrorHasCircularDependency(_resolveValue);
|
||||
|
||||
setError({
|
||||
message: _error,
|
||||
value: jsErrorType === 'Invalid' ? JSON.stringify(errValue, reservedKeywordReplacer) : resolvedValue,
|
||||
type: jsErrorType,
|
||||
message: isSecretError ? 'secrets cannot be used in apps' : _error,
|
||||
value: isSecretError ? 'Undefined' : jsErrorType === 'Invalid' ? JSON.stringify(errValue) : resolvedValue,
|
||||
type: isSecretError ? 'Error' : jsErrorType,
|
||||
});
|
||||
setCoersionData(null);
|
||||
}
|
||||
|
|
@ -180,13 +207,14 @@ export const PreviewBox = ({
|
|||
return (
|
||||
<>
|
||||
<PreviewBox.RenderResolvedValue
|
||||
error={error}
|
||||
error={error || undefinedError}
|
||||
currentValue={currentValue}
|
||||
previewType={previewType}
|
||||
resolvedValue={content}
|
||||
coersionData={coersionData}
|
||||
withValidation={!isEmpty(validationSchema)}
|
||||
isWorkspaceVariable={isWorkspaceVariable}
|
||||
isSecretConstant={isSecretConstant || false}
|
||||
isLargeDataset={largeDataset}
|
||||
/>
|
||||
<CodeHinter.PopupIcon
|
||||
|
|
@ -205,6 +233,7 @@ const RenderResolvedValue = ({
|
|||
coersionData,
|
||||
withValidation,
|
||||
isWorkspaceVariable,
|
||||
isSecretConstant = false,
|
||||
isLargeDataset,
|
||||
}) => {
|
||||
const computeCoersionPreview = (resolvedValue, coersionData) => {
|
||||
|
|
@ -229,7 +258,11 @@ const RenderResolvedValue = ({
|
|||
}`
|
||||
: previewType;
|
||||
|
||||
const previewContent = !withValidation ? resolvedValue : computeCoersionPreview(resolvedValue, coersionData);
|
||||
const previewContent = isSecretConstant
|
||||
? 'Values of secret constants are hidden'
|
||||
: !withValidation
|
||||
? resolvedValue
|
||||
: computeCoersionPreview(resolvedValue, coersionData);
|
||||
|
||||
const cls = error ? 'codehinter-error-banner' : 'codehinter-success-banner';
|
||||
|
||||
|
|
|
|||
|
|
@ -281,7 +281,11 @@ export const resolveReferences = (query, validationSchema, customResolvers = {})
|
|||
const queryHasJSCode = queryHasStringOtherThanVariable(query);
|
||||
let useJSResolvers = queryHasJSCode || getDynamicVariables(query)?.length > 1;
|
||||
|
||||
if (!queryHasJSCode && getDynamicVariables(query)?.length === 1 && !query.startsWith('{{') && query.includes('{{')) {
|
||||
if (
|
||||
!queryHasJSCode &&
|
||||
getDynamicVariables(query)?.length === 1 &&
|
||||
((!query.startsWith('{{') && query.includes('{{')) || (query.startsWith('{{') && !query.endsWith('}}')))
|
||||
) {
|
||||
useJSResolvers = true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ import { withTranslation } from 'react-i18next';
|
|||
import { v4 as uuid } from 'uuid';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
import EditorHeader from './Header';
|
||||
import { getWorkspaceId, isValidUUID } from '@/_helpers/utils';
|
||||
import { getWorkspaceId, isValidUUID, Constants } from '@/_helpers/utils';
|
||||
import { fetchAndSetWindowTitle, pageTitles, defaultWhiteLabellingSettings } from '@white-label/whiteLabelling';
|
||||
import '@/_styles/editor/react-select-search.scss';
|
||||
import { withRouter } from '@/_hoc/withRouter';
|
||||
|
|
@ -229,12 +229,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 = {
|
||||
|
|
@ -400,19 +403,26 @@ const EditorComponent = (props) => {
|
|||
});
|
||||
};
|
||||
|
||||
const fetchOrgEnvironmentConstants = () => {
|
||||
//! for @ee: get the constants from `getConstantsFromEnvironment ` -- '/organization-constants/:environmentId'
|
||||
orgEnvironmentConstantService.getAll().then(({ constants }) => {
|
||||
const fetchOrgEnvironmentConstants = async (environmentId) => {
|
||||
try {
|
||||
const { constants } = await orgEnvironmentConstantService.getConstantsFromEnvironment(
|
||||
environmentId,
|
||||
Constants.Global
|
||||
);
|
||||
const orgConstants = {};
|
||||
constants.map((constant) => {
|
||||
const constantValue = constant.values.find((value) => value.environmentName === 'production')['value'];
|
||||
orgConstants[constant.name] = constantValue;
|
||||
|
||||
constants.forEach((constant) => {
|
||||
orgConstants[constant.name] = constant.value;
|
||||
});
|
||||
|
||||
useCurrentStateStore.getState().actions.setCurrentState({
|
||||
constants: orgConstants,
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
toast.error('Failed to fetch organization environment constants', {
|
||||
position: 'top-center',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const initComponentVersioning = () => {
|
||||
|
|
@ -717,7 +727,8 @@ const EditorComponent = (props) => {
|
|||
fetchAndSetWindowTitle({ page: pageTitles.EDITOR, appName });
|
||||
useAppVersionStore.getState().actions.updateEditingVersion(editing_version);
|
||||
current_version_id && useAppVersionStore.getState().actions.updateReleasedVersionId(current_version_id);
|
||||
await fetchOrgEnvironmentConstants();
|
||||
const environmentId = editing_version?.current_environment_id;
|
||||
await fetchOrgEnvironmentConstants(environmentId);
|
||||
updateState({
|
||||
slug,
|
||||
isMaintenanceOn,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { appService, appsService, authenticationService } from '@/_services';
|
||||
import Modal from 'react-bootstrap/Modal';
|
||||
import { toast } from 'react-hot-toast';
|
||||
|
|
@ -15,6 +15,7 @@ import { ToolTip } from '@/_components/ToolTip';
|
|||
import { TOOLTIP_MESSAGES } from '@/_helpers/constants';
|
||||
import { useAppDataStore } from '@/_stores/appDataStore';
|
||||
import { retrieveWhiteLabelText } from '@white-label/whiteLabelling';
|
||||
import InfoIcon from '@assets/images/icons/info.svg';
|
||||
|
||||
class ManageAppUsersComponent extends React.Component {
|
||||
constructor(props) {
|
||||
|
|
@ -32,6 +33,7 @@ class ManageAppUsersComponent extends React.Component {
|
|||
value: null,
|
||||
error: '',
|
||||
},
|
||||
isHovered: false,
|
||||
isSlugUpdated: false,
|
||||
};
|
||||
}
|
||||
|
|
@ -85,7 +87,6 @@ class ManageAppUsersComponent extends React.Component {
|
|||
toast.error(error);
|
||||
});
|
||||
};
|
||||
|
||||
toggleAppVisibility = () => {
|
||||
const newState = !this.props.isPublic;
|
||||
this.setState({
|
||||
|
|
@ -170,7 +171,13 @@ class ManageAppUsersComponent extends React.Component {
|
|||
});
|
||||
}
|
||||
};
|
||||
handleMouseEnter = () => {
|
||||
this.setState({ isHovered: true });
|
||||
};
|
||||
|
||||
handleMouseLeave = () => {
|
||||
this.setState({ isHovered: false });
|
||||
};
|
||||
render() {
|
||||
const { appId, isSlugVerificationInProgress, newSlug, isSlugUpdated } = this.state;
|
||||
|
||||
|
|
@ -178,66 +185,104 @@ class ManageAppUsersComponent extends React.Component {
|
|||
const shareableLink = appLink + (this.props.slug || appId);
|
||||
const slugButtonClass = !_.isEmpty(newSlug.error) ? 'is-invalid' : 'is-valid';
|
||||
const embeddableLink = `<iframe width="560" height="315" src="${appLink}${this.props.slug}" title="${this.whiteLabelText} app - ${this.props.slug}" frameborder="0" allowfullscreen></iframe>`;
|
||||
const shouldWeDisableShareModal = !this.props.isVersionReleased;
|
||||
const { isHovered } = this.state.isHovered;
|
||||
|
||||
return (
|
||||
<ToolTip
|
||||
message={TOOLTIP_MESSAGES.SHARE_URL_UNAVAILABLE}
|
||||
placement={!this.props.isVersionReleased ? 'bottom' : 'left'}
|
||||
show={shouldWeDisableShareModal}
|
||||
>
|
||||
<div
|
||||
title={!shouldWeDisableShareModal ? 'Share' : ''}
|
||||
className="manage-app-users editor-header-icon tj-secondary-btn"
|
||||
data-cy="share-button-link"
|
||||
<div title={'Share'} className="manage-app-users" data-cy="share-button-link">
|
||||
<span
|
||||
className="manage-app-users tj-secondary-btn editor-header-icon cursor-pointer"
|
||||
onClick={() => {
|
||||
this.validateThePreExistingSlugs();
|
||||
this.setState({ showModal: true });
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className={cx('d-flex', {
|
||||
'share-disabled': shouldWeDisableShareModal,
|
||||
'share-disabled': false,
|
||||
})}
|
||||
onClick={() => {
|
||||
this.validateThePreExistingSlugs();
|
||||
!shouldWeDisableShareModal && this.setState({ showModal: true });
|
||||
}}
|
||||
>
|
||||
<SolidIcon name="share" width="14" className="cursor-pointer" fill="#3E63DD" />
|
||||
</span>
|
||||
<Modal
|
||||
show={this.state.showModal}
|
||||
size="lg"
|
||||
backdrop="static"
|
||||
centered={true}
|
||||
keyboard={true}
|
||||
animation={false}
|
||||
onEscapeKeyDown={this.hideModal}
|
||||
className={`app-sharing-modal animation-fade ${this.props.darkMode ? 'dark-theme' : ''}`}
|
||||
contentClassName={this.props.darkMode ? 'dark-theme' : ''}
|
||||
>
|
||||
<Modal.Header>
|
||||
<Modal.Title data-cy="modal-header">{this.props.t('editor.share', 'Share')}</Modal.Title>
|
||||
<span onClick={this.hideModal} data-cy="modal-close-button">
|
||||
<SolidIcon name="remove" className="cursor-pointer" aria-label="Close" />
|
||||
</span>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
{
|
||||
<div class="shareable-link-container">
|
||||
<div className="make-public mb-3">
|
||||
<div className="form-check form-switch d-flex align-items-center">
|
||||
<input
|
||||
className="form-check-input"
|
||||
type="checkbox"
|
||||
onClick={this.toggleAppVisibility}
|
||||
checked={this?.props?.isPublic}
|
||||
disabled={this.state.ischangingVisibility}
|
||||
data-cy="make-public-app-toggle"
|
||||
/>
|
||||
<span className="form-check-label field-name" data-cy="make-public-app-label">
|
||||
{this.props.t('editor.shareModal.makeApplicationPublic', 'Make application public')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
<Modal
|
||||
show={this.state.showModal}
|
||||
size="lg"
|
||||
backdrop="static"
|
||||
centered={true}
|
||||
keyboard={true}
|
||||
animation={false}
|
||||
onEscapeKeyDown={this.hideModal}
|
||||
className={`app-sharing-modal animation-fade ${this.props.darkMode ? 'dark-theme' : ''}`}
|
||||
contentClassName={this.props.darkMode ? 'dark-theme' : ''}
|
||||
>
|
||||
<Modal.Header>
|
||||
<Modal.Title data-cy="modal-header">{this.props.t('editor.share', 'Share')}</Modal.Title>
|
||||
<span onClick={this.hideModal} data-cy="modal-close-button">
|
||||
<SolidIcon name="remove" className="cursor-pointer" aria-label="Close" />
|
||||
</span>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
{
|
||||
<div class="shareable-link-container">
|
||||
<div className="make-public mb-3">
|
||||
<div className="form-check form-switch d-flex align-items-center">
|
||||
{this.props.isVersionReleased ? (
|
||||
<div>
|
||||
<input
|
||||
className="form-check-input"
|
||||
type="checkbox"
|
||||
onClick={this.toggleAppVisibility}
|
||||
checked={this?.props?.isPublic}
|
||||
disabled={this.state.ischangingVisibility}
|
||||
data-cy="make-public-app-toggle"
|
||||
/>
|
||||
<span className="form-check-label field-name" data-cy="make-public-app-label">
|
||||
{this.props.t('editor.shareModal.makeApplicationPublic', 'Make application public')}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', alignItems: 'left', gap: '8px' }}>
|
||||
<ToolTip
|
||||
message={TOOLTIP_MESSAGES.RELEASE_VERSION_URL_UNAVAILABLE}
|
||||
placement={'top'}
|
||||
show={isHovered}
|
||||
>
|
||||
<div
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseLeave}
|
||||
style={{
|
||||
width: '32px',
|
||||
height: '18px',
|
||||
marginLeft: '-40px',
|
||||
}}
|
||||
>
|
||||
<input
|
||||
className="form-check-input"
|
||||
type="checkbox"
|
||||
disabled
|
||||
style={{
|
||||
opacity: 0.3,
|
||||
cursor: 'default',
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</ToolTip>
|
||||
|
||||
<span
|
||||
className="form-check-label field-name"
|
||||
data-cy="make-public-app-label"
|
||||
style={{ opacity: 0.6 }}
|
||||
>
|
||||
{this.props.t('editor.shareModal.makeApplicationPublic', 'Make application public')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{this.props.isVersionReleased ? (
|
||||
<div className="shareable-link tj-app-input mb-2">
|
||||
<label data-cy="shareable-app-link-label" className="field-name">
|
||||
{this.props.t('editor.shareModal.shareableLink', 'Shareable app link')}
|
||||
|
|
@ -259,6 +304,7 @@ class ManageAppUsersComponent extends React.Component {
|
|||
style={{ maxWidth: '150px' }}
|
||||
defaultValue={this.props.slug}
|
||||
data-cy="app-name-slug-input"
|
||||
disabled={!this.props.isVersionReleased}
|
||||
/>
|
||||
{isSlugVerificationInProgress && (
|
||||
<div className="icon-container">
|
||||
|
|
@ -344,60 +390,69 @@ class ManageAppUsersComponent extends React.Component {
|
|||
>{`URL-friendly 'slug' consists of lowercase letters, numbers, and hyphens`}</label>
|
||||
)}
|
||||
</div>
|
||||
{(this?.props?.isPublic || window?.public_config?.ENABLE_PRIVATE_APP_EMBED === 'true') && (
|
||||
<div className="tj-app-input">
|
||||
<label className="field-name" data-cy="iframe-link-label">
|
||||
Embedded app link
|
||||
</label>
|
||||
<span className={`tj-text-input justify-content-between ${this.props.darkMode ? 'dark' : ''}`}>
|
||||
<span data-cy="iframe-link">{embeddableLink}</span>
|
||||
<span className="copy-container">
|
||||
<CopyToClipboard
|
||||
text={embeddableLink}
|
||||
onCopy={() => toast.success('Link copied to clipboard')}
|
||||
>
|
||||
<svg
|
||||
className="cursor-pointer"
|
||||
width="17"
|
||||
height="18"
|
||||
viewBox="0 0 17 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
data-cy="iframe-link-copy-button"
|
||||
>
|
||||
<path
|
||||
d="M9.11154 5.18031H5.88668V4.83302C5.88668 3.29859 7.13059 2.05469 8.66502 2.05469H12.8325C14.3669 2.05469 15.6109 3.29859 15.6109 4.83302V9.00052C15.6109 10.535 14.3669 11.7789 12.8325 11.7789H12.4852V8.554C12.4852 6.69076 10.9748 5.18031 9.11154 5.18031Z"
|
||||
fill="#889096"
|
||||
/>
|
||||
<path
|
||||
d="M8.66502 15.9464H4.49752C2.96309 15.9464 1.71918 14.7025 1.71918 13.168V9.00052C1.71918 7.46609 2.96309 6.22219 4.49752 6.22219H8.66502C10.1994 6.22219 11.4434 7.46609 11.4434 9.00052V13.168C11.4434 14.7025 10.1994 15.9464 8.66502 15.9464Z"
|
||||
fill="#889096"
|
||||
/>
|
||||
</svg>
|
||||
</CopyToClipboard>
|
||||
</span>
|
||||
</span>
|
||||
) : (
|
||||
<div className="shareable-link tj-app-input mb-2">
|
||||
<label data-cy="shareable-app-link-label" className="field-name">
|
||||
{this.props.t('editor.shareModal.shareableLink', 'Shareable app link')}
|
||||
</label>
|
||||
<div className="empty-version">
|
||||
<InfoIcon style={{ width: '12px', marginRight: '5px' }} />
|
||||
<span>This version has not been released yet</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
</Modal.Body>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Modal.Footer className="manage-app-users-footer">
|
||||
{this.isUserAdmin && (
|
||||
<Link
|
||||
to={getPrivateRoute('workspace_settings')}
|
||||
target="_blank"
|
||||
className={`btn border-0 default-secondary-button float-right1`}
|
||||
data-cy="manage-users-button"
|
||||
>
|
||||
Manage users
|
||||
</Link>
|
||||
)}
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
</div>
|
||||
</ToolTip>
|
||||
{((this?.props?.isVersionReleased && this?.props?.isPublic) ||
|
||||
window?.public_config?.ENABLE_PRIVATE_APP_EMBED === 'true') && (
|
||||
<div className="tj-app-input">
|
||||
<label className="field-name" data-cy="iframe-link-label">
|
||||
Embedded app link
|
||||
</label>
|
||||
<span className={`tj-text-input justify-content-between ${this.props.darkMode ? 'dark' : ''}`}>
|
||||
<span data-cy="iframe-link">{embeddableLink}</span>
|
||||
<span className="copy-container">
|
||||
<CopyToClipboard text={embeddableLink} onCopy={() => toast.success('Link copied to clipboard')}>
|
||||
<svg
|
||||
className="cursor-pointer"
|
||||
width="17"
|
||||
height="18"
|
||||
viewBox="0 0 17 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
data-cy="iframe-link-copy-button"
|
||||
>
|
||||
<path
|
||||
d="M9.11154 5.18031H5.88668V4.83302C5.88668 3.29859 7.13059 2.05469 8.66502 2.05469H12.8325C14.3669 2.05469 15.6109 3.29859 15.6109 4.83302V9.00052C15.6109 10.535 14.3669 11.7789 12.8325 11.7789H12.4852V8.554C12.4852 6.69076 10.9748 5.18031 9.11154 5.18031Z"
|
||||
fill="#889096"
|
||||
/>
|
||||
<path
|
||||
d="M8.66502 15.9464H4.49752C2.96309 15.9464 1.71918 14.7025 1.71918 13.168V9.00052C1.71918 7.46609 2.96309 6.22219 4.49752 6.22219H8.66502C10.1994 6.22219 11.4434 7.46609 11.4434 9.00052V13.168C11.4434 14.7025 10.1994 15.9464 8.66502 15.9464Z"
|
||||
fill="#889096"
|
||||
/>
|
||||
</svg>
|
||||
</CopyToClipboard>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
</Modal.Body>
|
||||
|
||||
<Modal.Footer className="manage-app-users-footer">
|
||||
{this.isUserAdmin && (
|
||||
<Link
|
||||
to={getPrivateRoute('workspace_settings')}
|
||||
target="_blank"
|
||||
className={`btn border-0 default-secondary-button float-right1`}
|
||||
data-cy="manage-users-button"
|
||||
>
|
||||
Manage users
|
||||
</Link>
|
||||
)}
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,12 @@ import Information from '@/_ui/Icon/solidIcons/Information';
|
|||
import CodeHinter from '@/Editor/CodeEditor';
|
||||
|
||||
const isValidVariableName = (str) => /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(str);
|
||||
const isConstant = (str) => {
|
||||
if (typeof str !== 'string') {
|
||||
return false;
|
||||
}
|
||||
return str.includes('secrets.') || str.includes('constants.');
|
||||
};
|
||||
|
||||
const ParameterForm = ({
|
||||
darkMode,
|
||||
|
|
@ -38,7 +44,9 @@ const ParameterForm = ({
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isValidVariableName(name)) {
|
||||
if (isConstant(name)) {
|
||||
setError('Constants cannot be used in params');
|
||||
} else if (!isValidVariableName(name)) {
|
||||
setError('Variable name invalid');
|
||||
} else if (name && otherParams.some((param) => param.name === name.trim())) {
|
||||
setError('Variable name exists');
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import {
|
|||
import queryString from 'query-string';
|
||||
import ViewerLogoIcon from './Icons/viewer-logo.svg';
|
||||
import { DataSourceTypes } from './DataSourceManager/SourceComponents';
|
||||
import { resolveReferences, isQueryRunnable, isValidUUID } from '@/_helpers/utils';
|
||||
import { resolveReferences, isQueryRunnable, isValidUUID, Constants } from '@/_helpers/utils';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import _ from 'lodash';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
|
|
@ -283,13 +283,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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -438,11 +441,10 @@ class ViewerComponent extends React.Component {
|
|||
|
||||
let variablesResult;
|
||||
if (!isPublic) {
|
||||
const { constants } = await orgEnvironmentConstantService.getAll();
|
||||
const { constants } = await orgEnvironmentConstantService.getConstantsFromApp(slug);
|
||||
variablesResult = constants;
|
||||
} else {
|
||||
const { constants } = await orgEnvironmentConstantService.getConstantsFromPublicApp(slug);
|
||||
|
||||
variablesResult = constants;
|
||||
}
|
||||
|
||||
|
|
@ -564,15 +566,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: {
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ export const GlobalDataSourcesPage = ({ darkMode = false, updateSelectedDatasour
|
|||
const [addingDataSource, setAddingDataSource] = useState(false);
|
||||
const [suggestingDataSource, setSuggestingDataSource] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { admin } = authenticationService.currentSessionValue;
|
||||
const marketplaceEnabled = admin && window.public_config?.ENABLE_MARKETPLACE_FEATURE == 'true';
|
||||
const [modalProps, setModalProps] = useState({
|
||||
|
|
@ -296,34 +295,6 @@ export const GlobalDataSourcesPage = ({ darkMode = false, updateSelectedDatasour
|
|||
};
|
||||
|
||||
const renderCardGroup = (source, type) => {
|
||||
if (type === 'Plugins' && source.length === 0) {
|
||||
return (
|
||||
<div className="add-plugins-container">
|
||||
<div className="warning-container mb-2">
|
||||
<SolidIcon name="warning" />
|
||||
</div>
|
||||
<div className="tj-text-sm font-weight-500 tj-text">No plugins added</div>
|
||||
{admin && (
|
||||
<>
|
||||
<div className="tj-text-xsm font-weight-400 mt-2 mb-3">
|
||||
Browse through plugins in marketplace to add them as a Data Source.{' '}
|
||||
</div>
|
||||
<ButtonSolid
|
||||
onClick={() => {
|
||||
marketplaceEnabled
|
||||
? navigate('/integrations')
|
||||
: toast.error('Please enable marketplace to add plugins');
|
||||
}}
|
||||
style={{ margin: 'auto' }}
|
||||
variant="secondary"
|
||||
>
|
||||
Add plugins
|
||||
</ButtonSolid>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const addDataSourceBtn = (item) => (
|
||||
<ButtonSolid
|
||||
disabled={addingDataSource}
|
||||
|
|
@ -366,6 +337,36 @@ export const GlobalDataSourcesPage = ({ darkMode = false, updateSelectedDatasour
|
|||
titleClassName={'datasource-card-title'}
|
||||
/>
|
||||
))}
|
||||
{type === 'Plugins' && (
|
||||
<div style={{ height: '122px', width: '164px' }} className={`col-md-2 mb-4 `}>
|
||||
<div
|
||||
className="card add-plugin-card"
|
||||
role="button"
|
||||
onClick={() => {
|
||||
if (marketplaceEnabled) {
|
||||
window.open('/integrations', '_blank');
|
||||
} else {
|
||||
toast.error('Please enable marketplace to add plugins');
|
||||
}
|
||||
}}
|
||||
data-cy={`data-source-add-plugin`}
|
||||
>
|
||||
<div className="card-body">
|
||||
<center style={{ paddingTop: '5px' }}>
|
||||
<SolidIcon
|
||||
name="plus"
|
||||
fill={'var(--icon-default)'}
|
||||
width={35}
|
||||
height={35}
|
||||
style={{ marginBottom: '8px' }}
|
||||
/>
|
||||
<br></br>
|
||||
<span className={`datasource-card-title add-plugin-card-title`}>Add plugin</span>
|
||||
</center>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -48,14 +48,17 @@ const AppList = (props) => {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!props.isLoading && props.currentFolder.count === 0 && (
|
||||
{!props.isLoading && props.apps?.length === 0 && (
|
||||
<div className="text-center d-block">
|
||||
<EmptyFoldersIllustration className="mb-4" data-cy="empty-folder-image" />
|
||||
<span
|
||||
className={`d-block text-center text-body ${props.darkMode && 'text-white-50'}`}
|
||||
data-cy="empty-folder-text"
|
||||
>
|
||||
{t('homePage.thisFolderIsEmpty', 'This folder is empty')}
|
||||
{/* removed this error message display for now -> as it was leading to multiple message being shown in the UI*/}
|
||||
{/* {props.currentFolder?.count == 0
|
||||
? t('homePage.thisFolderIsEmpty', 'This folder is empty')
|
||||
: t('homePage.nonAccessibleFolderApps', 'You do not have access to any applications in this folder.')} */}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import cx from 'classnames';
|
|||
import { appsService, folderService, authenticationService, libraryAppService } from '@/_services';
|
||||
import { ConfirmDialog, AppModal } from '@/_components';
|
||||
import Select from '@/_ui/Select';
|
||||
import _, { sample, isEmpty } from 'lodash';
|
||||
import { Folders } from './Folders';
|
||||
import { BlankPage } from './BlankPage';
|
||||
import { toast } from 'react-hot-toast';
|
||||
|
|
@ -14,7 +15,6 @@ import HomeHeader from './Header';
|
|||
import Modal from './Modal';
|
||||
import configs from './Configs/AppIcon.json';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import { sample, isEmpty } from 'lodash';
|
||||
import ExportAppModal from './ExportAppModal';
|
||||
import Footer from './Footer';
|
||||
import { OrganizationList } from '@/_components/OrganizationManager/List';
|
||||
|
|
@ -25,7 +25,6 @@ import { getQueryParams } from '@/_helpers/routes';
|
|||
import { withRouter } from '@/_hoc/withRouter';
|
||||
import FolderFilter from './FolderFilter';
|
||||
import { APP_ERROR_TYPE } from '@/_helpers/error_constants';
|
||||
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
|
||||
import { fetchAndSetWindowTitle, pageTitles } from '@white-label/whiteLabelling';
|
||||
import HeaderSkeleton from '@/_ui/FolderSkeleton/HeaderSkeleton';
|
||||
|
||||
|
|
@ -304,23 +303,27 @@ class HomePageComponent extends React.Component {
|
|||
|
||||
canUserPerform(user, action, app) {
|
||||
const currentSession = authenticationService.currentSessionValue;
|
||||
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;
|
||||
|
|
@ -364,15 +367,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 = () => {
|
||||
|
|
@ -853,8 +856,12 @@ class HomePageComponent extends React.Component {
|
|||
{isLoading && !appSearchKey && <HeaderSkeleton />}
|
||||
{(meta?.total_count > 0 || appSearchKey) && (
|
||||
<>
|
||||
<HomeHeader onSearchSubmit={this.onSearchSubmit} darkMode={this.props.darkMode} />
|
||||
<div className="liner"></div>
|
||||
{!(isLoading && !appSearchKey) && (
|
||||
<>
|
||||
<HomeHeader onSearchSubmit={this.onSearchSubmit} darkMode={this.props.darkMode} />
|
||||
<div className="liner"></div>
|
||||
</>
|
||||
)}
|
||||
<div className="filter-container">
|
||||
<span>{currentFolder?.count ?? meta?.total_count} APPS</span>
|
||||
<div className="d-flex align-items-center">
|
||||
|
|
@ -900,7 +907,7 @@ class HomePageComponent extends React.Component {
|
|||
</span>
|
||||
</div>
|
||||
)}
|
||||
{(isLoading || meta.total_count > 0 || currentFolder.count === 0) && (
|
||||
{(isLoading || meta.total_count > 0 || !_.isEmpty(currentFolder)) && (
|
||||
<AppList
|
||||
apps={apps}
|
||||
canCreateApp={this.canCreateApp}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,188 @@
|
|||
import React from 'react';
|
||||
import '../../ManageGroupPermissionsV2/groupPermissions.theme.scss';
|
||||
import ModalBase from '@/_ui/Modal';
|
||||
import { AppsSelect } from '@/_ui/Modal/AppsSelect';
|
||||
import AppPermissionsActions from './AppPermissionActionContainer';
|
||||
import { OverlayTrigger, Tooltip } from 'react-bootstrap';
|
||||
|
||||
function AddEditResourcePermissionsModal({
|
||||
handleClose,
|
||||
handleConfirm,
|
||||
updateParentState,
|
||||
resourceType,
|
||||
currentState,
|
||||
show,
|
||||
title,
|
||||
confirmBtnProps,
|
||||
disableBuilderLevelUpdate,
|
||||
selectedApps,
|
||||
setSelectedApps,
|
||||
addableApps,
|
||||
darkMode,
|
||||
groupName,
|
||||
}) {
|
||||
const isCustom = currentState?.isCustom;
|
||||
const newPermissionName = currentState?.newPermissionName;
|
||||
const initialPermissionState = currentState?.initialPermissionState;
|
||||
const errors = currentState?.errors;
|
||||
const isAll = currentState?.isAll;
|
||||
|
||||
return (
|
||||
<ModalBase
|
||||
size="md"
|
||||
show={show}
|
||||
handleClose={handleClose}
|
||||
handleConfirm={handleConfirm}
|
||||
className="permission-manager-modal"
|
||||
title={title}
|
||||
confirmBtnProps={confirmBtnProps}
|
||||
darkMode={darkMode}
|
||||
>
|
||||
<div className="form-group mb-3">
|
||||
<label className="form-label bold-text" data-cy="permission-name-label">
|
||||
Permission name
|
||||
</label>
|
||||
<div className="tj-app-input">
|
||||
<input
|
||||
type="text"
|
||||
className={`form-control ${newPermissionName?.length == 50 ? 'error-input' : ''}`}
|
||||
placeholder={'Eg. Product analytics apps'}
|
||||
name="permissionName"
|
||||
value={newPermissionName}
|
||||
onChange={(e) => {
|
||||
let value = e.target.value;
|
||||
if (value?.length > 50) {
|
||||
value = value.slice(0, 50);
|
||||
}
|
||||
updateParentState(() => ({
|
||||
newPermissionName: value,
|
||||
}));
|
||||
}}
|
||||
data-cy="permission-name-input"
|
||||
/>
|
||||
<span className="text-danger" data-cy="permission-name-error">
|
||||
{errors['permissionName']}
|
||||
</span>
|
||||
</div>
|
||||
<div className={`mt-1 tj-text-xxsm ${newPermissionName?.length > 50 ? 'error-text' : ''}`}>
|
||||
<div data-cy="permission-name-help-text">Permission name must be unique and max 50 characters</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Till here */}
|
||||
<div className="form-group mb-3">
|
||||
<label className="form-label bold-text" data-cy="permission-label">
|
||||
Permission
|
||||
</label>
|
||||
<AppPermissionsActions
|
||||
handleClickEdit={() => {
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group mb-3">
|
||||
<label className="form-label bold-text" data-cy="resource-label">
|
||||
Resources
|
||||
</label>
|
||||
<div className="resources-container" data-cy="resources-container">
|
||||
<label className="form-check form-check-inline">
|
||||
<input
|
||||
className="form-check-input"
|
||||
type="radio"
|
||||
checked={isAll}
|
||||
onClick={() => {
|
||||
!isAll && updateParentState((prevState) => ({ isAll: !prevState.isAll, isCustom: !!prevState.isAll }));
|
||||
}}
|
||||
data-cy="all-apps-radio"
|
||||
/>
|
||||
<div>
|
||||
<span className="form-check-label" data-cy="all-apps-label">
|
||||
All apps
|
||||
</span>
|
||||
<span className="tj-text-xsm" data-cy="all-apps-info-text">
|
||||
This will select all apps in the workspace including any new apps created
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
<OverlayTrigger
|
||||
overlay={
|
||||
disableBuilderLevelUpdate || resourceType === '' || groupName === 'builder' ? (
|
||||
<Tooltip id="tooltip-disable-edit-update">Use custom groups to select custom resources</Tooltip>
|
||||
) : (
|
||||
<span></span>
|
||||
)
|
||||
}
|
||||
placement="left"
|
||||
>
|
||||
<label className="form-check form-check-inline">
|
||||
<input
|
||||
className="form-check-input"
|
||||
type="radio"
|
||||
disabled={addableApps.length === 0 || disableBuilderLevelUpdate || groupName === 'builder'}
|
||||
checked={isCustom}
|
||||
onClick={() => {
|
||||
!isCustom &&
|
||||
updateParentState((prevState) => ({ isCustom: !prevState.isCustom, isAll: prevState.isCustom }));
|
||||
}}
|
||||
data-cy="custom-radio"
|
||||
/>
|
||||
<div>
|
||||
<span
|
||||
className="form-check-label"
|
||||
style={{ color: disableBuilderLevelUpdate ? 'var(--text-disabled)' : '' }}
|
||||
data-cy="custom-label"
|
||||
>
|
||||
Custom
|
||||
</span>
|
||||
<span
|
||||
className="tj-text-xsm"
|
||||
style={{ color: disableBuilderLevelUpdate ? 'var(--text-disabled)' : '' }}
|
||||
data-cy="custom-info-text"
|
||||
>
|
||||
Select specific applications you want to add to the group
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
</OverlayTrigger>
|
||||
<AppsSelect
|
||||
disabled={!isCustom}
|
||||
allowSelectAll={true}
|
||||
value={selectedApps}
|
||||
onChange={setSelectedApps}
|
||||
options={addableApps}
|
||||
data-value="test"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ModalBase>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddEditResourcePermissionsModal;
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
import React from 'react';
|
||||
import '../../ManageGroupPermissionsV2/groupPermissions.theme.scss';
|
||||
import { OverlayTrigger, Tooltip } from 'react-bootstrap';
|
||||
|
||||
function AppPermissionsActions({
|
||||
handleClickEdit,
|
||||
handleClickView,
|
||||
handleHideFromDashboard,
|
||||
disableBuilderLevelUpdate,
|
||||
initialPermissionState,
|
||||
}) {
|
||||
return (
|
||||
<div className="type-container">
|
||||
<div className="left-container">
|
||||
<OverlayTrigger
|
||||
overlay={
|
||||
disableBuilderLevelUpdate ? (
|
||||
<Tooltip id="tooltip-disable-edit-update">End-user cannot have edit permission</Tooltip>
|
||||
) : (
|
||||
<span></span>
|
||||
)
|
||||
}
|
||||
placement="left"
|
||||
>
|
||||
<label className="form-check form-check-inline">
|
||||
<input
|
||||
className="form-check-input"
|
||||
type="radio"
|
||||
disabled={disableBuilderLevelUpdate}
|
||||
checked={initialPermissionState.canEdit}
|
||||
onClick={() => {
|
||||
!initialPermissionState.canEdit && handleClickEdit();
|
||||
}}
|
||||
data-cy="edit-permission-radio"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<span
|
||||
className="form-check-label"
|
||||
style={{ color: disableBuilderLevelUpdate ? 'var(--text-disabled)' : '' }}
|
||||
data-cy="edit-permission-label"
|
||||
>
|
||||
Edit
|
||||
</span>
|
||||
<span
|
||||
className="tj-text-xsm"
|
||||
style={{ color: disableBuilderLevelUpdate ? 'var(--text-disabled)' : '' }}
|
||||
data-cy="edit-permission-info-text"
|
||||
>
|
||||
Access to app builder
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
</OverlayTrigger>
|
||||
</div>
|
||||
<div className="right-container">
|
||||
<label className="form-check form-check-inline">
|
||||
<input
|
||||
className="form-check-input"
|
||||
type="radio"
|
||||
disabled={disableBuilderLevelUpdate}
|
||||
checked={initialPermissionState.canView}
|
||||
onClick={() => {
|
||||
!initialPermissionState.canView && handleClickView();
|
||||
}}
|
||||
data-cy="view-permission-radio"
|
||||
/>
|
||||
<div>
|
||||
<span
|
||||
className="form-check-label"
|
||||
style={{ color: disableBuilderLevelUpdate ? 'var(--text-disabled)' : '' }}
|
||||
data-cy="view-permission-label"
|
||||
>
|
||||
View
|
||||
</span>
|
||||
<span
|
||||
className="tj-text-xsm"
|
||||
style={{ color: disableBuilderLevelUpdate ? 'var(--text-disabled)' : '' }}
|
||||
data-cy="view-permission-info-text"
|
||||
>
|
||||
Only access released version of apps
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
<label className="form-check form-check-inline">
|
||||
<input
|
||||
className="form-check-input"
|
||||
type="checkbox"
|
||||
disabled={!initialPermissionState.canView}
|
||||
checked={initialPermissionState.hideFromDashboard}
|
||||
onClick={() => {
|
||||
handleHideFromDashboard();
|
||||
}}
|
||||
data-cy="hide-from-dashboard-permission-input"
|
||||
/>
|
||||
<div>
|
||||
<span className={`form-check-label`} data-cy="hide-from-dashboard-permission-label">
|
||||
Hide from dashboard
|
||||
</span>
|
||||
<span className="tj-text-xsm" data-cy="hide-from-dashboard-permission-info-text">
|
||||
App will be accessible by URL only
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AppPermissionsActions;
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
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 ? (
|
||||
<OverlayTrigger
|
||||
rootClose={true}
|
||||
trigger="click"
|
||||
placement={'bottom'}
|
||||
overlay={
|
||||
<div className={`settings-card tj-text card ${this.props.darkMode && 'dark-theme'}`}>
|
||||
<ButtonSolid
|
||||
variant="tertiary"
|
||||
iconWidth="17"
|
||||
fill="var(--slate9)"
|
||||
className="apps-remove-btn permission-type remove-decoration tj-text-xsm font-weight-600"
|
||||
leftIcon="dashboard"
|
||||
onClick={() => {
|
||||
openAddPermissionModal();
|
||||
}}
|
||||
>
|
||||
Apps
|
||||
</ButtonSolid>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className={'cursor-pointer'}>
|
||||
<ButtonSolid
|
||||
variant="tertiary"
|
||||
iconWidth="17"
|
||||
fill="var(--slate9)"
|
||||
className="add-icon tj-text-xsm font-weight-600"
|
||||
leftIcon="plus"
|
||||
disabled={currentGroupPermission.name === 'admin'}
|
||||
>
|
||||
Add permission
|
||||
</ButtonSolid>
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
) : (
|
||||
<div className={'cursor-pointer'}>
|
||||
<ButtonSolid
|
||||
variant="tertiary"
|
||||
iconWidth="17"
|
||||
fill="var(--slate9)"
|
||||
className="add-icon tj-text-xsm font-weight-600"
|
||||
leftIcon="plus"
|
||||
disabled={currentGroupPermission.name === 'admin'}
|
||||
onClick={() => {
|
||||
openAddPermissionModal();
|
||||
}}
|
||||
data-cy="add-apps-buton"
|
||||
>
|
||||
Add apps
|
||||
</ButtonSolid>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddResourcePermissionsMenu;
|
||||
174
frontend/src/ManageGranularAccess/AppResourcePermission.jsx
Normal file
174
frontend/src/ManageGranularAccess/AppResourcePermission.jsx
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
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';
|
||||
import OverflowTooltip from '@/_components/OverflowTooltip';
|
||||
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 (permissions.isAll) apps = ['All apps'];
|
||||
|
||||
return (
|
||||
<div
|
||||
className="manage-resource-permission"
|
||||
onMouseEnter={() => {
|
||||
setHover(true);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setHover(false);
|
||||
}}
|
||||
onClick={() => {
|
||||
!isRoleGroup && !notClickable && openEditPermissionModal(permissions);
|
||||
}}
|
||||
>
|
||||
<div className="resource-name d-flex">
|
||||
<SolidIcon name="app" width="20px" className="resource-icon" />
|
||||
|
||||
<div className="resource-text" data-cy={`${permissions.name.toLowerCase().replace(/\s+/g, '-')}-text`}>
|
||||
<OverflowTooltip width={160}>{` ${permissions.name}`}</OverflowTooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-muted">
|
||||
<div className="d-flex apps-permission-wrap flex-column">
|
||||
<OverlayTrigger
|
||||
overlay={
|
||||
disableEditUpdate ? (
|
||||
<Tooltip id="tooltip-disable-edit-update">End-user cannot have edit permission</Tooltip>
|
||||
) : (
|
||||
<span></span>
|
||||
)
|
||||
}
|
||||
placement="top"
|
||||
>
|
||||
<div
|
||||
onMouseEnter={() => {
|
||||
setNotClickable(true);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setNotClickable(false);
|
||||
}}
|
||||
>
|
||||
<label className="form-check form-check-inline">
|
||||
<input
|
||||
className="form-check-input"
|
||||
type="radio"
|
||||
onClick={() => {
|
||||
!appsPermissions.canEdit &&
|
||||
updateOnlyGranularPermissions(permissions, {
|
||||
canEdit: !appsPermissions.canEdit,
|
||||
canView: appsPermissions.canEdit,
|
||||
...(!appsPermissions.canEdit && { hideFromDashboard: false }),
|
||||
});
|
||||
}}
|
||||
checked={appsPermissions.canEdit}
|
||||
disabled={isRoleGroup || disableEditUpdate}
|
||||
data-cy="app-edit-radio"
|
||||
/>
|
||||
<span className="form-check-label" data-cy="app-edit-label">
|
||||
{'Edit'}
|
||||
</span>
|
||||
{/* <span class={`text-muted tj-text-xxsm ${isRoleGroup && 'check-label-disable'}`}>Create apps in this workspace</span> */}
|
||||
<span class={`tj-text-xxsm`} data-cy="app-edit-helper-text">
|
||||
Access to app builder
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
<div
|
||||
onMouseEnter={() => {
|
||||
setNotClickable(true);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setNotClickable(false);
|
||||
}}
|
||||
>
|
||||
<label className="form-check form-check-inline">
|
||||
<input
|
||||
className="form-check-input"
|
||||
type="radio"
|
||||
onClick={() => {
|
||||
!appsPermissions.canView &&
|
||||
updateOnlyGranularPermissions(permissions, {
|
||||
canView: !appsPermissions.canView,
|
||||
canEdit: appsPermissions.canView,
|
||||
});
|
||||
}}
|
||||
checked={appsPermissions.canView}
|
||||
disabled={isRoleGroup || disableEditUpdate}
|
||||
data-cy="app-view-radio"
|
||||
/>
|
||||
<span className="form-check-label" data-cy="app-view-label">
|
||||
{'View'}
|
||||
</span>
|
||||
<span class={`tj-text-xxsm`} data-cy="app-view-helper-text">
|
||||
Only access released version of apps
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
onMouseEnter={() => {
|
||||
setNotClickable(true);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setNotClickable(false);
|
||||
}}
|
||||
>
|
||||
<label className="form-check form-check-inline">
|
||||
<input
|
||||
className="form-check-input"
|
||||
type="checkbox"
|
||||
onChange={() => {
|
||||
updateOnlyGranularPermissions(permissions, {
|
||||
hideFromDashboard: !appsPermissions.hideFromDashboard,
|
||||
});
|
||||
}}
|
||||
checked={appsPermissions.hideFromDashboard}
|
||||
disabled={isRoleGroup || !appsPermissions.canView}
|
||||
data-cy="app-hide-from-dashboard-radio"
|
||||
/>
|
||||
<span className="form-check-label" data-cy="app-hide-from-dashboard-label">
|
||||
{'Hide from dashbaord'}
|
||||
</span>
|
||||
<span class={`tj-text-xxsm`} data-cy="app-hide-from-dashboard-helper-text">
|
||||
App will be accessible by URL only
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<GroupChipTD groups={apps} />
|
||||
</div>
|
||||
<div className="edit-icon-container">
|
||||
{onHover && (
|
||||
<ButtonSolid
|
||||
leftIcon="editrectangle"
|
||||
className="edit-permission-custom"
|
||||
iconWidth="14"
|
||||
onClick={() => {
|
||||
openEditPermissionModal(permissions);
|
||||
}}
|
||||
disabled={isRoleGroup}
|
||||
data-cy="edit-permission-button"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AppResourcePermissions;
|
||||
546
frontend/src/ManageGranularAccess/index.jsx
Normal file
546
frontend/src/ManageGranularAccess/index.jsx
Normal file
|
|
@ -0,0 +1,546 @@
|
|||
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 Spinner from 'react-bootstrap/Spinner';
|
||||
|
||||
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();
|
||||
toast.success('Permission created successfully!');
|
||||
})
|
||||
.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,
|
||||
});
|
||||
return;
|
||||
}
|
||||
toast.error(error, {
|
||||
style: {
|
||||
maxWidth: '500px',
|
||||
},
|
||||
});
|
||||
});
|
||||
// .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 update the permissions',
|
||||
errorMessage: error.error,
|
||||
errorIconName: 'usergear',
|
||||
errorListItems: error.data,
|
||||
showAddPermissionModal: false,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
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, {
|
||||
style: {
|
||||
maxWidth: '500px',
|
||||
},
|
||||
});
|
||||
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 (
|
||||
<div className="manage-granular-permissions-info">
|
||||
<p
|
||||
className="tj-text-xsm"
|
||||
style={{ display: 'flex', alignItems: 'center', gap: '4px' }}
|
||||
data-cy="helper-text-admin-app-access"
|
||||
>
|
||||
<SolidIcon name="informationcircle" fill="#3E63DD" /> {text}
|
||||
<a
|
||||
style={{ margin: '0', padding: '0', textDecoration: 'underline', color: '#3E63DD' }}
|
||||
href="https://docs.tooljet.com/docs/tutorial/manage-users-groups/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
read documentation
|
||||
</a>{' '}
|
||||
to know more !
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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 {
|
||||
showAddPermissionModal,
|
||||
selectedApps,
|
||||
isCustom,
|
||||
granularPermissions,
|
||||
isLoading,
|
||||
addableApps,
|
||||
modalTitle,
|
||||
modalType,
|
||||
newPermissionName,
|
||||
showAutoRoleChangeModal,
|
||||
autoRoleChangeModalList,
|
||||
autoRoleChangeMessageType,
|
||||
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 (
|
||||
<div>
|
||||
<ConfirmDialog
|
||||
show={deleteConfirmationModal}
|
||||
message={'This permission will be permanently deleted. Do you want to continue?'}
|
||||
confirmButtonLoading={deletingPermissions}
|
||||
onConfirm={() => this.deleteGranularPermissions()}
|
||||
onCancel={() => {
|
||||
this.setState({ deleteConfirmationModal: false, deletingPermissions: false });
|
||||
}}
|
||||
darkMode={this.props.darkMode}
|
||||
/>
|
||||
<ChangeRoleModal
|
||||
showAutoRoleChangeModal={showAutoRoleChangeModal}
|
||||
autoRoleChangeModalList={autoRoleChangeModalList}
|
||||
autoRoleChangeMessageType={autoRoleChangeMessageType}
|
||||
handleAutoRoleChangeModalClose={this.handleAutoRoleChangeModalClose}
|
||||
handleConfirmation={
|
||||
updateType === 'ONLY_PERMISSIONS'
|
||||
? this.handleConfirmAutoRoleChangeOnlyGroupUpdate
|
||||
: this.handleConfirmAutoRoleChangeGroupUpdate
|
||||
}
|
||||
darkMode={this.props.darkMode}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<AddEditResourcePermissionsModal
|
||||
handleClose={this.closeAddPermissionModal}
|
||||
handleConfirm={
|
||||
modalType === 'add'
|
||||
? this.createGranularPermissions
|
||||
: () => {
|
||||
this.updateGranularPermissions();
|
||||
}
|
||||
}
|
||||
updateParentState={this.updateState}
|
||||
resourceType="app"
|
||||
currentState={this.state}
|
||||
show={showAddPermissionModal}
|
||||
title={
|
||||
<div className="my-3 permission-manager-title" data-cy="modal-title">
|
||||
<span className="font-weight-500">
|
||||
<SolidIcon name="apps" />
|
||||
</span>
|
||||
<div className="tj-text-md font-weight-500" data-cy="modal-title">
|
||||
{modalTitle}
|
||||
</div>
|
||||
{modalType === 'edit' && !isRoleGroup && (
|
||||
<div className="delete-icon-cont">
|
||||
<ButtonSolid
|
||||
leftIcon="delete"
|
||||
iconWidth="15px"
|
||||
className="icon-class"
|
||||
variant="tertiary"
|
||||
onClick={() => {
|
||||
this.setState({
|
||||
deleteConfirmationModal: true,
|
||||
showAddPermissionModal: false,
|
||||
});
|
||||
}}
|
||||
data-cy="delete-button"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
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}
|
||||
groupName={currentGroupPermission.name}
|
||||
/>
|
||||
{!granularPermissions.length && !isLoading ? (
|
||||
<div className="empty-container">
|
||||
<div className="icon-container" data-cy="empty-page-svg">
|
||||
<SolidIcon name="granularaccess" />
|
||||
</div>
|
||||
<p className="my-2 tj-text-md font-weight-500" data-cy="empty-page-title">
|
||||
No permissions added yet
|
||||
</p>
|
||||
<p className="tj-text-xsm mb-2" data-cy="empty-page-info-text">
|
||||
Add assets to configure granular, asset-level permissions for this user group
|
||||
</p>
|
||||
<div className="menu">
|
||||
<AddResourcePermissionsMenu
|
||||
openAddPermissionModal={this.openAddPermissionModal}
|
||||
resourcesOptions={resourcesOptions}
|
||||
currentGroupPermission={currentGroupPermission}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{showPermissionInfo && this.showPermissionText(currentGroupPermission)}
|
||||
<div className="manage-granular-permission-header">
|
||||
<p data-cy="name-header" className="tj-text-xsm">
|
||||
{'Name'}
|
||||
</p>
|
||||
<p data-cy="permissions-header" className="tj-text-xsm">
|
||||
{'Permission'}
|
||||
</p>
|
||||
<p data-cy="resource-header" className="tj-text-xsm">
|
||||
{'Resource'}
|
||||
</p>
|
||||
</div>
|
||||
<div className={showPermissionInfo ? 'permission-body-one' : 'permission-body-two'}>
|
||||
{isLoading ? (
|
||||
<div
|
||||
className="d-flex justify-content-center align-items-center"
|
||||
style={{ height: 'calc(100vh - 470px)' }}
|
||||
>
|
||||
<Spinner variant="primary" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{granularPermissions.map((permissions, index) => (
|
||||
<AppResourcePermissions
|
||||
updateOnlyGranularPermissions={this.updateOnlyGranularPermissions}
|
||||
permissions={permissions}
|
||||
currentGroupPermission={currentGroupPermission}
|
||||
openEditPermissionModal={this.openEditPermissionModal}
|
||||
key={index}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{granularPermissions.length > 0 && (
|
||||
<div className="side-button-cont">
|
||||
<AddResourcePermissionsMenu
|
||||
openAddPermissionModal={this.openAddPermissionModal}
|
||||
resourcesOptions={resourcesOptions}
|
||||
currentGroupPermission={currentGroupPermission}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const ManageGranularAccess = withTranslation()(ManageGranularAccessComponent);
|
||||
|
|
@ -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 = (
|
||||
<p className="tj-text-sm">
|
||||
Granting this permission to the user group will result in a role change for the following user(s) from{' '}
|
||||
<b>end-users</b> to <b>builders</b>. Are you sure you want to continue?
|
||||
</p>
|
||||
);
|
||||
const addUserMessage = (
|
||||
<p className="tj-text-sm">
|
||||
Adding the following user(s) to this group will change their default group from <b>end-users</b> to{' '}
|
||||
<b>builders</b>. Are you sure you want to continue?
|
||||
</p>
|
||||
);
|
||||
const message = type === 'USER_ROLE_CHANGE_ADD_USERS' ? addUserMessage : changePermissionMessage;
|
||||
return message;
|
||||
};
|
||||
|
||||
const renderUserChangeTitle = (type) => {
|
||||
const addUserTitle = (
|
||||
<div className="my-3" data-cy="modal-title">
|
||||
<span className="tj-text-md font-weight-500">Add user(s)</span>
|
||||
</div>
|
||||
);
|
||||
const updatePermissionTitile = (
|
||||
<div className="my-3" data-cy="modal-title">
|
||||
<span className="tj-text-md font-weight-500"> Change in user role</span>
|
||||
</div>
|
||||
);
|
||||
const message = type === 'USER_ROLE_CHANGE_ADD_USERS' ? addUserTitle : updatePermissionTitile;
|
||||
return message;
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalBase
|
||||
title={renderUserChangeTitle(autoRoleChangeMessageType)}
|
||||
handleConfirm={handleConfirmation}
|
||||
confirmBtnProps={{ title: 'Continue', tooltipMessage: false }}
|
||||
show={showAutoRoleChangeModal}
|
||||
handleClose={handleAutoRoleChangeModalClose}
|
||||
darkMode={darkMode}
|
||||
isLoading={isLoading}
|
||||
className="edit-role-confirm"
|
||||
>
|
||||
<>
|
||||
{renderUserChangeMessage(autoRoleChangeMessageType)}
|
||||
<p></p>
|
||||
<div className="item-list">
|
||||
{autoRoleChangeModalList.map((item, index) => (
|
||||
<div key={index}>
|
||||
<span className="tj-text-sm">{`${index + 1}. ${item}`}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
</ModalBase>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChangeRoleModal;
|
||||
86
frontend/src/ManageGroupPermissionResourcesV2/constant.js
Normal file
86
frontend/src/ManageGroupPermissionResourcesV2/constant.js
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import React from 'react';
|
||||
|
||||
const endUserToAdminMessage =
|
||||
"Updating the user's details will change their role from end-user to admin. Are you sure you want to continue?";
|
||||
const endUserToBuilderMessage =
|
||||
"Updating the user's details will change their role from end-user to builder. Are you sure you want to continue?";
|
||||
|
||||
export const EDIT_ROLE_MESSAGE = {
|
||||
admin: {
|
||||
builder: () => {
|
||||
return (
|
||||
<div>
|
||||
<p className="tj-text-sm" style={{ marginBottom: '10px' }}>
|
||||
Changing your user default group from admin to builder will revoke your access to settings.
|
||||
</p>
|
||||
<p className="tj-text-sm">Are you sure you want to continue?</p>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
'end-user': (isPaidPlan) => {
|
||||
return (
|
||||
<div>
|
||||
<p className="tj-text-sm" style={{ marginBottom: '10px' }}>
|
||||
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.'}
|
||||
</p>
|
||||
<p className="tj-text-sm">Are you sure you want to continue?</p>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
builder: {
|
||||
'end-user': (isPaidPlan) => {
|
||||
return (
|
||||
<div>
|
||||
{isPaidPlan && (
|
||||
<p className="tj-text-sm" style={{ marginBottom: '10px' }}>
|
||||
Changing user default group from builder to end-user will affect the count of users covered by your plan.
|
||||
</p>
|
||||
)}
|
||||
<p className="tj-text-sm">
|
||||
This will also remove the user from any custom groups with builder-like permissions.
|
||||
</p>
|
||||
<p className="tj-text-sm">Are you sure you want to continue?</p>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
admin: () => {
|
||||
return (
|
||||
<div>
|
||||
<p className="tj-text-sm" style={{ marginBottom: '10px' }}>
|
||||
Changing user role from builder to admin will grant access to all resources and settings.
|
||||
</p>
|
||||
|
||||
<p className="tj-text-sm">Are you sure you want to continue?</p>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
'end-user': {
|
||||
builder: (isPaidPlan) => {
|
||||
return (
|
||||
<div>
|
||||
{isPaidPlan && (
|
||||
<p className="tj-text-sm" style={{ marginBottom: '10px' }}>
|
||||
Changing user default group from end-user to builder will affect the count of users covered by your plan.
|
||||
</p>
|
||||
)}
|
||||
<p className="tj-text-sm">{endUserToBuilderMessage}</p>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
admin: (isPaidPlan) => {
|
||||
return (
|
||||
<div>
|
||||
{isPaidPlan && (
|
||||
<p className="tj-text-sm" style={{ marginBottom: '10px' }}>
|
||||
Changing user default group from end-user to admin will affect the count of users covered by your plan.
|
||||
</p>
|
||||
)}
|
||||
<p className="tj-text-sm">{endUserToAdminMessage}</p>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
1033
frontend/src/ManageGroupPermissionResourcesV2/index.jsx
Normal file
1033
frontend/src/ManageGroupPermissionResourcesV2/index.jsx
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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')
|
||||
}
|
||||
>
|
||||
<form
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
import React from 'react';
|
||||
import { Modal } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import SolidIcon from '@/_ui/Icon/SolidIcons';
|
||||
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
|
||||
import '../groupPermissions.theme.scss';
|
||||
|
||||
function EditRoleErrorModal({
|
||||
darkMode,
|
||||
errorTitle = '',
|
||||
listItems = [],
|
||||
errorMessage = '',
|
||||
show = false,
|
||||
iconName = '',
|
||||
onClose,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className={`edit-role-modal ${darkMode ? 'dark-mode' : ''}`}
|
||||
aria-labelledby="contained-modal-title-vcenter"
|
||||
centered
|
||||
show={show}
|
||||
>
|
||||
<Modal.Header>
|
||||
<div className="remove-icon-container">
|
||||
<ButtonSolid
|
||||
className="close-btn"
|
||||
leftIcon="remove"
|
||||
onClick={() => onClose()}
|
||||
data-cy="close-button"
|
||||
iconWidth="20"
|
||||
/>
|
||||
</div>
|
||||
<div className="icon-class" data-cy="modal-icon">
|
||||
<SolidIcon name={iconName} width="32" fill="#E54D2E" />
|
||||
</div>
|
||||
<span className="header-text" data-cy="modal-header">
|
||||
{errorTitle}
|
||||
</span>
|
||||
<p data-cy="modal-message">{errorMessage}</p>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<div className="item-list">
|
||||
{listItems.map((item, index) => (
|
||||
<div key={index}>
|
||||
<span className="tj-text-sm">{`${index + 1}. ${item}`}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditRoleErrorModal;
|
||||
|
|
@ -0,0 +1,799 @@
|
|||
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?.trim() == this.state.newGroupName?.trim();
|
||||
});
|
||||
this.setState({ selectedGroup: currentUpdatedGroup.name });
|
||||
return currentUpdatedGroup.id;
|
||||
};
|
||||
|
||||
duplicateGroup = () => {
|
||||
const { groupDuplicateOption, groupToDuplicate } = this.state;
|
||||
this.setState({ isDuplicatingGroup: 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,
|
||||
creatingGroup: 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 (
|
||||
<div
|
||||
{...props}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
...props.style,
|
||||
}}
|
||||
>
|
||||
<Popover
|
||||
id="popover-group-menu"
|
||||
className={this.props.darkMode ? 'popover-group-menu dark-theme' : 'popover-group-menu'}
|
||||
placement="bottom"
|
||||
>
|
||||
<Popover.Body bsPrefix="popover-body">
|
||||
<div>
|
||||
<Field
|
||||
customClass={this.props.darkMode ? 'dark-theme' : ''}
|
||||
leftIcon="copy"
|
||||
leftIconWidth="20"
|
||||
leftViewBox="0 0 20 20"
|
||||
text={'Duplicate group'}
|
||||
onClick={duplicateGroup}
|
||||
/>
|
||||
<Field
|
||||
customClass={this.props.darkMode ? 'dark-theme' : ''}
|
||||
leftIcon="delete"
|
||||
leftIconWidth="18"
|
||||
leftIconHeight="18"
|
||||
leftViewBox="0 0 20 20"
|
||||
text={'Delete group'}
|
||||
tooltipId="tooltip-for-delete"
|
||||
tooltipContent="Cannot delete default group"
|
||||
onClick={isDefaultGroup ? {} : deleteGroup}
|
||||
buttonDisable={isDefaultGroup}
|
||||
darkMode={this.props.darkMode}
|
||||
/>
|
||||
</div>
|
||||
</Popover.Body>
|
||||
</Popover>
|
||||
{(groupName == 'all_users' || groupName == 'admin' || groupName == 'builder' || groupName == 'end-user') && (
|
||||
<Tooltip
|
||||
id="tooltip-for-delete"
|
||||
className="tooltip"
|
||||
place="left"
|
||||
style={{
|
||||
zIndex: 99999,
|
||||
}}
|
||||
show={isDefaultGroup}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
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({
|
||||
newGroupName: value?.slice(0, 50).trim(),
|
||||
isSaveBtnDisabled: false,
|
||||
});
|
||||
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 = () => {
|
||||
const regex = /^[a-zA-Z0-9_ -]+$/;
|
||||
if (!regex.test(this.state.newGroupName)) {
|
||||
toast.error('Group name can only contain letters, numbers, underscores and hyphens');
|
||||
return;
|
||||
}
|
||||
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',
|
||||
},
|
||||
});
|
||||
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, {
|
||||
style: {
|
||||
maxWidth: '500px',
|
||||
},
|
||||
});
|
||||
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 (
|
||||
<ErrorBoundary showFallback={true}>
|
||||
<div className="wrapper org-users-page animation-fade">
|
||||
<div className="org-users-page-container">
|
||||
<ConfirmDialog
|
||||
show={showGroupDeletionConfirmation}
|
||||
message={'This group will be permanently deleted. Do you want to continue?'}
|
||||
confirmButtonLoading={isDeletingGroup}
|
||||
onConfirm={() => this.executeGroupDeletion()}
|
||||
onCancel={() => this.cancelDeleteGroupDialog()}
|
||||
darkMode={this.props.darkMode}
|
||||
/>
|
||||
<ModalBase
|
||||
show={showDuplicateGroupModal}
|
||||
handleConfirm={this.duplicateGroup}
|
||||
handleClose={this.toggleShowDuplicateModal}
|
||||
title="Duplicate group"
|
||||
confirmBtnProps={{ title: 'Duplicate', disabled: allFalse, tooltipMessage: false }}
|
||||
isLoading={isDuplicatingGroup}
|
||||
cancelDisabled={isDuplicatingGroup}
|
||||
data-cy="modal-title"
|
||||
darkMode={this.props.darkMode}
|
||||
>
|
||||
<div className="tj-text" data-cy="modal-message">
|
||||
Duplicate the following parts of the group
|
||||
</div>
|
||||
<div className="group-duplcate-modal-body">
|
||||
<div className="row check-row">
|
||||
<div className="col-1 ">
|
||||
<input
|
||||
class="form-check-input"
|
||||
checked={addUsers}
|
||||
type="checkbox"
|
||||
onChange={() => {
|
||||
this.setState((prevState) => ({
|
||||
groupDuplicateOption: {
|
||||
...prevState.groupDuplicateOption,
|
||||
addUsers: !prevState.groupDuplicateOption.addUsers,
|
||||
},
|
||||
}));
|
||||
}}
|
||||
data-cy="users-check-input"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-11">
|
||||
<div className="tj-text " data-cy="users-label">
|
||||
Users
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row check-row">
|
||||
<div className="col-1 ">
|
||||
<input
|
||||
class="form-check-input"
|
||||
checked={addPermission}
|
||||
type="checkbox"
|
||||
onChange={() => {
|
||||
this.setState((prevState) => ({
|
||||
groupDuplicateOption: {
|
||||
...prevState.groupDuplicateOption,
|
||||
addPermission: !prevState.groupDuplicateOption.addPermission,
|
||||
},
|
||||
}));
|
||||
}}
|
||||
data-cy="permissions-check-input"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-11">
|
||||
<div className="tj-text " data-cy="permissions-label">
|
||||
Permissions
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row check-row">
|
||||
<div className="col-1 ">
|
||||
<input
|
||||
class="form-check-input"
|
||||
checked={addApps}
|
||||
type="checkbox"
|
||||
onChange={() => {
|
||||
this.setState((prevState) => ({
|
||||
groupDuplicateOption: {
|
||||
...prevState.groupDuplicateOption,
|
||||
addApps: !prevState.groupDuplicateOption.addApps,
|
||||
},
|
||||
}));
|
||||
}}
|
||||
data-cy="apps-check-input"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-11">
|
||||
<div className="tj-text " data-cy="apps-label">
|
||||
Apps
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalBase>
|
||||
<div className="d-flex groups-btn-container">
|
||||
<p className="tj-text" data-cy="page-title">
|
||||
{groups?.length} Groups
|
||||
</p>
|
||||
{!showNewGroupForm && !showGroupNameUpdateForm && (
|
||||
<ButtonSolid
|
||||
className="btn btn-primary create-new-group-button"
|
||||
onClick={(e) => {
|
||||
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'
|
||||
)}
|
||||
</ButtonSolid>
|
||||
)}
|
||||
</div>
|
||||
<Modal
|
||||
show={showNewGroupForm || showGroupNameUpdateForm}
|
||||
closeModal={() =>
|
||||
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')
|
||||
}
|
||||
>
|
||||
<form
|
||||
id="my-form"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
if (showNewGroupForm) {
|
||||
this.createGroup();
|
||||
} else {
|
||||
this.executeGroupUpdation();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="form-group mb-3 ">
|
||||
<div className="row">
|
||||
<div className="col tj-app-input">
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
className={`form-control ${this.state.newGroupName?.length >= 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
|
||||
/>
|
||||
<span className="tj-text-xxsm" style={grounNameErrorStyle} data-cy="group-name-info-text">
|
||||
{this.state.groupNameMessage}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-footer d-flex create-group-modal-footer">
|
||||
<ButtonSolid
|
||||
onClick={() =>
|
||||
this.setState({
|
||||
showNewGroupForm: false,
|
||||
showGroupNameUpdateForm: false,
|
||||
newGroupName: null,
|
||||
})
|
||||
}
|
||||
disabled={creatingGroup}
|
||||
data-cy="cancel-button"
|
||||
variant="tertiary"
|
||||
>
|
||||
{this.props.t('globals.cancel', 'Cancel')}
|
||||
</ButtonSolid>
|
||||
<ButtonSolid
|
||||
type="submit"
|
||||
id="my-form"
|
||||
disabled={creatingGroup || this.state.isSaveBtnDisabled}
|
||||
data-cy="create-group-button"
|
||||
isLoading={creatingGroup || isUpdatingGroupName}
|
||||
leftIcon="plus"
|
||||
fill={creatingGroup || this.state.isSaveBtnDisabled ? '#4C5155' : '#FDFDFE'}
|
||||
>
|
||||
{showGroupNameUpdateForm
|
||||
? this.props.t('globals.save', 'Save')
|
||||
: this.props.t('header.organization.menus.manageGroups.permissions.createGroup', 'Create Group')}
|
||||
</ButtonSolid>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
<div className="org-users-page-card-wrap">
|
||||
<div className="org-users-page-sidebar">
|
||||
<div className="default-group-list-container">
|
||||
<div className="mb-2 d-flex align-items-center">
|
||||
<SolidIcon name="usergear" />
|
||||
<span className="ml-1 group-title" data-cy="user-role-title">
|
||||
USER ROLE
|
||||
</span>
|
||||
</div>
|
||||
{defaultGroups.map((permissionGroup) => {
|
||||
return (
|
||||
<FolderList
|
||||
key={permissionGroup.id}
|
||||
listId={permissionGroup.id}
|
||||
overlayFunctionParam={{
|
||||
id: permissionGroup.id,
|
||||
groupName: permissionGroup.name,
|
||||
}}
|
||||
selectedItem={this.state.selectedGroup == this.humanizeifDefaultGroupName(permissionGroup.name)}
|
||||
onClick={() => {
|
||||
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, '-')}
|
||||
>
|
||||
<span>
|
||||
<OverflowTooltip>{this.humanizeifDefaultGroupName(permissionGroup.name)}</OverflowTooltip>
|
||||
</span>
|
||||
</FolderList>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div>
|
||||
{!showGroupSearchBar ? (
|
||||
<div className="mb-2 d-flex align-items-center">
|
||||
<SolidIcon name="usergroup" width="18px" fill="#889096" />
|
||||
<span className="ml-1 group-title" data-cy="custom-groups-title">
|
||||
CUSTOM GROUPS
|
||||
</span>
|
||||
<div className="create-group-cont">
|
||||
<ButtonSolid
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
this.setState({ showGroupSearchBar: true });
|
||||
}}
|
||||
size="xsm"
|
||||
rightIcon="search"
|
||||
iconWidth="15"
|
||||
fill="#889096"
|
||||
className="create-group-custom"
|
||||
data-cy="custom-group-search"
|
||||
/>
|
||||
<ButtonSolid
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
this.setState({ newGroupName: null, showNewGroupForm: true, isSaveBtnDisabled: true });
|
||||
}}
|
||||
size="sm"
|
||||
fill="#889096"
|
||||
rightIcon="plus"
|
||||
iconWidth="20"
|
||||
className="create-group-custom"
|
||||
data-cy="create-custom-group-button"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="searchbox-custom">
|
||||
<SearchBox
|
||||
dataCy={`custom-group`}
|
||||
width="70px !important"
|
||||
callBack={this.handleGroupSearch}
|
||||
placeholder={'Search'}
|
||||
customClass="tj-common-search-input-group"
|
||||
onClearCallback={this.handleGroupSearchClose}
|
||||
autoFocus={true}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{groups.length ? (
|
||||
filteredGroup.map((permissionGroup) => {
|
||||
return (
|
||||
<FolderList
|
||||
key={permissionGroup.id}
|
||||
listId={permissionGroup.id}
|
||||
overlayFunctionParam={{
|
||||
id: permissionGroup.id,
|
||||
groupName: permissionGroup.name,
|
||||
}}
|
||||
selectedItem={
|
||||
this.state.selectedGroup == this.humanizeifDefaultGroupName(permissionGroup.name)
|
||||
}
|
||||
onClick={() => {
|
||||
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, '-')}
|
||||
>
|
||||
<span>
|
||||
<OverflowTooltip>{this.humanizeifDefaultGroupName(permissionGroup.name)}</OverflowTooltip>
|
||||
</span>
|
||||
</FolderList>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="empty-custom-group-info">
|
||||
<SolidIcon className="info-icon" name="information" width="18px" />
|
||||
<span className="tj-text-xsm text-center info-label" data-cy="empty-custom-group-info">
|
||||
No custom groups added
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="org-users-page-card-body">
|
||||
{isLoading ? (
|
||||
<Loader />
|
||||
) : (
|
||||
<ManageGroupPermissionResourcesV2
|
||||
groupPermissionId={this.state.selectedGroupPermissionId}
|
||||
darkMode={this.props.darkMode}
|
||||
selectedGroup={this.state.selectedGroup}
|
||||
selectedGroupObject={this.state.selectedGroupObject}
|
||||
updateGroupName={this.updateGroupName}
|
||||
deleteGroup={this.deleteGroup}
|
||||
roleOptions={defaultGroups.map((group) => {
|
||||
return {
|
||||
name: this.humanizeifDefaultGroupName(group.name),
|
||||
value: group.name,
|
||||
};
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const ManageGroupPermissionsV2 = withTranslation()(ManageGroupPermissionsComponent);
|
||||
|
||||
const Field = ({
|
||||
text,
|
||||
onClick,
|
||||
customClass,
|
||||
leftIcon,
|
||||
leftIconWidth,
|
||||
leftIconHeight = '18',
|
||||
leftIconClassName,
|
||||
buttonDisable = false,
|
||||
tooltipContent = '',
|
||||
tooltipId = '',
|
||||
darkMode = false,
|
||||
}) => {
|
||||
return (
|
||||
<div className={`field ${customClass ? ` ${customClass}` : ''}`}>
|
||||
<span
|
||||
className="row option-row"
|
||||
role="button"
|
||||
onClick={!buttonDisable && onClick}
|
||||
data-cy={`${text.toLowerCase().replace(/\s+/g, '-')}-card-option`}
|
||||
data-tooltip-content={tooltipContent}
|
||||
data-tooltip-id={tooltipId}
|
||||
>
|
||||
<div className={`col-2 ${leftIconClassName}`}>
|
||||
{leftIcon && (
|
||||
<SolidIcon
|
||||
name={leftIcon}
|
||||
width={leftIconWidth}
|
||||
height={leftIconHeight}
|
||||
{...(buttonDisable ? { fill: '#D7DBDF' } : {})}
|
||||
></SolidIcon>
|
||||
)}
|
||||
</div>
|
||||
<div className={`col ${buttonDisable ? 'disable' : ''} ${darkMode ? 'dark-theme' : ''}`}>{text}</div>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
105
frontend/src/ManageGroupPermissionsV2/ResourceChip.jsx
Normal file
105
frontend/src/ManageGroupPermissionsV2/ResourceChip.jsx
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
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) => (
|
||||
<span className="group-chip" key={index} data-cy="group-chip">
|
||||
{humanizeifDefaultGroupName(group)}
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-active={showAllGroups}
|
||||
onClick={(e) => {
|
||||
orderedArray.length > 2 && toggleAllGroupsList(e);
|
||||
}}
|
||||
className={cx('text-muted resource-name-cell', { 'groups-hover': orderedArray.length > 2 })}
|
||||
>
|
||||
<div className="groups-name-container tj-text-sm font-weight-500">
|
||||
{orderedArray.length === 0 ? (
|
||||
<div className="groups-name-row">
|
||||
<div className="empty-text">-</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="groups-name-row">
|
||||
{orderedArray.slice(0, 2).map((group, index) => {
|
||||
return renderGroupChip(group, index);
|
||||
})}
|
||||
</div>
|
||||
<div className="groups-name-row">
|
||||
{orderedArray.slice(2, 4).map((group, index) => {
|
||||
return renderGroupChip(group, index);
|
||||
})}
|
||||
</div>
|
||||
{orderedArray.length > 4 && (
|
||||
<React.Fragment key={4}>
|
||||
<div className="groups-name-row" ref={groupsListRef}>
|
||||
<span className="group-chip">+{orderedArray.length - 4} more</span>
|
||||
</div>
|
||||
{showAllGroups && (
|
||||
<div className="all-groups-list">
|
||||
{orderedArray.slice(4).map((group, index) => renderGroupChip(group, index))}
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
||||
{/* 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);
|
||||
}) */}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupChipTD;
|
||||
|
|
@ -0,0 +1,455 @@
|
|||
@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: var(--base) !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: left;
|
||||
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:flex-start;
|
||||
padding: 12px;
|
||||
gap: 10px;
|
||||
|
||||
.resource-name {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
width: 195px;
|
||||
padding-right: 10px;
|
||||
|
||||
.resource-icon {
|
||||
}
|
||||
|
||||
.resource-text{
|
||||
max-width: 160px;
|
||||
padding-top: 1px;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
div {
|
||||
width: 206px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--slate3);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
.manage-resource-body {
|
||||
padding: 24px;
|
||||
font-size: 12px;
|
||||
overflow-y: auto;
|
||||
height: calc(100vh - 300px);
|
||||
|
||||
}
|
||||
|
||||
.error-text{
|
||||
color: red;
|
||||
}
|
||||
|
|
@ -6,6 +6,9 @@ import { Tooltip } from 'react-tooltip';
|
|||
import { FormWrapper, textAreaEnterOnSave } from '@/_components/FormWrapper';
|
||||
import EyeHide from '../../assets/images/onboardingassets/Icons/EyeHide';
|
||||
import EyeShow from '../../assets/images/onboardingassets/Icons/EyeShow';
|
||||
import './ConstantFormStyle.scss';
|
||||
import { Constants } from '@/_helpers/utils';
|
||||
import CloseIcon from '@/_ui/Icon/bulkIcons/CloseIcon';
|
||||
|
||||
const ConstantForm = ({
|
||||
selectedConstant,
|
||||
|
|
@ -13,11 +16,12 @@ const ConstantForm = ({
|
|||
onCancelBtnClicked,
|
||||
isLoading,
|
||||
currentEnvironment,
|
||||
checkIfConstantNameExists,
|
||||
mode,
|
||||
}) => {
|
||||
console.log(isLoading);
|
||||
const [fields, setFields] = useState(() => ({
|
||||
...selectedConstant,
|
||||
type: selectedConstant?.type,
|
||||
environments: [{ label: currentEnvironment?.name, value: currentEnvironment?.id }],
|
||||
}));
|
||||
|
||||
|
|
@ -47,7 +51,7 @@ const ConstantForm = ({
|
|||
name_already_exists: `Constant with this name already exists in ${capitalize(
|
||||
currentEnvironment?.name
|
||||
)} environment`,
|
||||
invalid_name_length: 'Constant name should be between 1 and 32 characters',
|
||||
invalid_name_length: 'Constant name has exceeded 50 characters',
|
||||
max_name_length_reached: 'Maximum length has been reached',
|
||||
invalid_name:
|
||||
'Constant name should start with a letter or underscore and can only contain letters, numbers and underscores',
|
||||
|
|
@ -60,16 +64,10 @@ const ConstantForm = ({
|
|||
|
||||
if (name !== 'name') return;
|
||||
|
||||
const isNameAlreadyExists = checkIfConstantNameExists(value, currentEnvironment?.id);
|
||||
const invalidNameLength = value.length > 32;
|
||||
const maxNameLengthReached = value.length === 32;
|
||||
const invalidNameLength = value.length > 50;
|
||||
const maxNameLengthReached = value.length === 50;
|
||||
const invalidName = !isValidPropertyName(value);
|
||||
|
||||
if (isNameAlreadyExists) {
|
||||
return setError({
|
||||
name: ERROR_MESSAGES.name_already_exists,
|
||||
});
|
||||
}
|
||||
if (invalidNameLength) {
|
||||
return setError({
|
||||
name: ERROR_MESSAGES.invalid_name_length,
|
||||
|
|
@ -133,6 +131,7 @@ const ConstantForm = ({
|
|||
!isActiveErrorState(error) &&
|
||||
fields['name'] &&
|
||||
fields['value'] &&
|
||||
fields['type'] &&
|
||||
(fields['name'].length > 0 || fields['value'].length > 0)
|
||||
? false
|
||||
: true;
|
||||
|
|
@ -160,6 +159,9 @@ const ConstantForm = ({
|
|||
<h3 className="card-title" data-cy="constant-form-title">
|
||||
{!selectedConstant ? 'Add new constant' : 'Update constant'} in {currentEnvironment?.name}{' '}
|
||||
</h3>
|
||||
<div style={{ marginLeft: '200px' }} onClick={onCancelBtnClicked}>
|
||||
<CloseIcon width="15" fill="#3E63DD" opacity="1" secondaryFill="#ffffff" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-body org-constant-form">
|
||||
<FormWrapper callback={handlecreateOrUpdate} id="variable-form">
|
||||
|
|
@ -167,9 +169,6 @@ const ConstantForm = ({
|
|||
<div className="d-flex mb-3">
|
||||
<div
|
||||
className="col tj-app-input"
|
||||
style={{
|
||||
marginRight: '10px',
|
||||
}}
|
||||
onMouseEnter={() => setIsOpen(true)}
|
||||
onMouseLeave={() => setIsOpen(false)}
|
||||
>
|
||||
|
|
@ -179,7 +178,7 @@ const ConstantForm = ({
|
|||
<input
|
||||
type="text"
|
||||
className={`tj-input-element ${error['name'] ? 'tj-input-error-state' : ''}`}
|
||||
placeholder={'Enter Constant Name'}
|
||||
placeholder={'Enter constant name'}
|
||||
name="name"
|
||||
onChange={handleFieldChange}
|
||||
value={fields['name']}
|
||||
|
|
@ -193,8 +192,55 @@ const ConstantForm = ({
|
|||
<span className="text-danger" data-cy="name-error-text">
|
||||
{error['name']}
|
||||
</span>
|
||||
{!error['name'] && (
|
||||
<small style={{ color: 'var(--text-placeholder)' }}>Name must be unique and max 50 characters</small>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label" data-cy="name-label">
|
||||
Type
|
||||
</label>
|
||||
<div className="radio-group" data-tooltip-id="type-tooltip">
|
||||
<div className="radio-item">
|
||||
<label style={{ color: mode === 'edit' ? '#adb5bd' : 'inherit' }}>
|
||||
<input
|
||||
type="radio"
|
||||
name="type"
|
||||
value="Global"
|
||||
checked={fields['type'] === Constants.Global}
|
||||
onChange={handleFieldChange}
|
||||
disabled={mode === 'edit'}
|
||||
/>
|
||||
Global constants
|
||||
</label>
|
||||
<small style={{ color: mode === 'edit' ? '#adb5bd' : 'inherit' }}>
|
||||
The values can be used anywhere in the product
|
||||
</small>
|
||||
</div>
|
||||
<div className="radio-item">
|
||||
<label style={{ color: mode === 'edit' ? '#adb5bd' : 'inherit' }}>
|
||||
<input
|
||||
type="radio"
|
||||
name="type"
|
||||
value="Secret"
|
||||
checked={fields['type'] === Constants.Secret}
|
||||
onChange={handleFieldChange}
|
||||
disabled={mode === 'edit'}
|
||||
/>
|
||||
Secrets
|
||||
</label>
|
||||
<small style={{ color: mode === 'edit' ? '#adb5bd' : 'inherit' }}>
|
||||
The values are hidden and can only be used in data sources and queries
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
{mode === 'edit' && (
|
||||
<Tooltip id="type-tooltip" place="top">
|
||||
Cannot edit constant type
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<div className="col tj-app-input">
|
||||
<div className="d-flex justify-content-between align-items-center w-100">
|
||||
<label className="form-label" data-cy="value-label">
|
||||
|
|
@ -294,7 +340,11 @@ const ConstantForm = ({
|
|||
<ButtonSolid
|
||||
type="submit"
|
||||
isLoading={isLoading}
|
||||
disabled={isLoading || shouldDisableButton || selectedConstant?.value === fields['value']}
|
||||
disabled={
|
||||
isLoading ||
|
||||
shouldDisableButton ||
|
||||
(selectedConstant?.value === fields['value'] && selectedConstant?.type === fields['type'])
|
||||
}
|
||||
data-cy="add-constant-button"
|
||||
form="variable-form"
|
||||
>
|
||||
|
|
|
|||
320
frontend/src/ManageOrgConstants/ConstantFormStyle.scss
Normal file
320
frontend/src/ManageOrgConstants/ConstantFormStyle.scss
Normal file
|
|
@ -0,0 +1,320 @@
|
|||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.radio-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: left;
|
||||
flex: 1;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.radio-item label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.radio-item input[type="radio"]:checked + label {
|
||||
color: var(--indigo9);
|
||||
}
|
||||
|
||||
.radio-item input[type="radio"]:checked {
|
||||
accent-color: var(--indigo9);
|
||||
}
|
||||
|
||||
.radio-item small {
|
||||
margin-top: 0.25rem;
|
||||
margin-left: 20px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.constant-wrapper {
|
||||
background-color: #f8f9fa;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.constant-page-wrapper {
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #e9ece;
|
||||
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
width: 920px;
|
||||
height: 620px;
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.workspace-constant-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
.dark-theme .workspace-constant-header {
|
||||
border-bottom: 1px solid var(--border-weak, #2B3036);
|
||||
}
|
||||
.workspace-constant-header .tj-text-sm {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.manage-sso-wrapper-card {
|
||||
width: 100%;
|
||||
outline: none
|
||||
}
|
||||
.manage-sso-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.add-new-constant-button {
|
||||
background-color: #3e63dd;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.add-new-constant-button:hover {
|
||||
background-color: #2b50ba;
|
||||
}
|
||||
|
||||
.workspace-variable-container-wrap {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.align-items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.p-3 {
|
||||
padding: 16px;
|
||||
padding-left: 0px !important;
|
||||
}
|
||||
|
||||
.workspace-setting-buttons-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.left-menu ul {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.left-menu li {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.left-menu li.selected {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.left-menu li:hover {
|
||||
cursor: pointer;
|
||||
color: #3e63dd;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
background-color: #3e63dd;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background-color: #2b50ba;
|
||||
}
|
||||
|
||||
.button:disabled {
|
||||
background-color: #adb5bd;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.add-new-constant-button {
|
||||
min-width: 200px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.tabs-and-search {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
.tab {
|
||||
padding-bottom: 0px;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
padding-top: 10px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #687076;
|
||||
border-bottom: 1px solid transparent;
|
||||
|
||||
.workspace-constant-text {
|
||||
color: #687076;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
}
|
||||
.tab.active {
|
||||
border-radius: 0px;
|
||||
color: #3e63dd;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 2px solid #3e63dd;
|
||||
|
||||
.workspace-constant-text {
|
||||
padding-bottom: 8px;
|
||||
color: #3e63dd;
|
||||
border-bottom: 2px solid #3e63dd;
|
||||
}
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
border-radius: 0px;
|
||||
color: #3e63dd;
|
||||
.workspace-constant-text {
|
||||
padding-bottom: 8px;
|
||||
color: #3e63dd;
|
||||
border-bottom: 2px solid #3e63dd;
|
||||
}
|
||||
}
|
||||
|
||||
.dark-theme{
|
||||
.tab {
|
||||
.workspace-constant-text {
|
||||
color: var(--text-medium, #CFD3D8);
|
||||
}
|
||||
.tab-count{
|
||||
margin-left: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--text-medium, #CFD3D8);
|
||||
}
|
||||
.tab-count.active {
|
||||
color: #3e63dd;
|
||||
}
|
||||
}
|
||||
}
|
||||
.tab-count {
|
||||
margin-left: 4px;
|
||||
font-size: 12px;
|
||||
color: #687076;
|
||||
}
|
||||
|
||||
.tab-count.active {
|
||||
color: #3e63dd;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
height: 45px !important;
|
||||
padding-bottom: 15px;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
font-size: 14px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
background-color: #ffffff;
|
||||
color: #495057;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: #adb5bd;
|
||||
}
|
||||
|
||||
/* Dark Theme Styles */
|
||||
.dark-theme .constant-wrapper,
|
||||
.theme-dark .constant-wrapper {
|
||||
background-color: var(--slate2);
|
||||
}
|
||||
|
||||
.dark-theme .constant-page-wrapper,
|
||||
.theme-dark .constant-page-wrapper {
|
||||
background-color: var(--base);
|
||||
border: 1px solid #6c757d;
|
||||
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.dark-theme .workspace-constant-header .tj-text-sm,
|
||||
.theme-dark .workspace-constant-header .tj-text-sm {
|
||||
color: #cccccc;
|
||||
}
|
||||
|
||||
.dark-theme .radio-item small,
|
||||
.theme-dark .radio-item small {
|
||||
color: #cccccc;
|
||||
}
|
||||
|
||||
/* Additional dark theme styles for other components */
|
||||
.dark-theme .search-input,
|
||||
.theme-dark .search-input {
|
||||
background-color: #2b394b;
|
||||
color: #ffffff;
|
||||
border: 1px solid #6c757d;
|
||||
}
|
||||
|
||||
.dark-theme .search-input::placeholder,
|
||||
.theme-dark .search-input::placeholder {
|
||||
color: #ced4da;
|
||||
}
|
||||
|
||||
/* Adjust the button styles for dark theme */
|
||||
.dark-theme .button,
|
||||
.theme-dark .button {
|
||||
background-color: #2b394b;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.dark-theme .button:hover,
|
||||
.theme-dark .button:hover {
|
||||
background-color: #2b394b;
|
||||
}
|
||||
|
||||
.enabled-tag{
|
||||
color: var(--grass9);
|
||||
}
|
||||
|
||||
.disabled-tag{
|
||||
color: var(--tomato9);
|
||||
}
|
||||
.dark-theme .tj-input-element {
|
||||
&::placeholder {
|
||||
color: #858C94;
|
||||
}
|
||||
}
|
||||
|
|
@ -21,6 +21,9 @@ const ConstantTable = ({
|
|||
};
|
||||
const darkMode = localStorage.getItem('darkMode') === 'true';
|
||||
const displayValue = (constant) => {
|
||||
if (typeof constant.value === 'undefined' || constant.value === null) {
|
||||
return '';
|
||||
}
|
||||
return String(constant.value).length > (canUpdateDeleteConstant ? 30 : 50)
|
||||
? String(constant.value).substring(0, canUpdateDeleteConstant ? 30 : 50) + '...'
|
||||
: constant.value;
|
||||
|
|
@ -32,10 +35,10 @@ const ConstantTable = ({
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="container-xl">
|
||||
<div>
|
||||
<div className="card constant-table-card" style={{ border: 'none' }}>
|
||||
<div
|
||||
className="fixedHeader table-responsive constant-table-wrapper px-2"
|
||||
className="fixedHeader table-responsive px-2"
|
||||
ref={tableRef}
|
||||
style={{ maxHeight: tableRef.current && calculateOffset() }}
|
||||
>
|
||||
|
|
@ -44,7 +47,24 @@ const ConstantTable = ({
|
|||
<tr>
|
||||
<th data-cy="workspace-variable-table-name-header">Name</th>
|
||||
<th data-cy="workspace-variable-table-value-header">Value</th>
|
||||
{canUpdateDeleteConstant && <th className="w-1"></th>}
|
||||
{canUpdateDeleteConstant && (
|
||||
<th className="w-1" style={{ paddingRight: '16px' }}>
|
||||
{' '}
|
||||
<small
|
||||
className="text-green d-flex align-items-center justify-content-end"
|
||||
data-cy="encrypted-label"
|
||||
>
|
||||
<img
|
||||
className="encrypted-icon me-1"
|
||||
src="assets/images/icons/padlock.svg"
|
||||
alt="Encrypted"
|
||||
width="12"
|
||||
height="12"
|
||||
/>
|
||||
Encrypted
|
||||
</small>
|
||||
</th>
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
{isLoading ? (
|
||||
|
|
|
|||
|
|
@ -1,21 +1,23 @@
|
|||
import React from 'react';
|
||||
import { ButtonSolid } from '@/_ui/AppButton/AppButton';
|
||||
|
||||
const EmptyState = ({ canCreateVariable, setIsManageVarDrawerOpen, isLoading }) => {
|
||||
const EmptyState = ({ canCreateVariable, setIsManageVarDrawerOpen, isLoading, searchTerm = '' }) => {
|
||||
if (isLoading) return null;
|
||||
|
||||
return (
|
||||
<div className="w-100 workspace-constant-card-body">
|
||||
<div className="w-100 constant-card-body">
|
||||
<div className="align-items-center p-3 justify-content-between">
|
||||
<div className="empty-state-org-constants">
|
||||
<center className={`empty-result`}>
|
||||
<img src="assets/images/icons/org-constants.svg" width="64" height="64" data-cy="empty-state-image" />
|
||||
<div className="w-50 mt-2">
|
||||
<h3 data-cy="empty-state-header">No Workspace constants yet</h3>
|
||||
<h3 data-cy="empty-state-header">
|
||||
{searchTerm === '' ? 'No Workspace constants yet' : 'No workspace constants found'}
|
||||
</h3>
|
||||
<p className="info mt-2" data-cy="empty-state-text">
|
||||
Use workspace constants seamlessly in both the app builder and data source connections across ToolJet.
|
||||
</p>
|
||||
{canCreateVariable && (
|
||||
{canCreateVariable && searchTerm === '' && (
|
||||
<ButtonSolid
|
||||
data-cy="add-new-constant-button"
|
||||
vaiant="primary"
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
//Do not merge changes from ee
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { authenticationService, orgEnvironmentConstantService, appEnvironmentService } from '@/_services';
|
||||
import { ConfirmDialog } from '@/_components';
|
||||
|
|
@ -13,6 +14,8 @@ import ConstantForm from './ConstantForm';
|
|||
import EmptyState from './EmptyState';
|
||||
import FolderList from '@/_ui/FolderList/FolderList';
|
||||
import { BreadCrumbContext } from '@/App';
|
||||
import './ConstantFormStyle.scss';
|
||||
import { Constants } from '@/_helpers/utils';
|
||||
|
||||
const MODES = Object.freeze({
|
||||
CREATE: 'create',
|
||||
|
|
@ -30,7 +33,7 @@ const ManageOrgConstantsComponent = ({ darkMode }) => {
|
|||
|
||||
const [mode, setMode] = useState(MODES.NULL);
|
||||
|
||||
const perPage = 7;
|
||||
const perPage = 6;
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [currentTableData, setTableData] = useState([]);
|
||||
|
|
@ -40,6 +43,25 @@ const ManageOrgConstantsComponent = ({ darkMode }) => {
|
|||
const [selectedConstant, setSelectedConstant] = useState(null);
|
||||
const { updateSidebarNAV } = useContext(BreadCrumbContext);
|
||||
|
||||
const [activeTab, setActiveTab] = useState(Constants.Global);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [globalCount, setGlobalCount] = useState(0);
|
||||
const [secretCount, setSecretCount] = useState(0);
|
||||
|
||||
const handleTabChange = (tab) => {
|
||||
setCurrentPage(1);
|
||||
updateTableData(constants, activeTabEnvironment?.name, 0, perPage, true, tab, searchTerm);
|
||||
setActiveTab(tab);
|
||||
};
|
||||
|
||||
const handleSearchChange = (e) => {
|
||||
const searchTerm = e?.target?.value.toLowerCase();
|
||||
setSearchTerm(searchTerm);
|
||||
|
||||
// Re-filter the constants based on the current search term and active tab
|
||||
updateTableData(constants, activeTabEnvironment?.name, 0, perPage, true, activeTab, searchTerm);
|
||||
};
|
||||
|
||||
const onCancelBtnClicked = () => {
|
||||
setSelectedConstant(null);
|
||||
setIsManageVarDrawerOpen(false);
|
||||
|
|
@ -69,52 +91,107 @@ const ManageOrgConstantsComponent = ({ darkMode }) => {
|
|||
}
|
||||
setCurrentPage(1);
|
||||
|
||||
const envName = activeTabEnvironment ? activeTabEnvironment.name : environment.name;
|
||||
updateTableData(allConstants, envName, 0, perPage, true);
|
||||
const envName = activeTabEnvironment ? activeTabEnvironment?.name : environment?.name;
|
||||
updateTableData(allConstants, envName, 0, perPage, true, activeTab, searchTerm);
|
||||
// Calculate counts for Global and Secret constants
|
||||
const globalCount =
|
||||
allConstants.length > 0
|
||||
? allConstants.filter(
|
||||
(constant) =>
|
||||
constant.type === Constants.Global &&
|
||||
constant.values.find((env) => env.environmentName === envName)?.value !== ''
|
||||
).length
|
||||
: 0;
|
||||
|
||||
const secretCount =
|
||||
allConstants.length > 0
|
||||
? allConstants.filter(
|
||||
(constant) =>
|
||||
constant.type === Constants.Secret &&
|
||||
constant.values.find((env) => env.environmentName === envName)?.value !== ''
|
||||
).length
|
||||
: 0;
|
||||
|
||||
setGlobalCount(globalCount);
|
||||
setSecretCount(secretCount);
|
||||
};
|
||||
|
||||
const updateTableData = (orgContants, envName, start, end, activeTabChanged = false) => {
|
||||
const constantsForEnvironment = orgContants
|
||||
const updateTableData = (orgConstants, envName, start, end, activeTabChanged = false, tab = null, search = '') => {
|
||||
if (!Array.isArray(orgConstants)) {
|
||||
return;
|
||||
}
|
||||
const filteredConstants = orgConstants
|
||||
.filter((constant) => {
|
||||
const envConstant = constant?.values.find((value) => value.environmentName === envName);
|
||||
|
||||
// Filter based on the active tab: 'Global' or 'Secret'
|
||||
if (tab === Constants.Global) {
|
||||
return envConstant && envConstant.value !== '' && constant.type === Constants.Global;
|
||||
} else if (tab === Constants.Secret) {
|
||||
return envConstant && envConstant.value !== '' && constant.type === Constants.Secret;
|
||||
}
|
||||
return envConstant && envConstant.value !== '';
|
||||
})
|
||||
.map((constant) => {
|
||||
return {
|
||||
id: constant.id,
|
||||
name: constant.name,
|
||||
value: findValueForEnvironment(constant.values, envName),
|
||||
};
|
||||
});
|
||||
.filter((constant) => {
|
||||
// Filter based on the search term
|
||||
return constant.name.toLowerCase().includes(search.toLowerCase());
|
||||
})
|
||||
.map((constant) => ({
|
||||
id: constant.id,
|
||||
name: constant.name,
|
||||
type: constant.type,
|
||||
value: findValueForEnvironment(constant.values, envName),
|
||||
}));
|
||||
|
||||
let globalTabCount = 0;
|
||||
let secretTabCount = 0;
|
||||
|
||||
globalTabCount = orgConstants.filter(
|
||||
(constant) =>
|
||||
constant.type === Constants.Global &&
|
||||
constant.name.toLowerCase().includes(search.toLowerCase()) &&
|
||||
constant?.values.find((value) => value.environmentName === envName && value.value !== '')
|
||||
).length;
|
||||
|
||||
secretTabCount = orgConstants.filter(
|
||||
(constant) =>
|
||||
constant.type === Constants.Secret &&
|
||||
constant.name.toLowerCase().includes(search.toLowerCase()) &&
|
||||
constant?.values.find((value) => value.environmentName === envName && value.value !== '')
|
||||
).length;
|
||||
|
||||
setGlobalCount(globalTabCount);
|
||||
setSecretCount(secretTabCount);
|
||||
if (activeTabChanged) {
|
||||
computeTotalPages(constantsForEnvironment.length);
|
||||
computeTotalPages(filteredConstants.length || 1);
|
||||
}
|
||||
const paginatedConstants = filteredConstants ? filteredConstants.slice(start, end) : filteredConstants;
|
||||
setTableData(paginatedConstants);
|
||||
if (tab === Constants.Global) {
|
||||
setGlobalCount(filteredConstants.length);
|
||||
} else {
|
||||
setSecretCount(filteredConstants.length);
|
||||
}
|
||||
|
||||
const envConstantants = constantsForEnvironment.slice(start, end);
|
||||
|
||||
setTableData(envConstantants);
|
||||
};
|
||||
|
||||
const goToNextPage = () => {
|
||||
setCurrentPage(currentPage + 1);
|
||||
|
||||
const start = (currentPage + 1 - 1) * perPage;
|
||||
const start = currentPage * perPage;
|
||||
const end = start + perPage;
|
||||
|
||||
const envName = activeTabEnvironment.name;
|
||||
updateTableData(constants, envName, start, end);
|
||||
const envName = activeTabEnvironment?.name;
|
||||
updateTableData(constants, envName, start, end, false, activeTab, searchTerm);
|
||||
};
|
||||
|
||||
const goToPreviousPage = () => {
|
||||
setCurrentPage(currentPage - 1);
|
||||
|
||||
const start = (currentPage - 1 - 1) * perPage;
|
||||
const start = (currentPage - 2) * perPage;
|
||||
const end = start + perPage;
|
||||
|
||||
const envName = activeTabEnvironment.name;
|
||||
updateTableData(constants, envName, start, end);
|
||||
const envName = activeTabEnvironment?.name;
|
||||
updateTableData(constants, envName, start, end, false, activeTab, searchTerm);
|
||||
};
|
||||
|
||||
const canAnyGroupPerformAction = (action, permissions) => {
|
||||
|
|
@ -126,24 +203,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 = () => {
|
||||
|
|
@ -172,7 +240,7 @@ const ManageOrgConstantsComponent = ({ darkMode }) => {
|
|||
};
|
||||
|
||||
const fetchConstantsAndEnvironments = async () => {
|
||||
const orgConstants = await orgEnvironmentConstantService.getAll(true);
|
||||
const orgConstants = await orgEnvironmentConstantService.getAll();
|
||||
|
||||
if (orgConstants?.constants?.length > 1) {
|
||||
orgConstants.constants.sort((a, b) => {
|
||||
|
|
@ -189,29 +257,61 @@ const ManageOrgConstantsComponent = ({ darkMode }) => {
|
|||
|
||||
setIsLoading(false);
|
||||
setSelectedConstant(null);
|
||||
const start = (currentPage - 1 - 1) * perPage;
|
||||
const end = start + perPage;
|
||||
|
||||
const envName = activeTabEnvironment ? activeTabEnvironment?.name : currentEnvironment?.name;
|
||||
updateTableData(orgConstants, envName, start, end, activeTab, searchTerm);
|
||||
};
|
||||
|
||||
const checkIfConstantNameExists = (name, environementId) => {
|
||||
const checkIfConstantNameExists = (name, type, environementId) => {
|
||||
if (!environementId) {
|
||||
return constants.some((constant) => constant.name === name);
|
||||
return constants.some((constant) => constant.name === name && constant.type === type);
|
||||
}
|
||||
|
||||
const existingConstants = constants.filter((constant) => {
|
||||
return (
|
||||
constant.type === type && constant.values.some((value) => value.id === environementId && value.value !== '')
|
||||
);
|
||||
});
|
||||
return existingConstants.some((constant) => constant.name === name);
|
||||
};
|
||||
|
||||
const checkIfConstantNameExistsInDiffEnv = (name, type, environementId) => {
|
||||
if (!environementId) {
|
||||
return constants.some((constant) => constant.name === name && constant.type === type);
|
||||
}
|
||||
|
||||
const envConstants = constants.filter((constant) => {
|
||||
return constant.values.some((value) => value.id === environementId && value.value !== '');
|
||||
return (
|
||||
constant.type === type && constant.values.some((value) => value.id === environementId && value.value === '')
|
||||
);
|
||||
});
|
||||
|
||||
return envConstants.some((constant) => constant.name === name);
|
||||
};
|
||||
|
||||
const createOrUpdate = (variable, shouldUpdate = false) => {
|
||||
const currentEnv = activeTabEnvironment;
|
||||
|
||||
const constantExistsInDiffEnv = checkIfConstantNameExists(variable.name);
|
||||
const constantExists = checkIfConstantNameExists(
|
||||
variable?.name,
|
||||
variable?.type,
|
||||
variable?.environments?.[0]?.value
|
||||
);
|
||||
const constantExistsInDiffEnv = checkIfConstantNameExistsInDiffEnv(
|
||||
variable?.name,
|
||||
variable?.type,
|
||||
variable?.environments?.[0]?.value
|
||||
);
|
||||
if (constantExists && !shouldUpdate) {
|
||||
toast.error(`${variable.type} constant already exists!`);
|
||||
return;
|
||||
}
|
||||
const shouldUpdateConstant = mode === 'edit' && shouldUpdate ? true : constantExistsInDiffEnv;
|
||||
|
||||
if (shouldUpdateConstant) {
|
||||
const variableId = constantExistsInDiffEnv
|
||||
? constants.find((constant) => constant.name === variable.name).id
|
||||
? constants.find((constant) => constant.name === variable.name && constant.type === variable.type).id
|
||||
: variable.id;
|
||||
|
||||
return orgEnvironmentConstantService
|
||||
|
|
@ -228,9 +328,9 @@ const ManageOrgConstantsComponent = ({ darkMode }) => {
|
|||
}
|
||||
|
||||
return orgEnvironmentConstantService
|
||||
.create(variable.name, variable.value, [currentEnv['id']])
|
||||
.create(variable.name, variable.value, variable.type, [currentEnv['id']])
|
||||
.then(() => {
|
||||
toast.success('Constant has been created');
|
||||
toast.success(`${variable.type} constant created successfully!`);
|
||||
onCancelBtnClicked();
|
||||
})
|
||||
.catch(({ error }) => {
|
||||
|
|
@ -288,7 +388,7 @@ const ManageOrgConstantsComponent = ({ darkMode }) => {
|
|||
</span>
|
||||
);
|
||||
return (
|
||||
<div className="wrapper org-constant-page org-variables-page animation-fade">
|
||||
<div className="constant-wrapper org-constant-page org-variables-page animation-fade">
|
||||
<ConfirmDialog
|
||||
show={showConstantDeleteConfirmation}
|
||||
message={confirmMessage}
|
||||
|
|
@ -311,14 +411,71 @@ const ManageOrgConstantsComponent = ({ darkMode }) => {
|
|||
/>
|
||||
</Drawer>
|
||||
)}
|
||||
|
||||
<div className="page-wrapper">
|
||||
<div className="align-items-center d-flex justify-content-between" style={{ marginBottom: '10px' }}>
|
||||
<div className="tj-text-sm font-weight-500" data-cy="env-name">
|
||||
{capitalize(activeTabEnvironment?.name)} ({globalCount + secretCount})
|
||||
</div>
|
||||
<div className="workspace-setting-buttons-wrap">
|
||||
{canCreateVariable() && (
|
||||
<ButtonSolid
|
||||
data-cy="add-new-constant-button"
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
setMode(() => MODES.CREATE);
|
||||
setIsManageVarDrawerOpen(() => true);
|
||||
}}
|
||||
className="add-new-constant-button"
|
||||
customStyles={{ minWidth: '200px', height: '32px' }}
|
||||
disabled={isManageVarDrawerOpen}
|
||||
>
|
||||
+ Create new constant
|
||||
</ButtonSolid>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="constant-page-wrapper">
|
||||
<div className="container-xl">
|
||||
<div>
|
||||
<div className="page-header workspace-constant-header">
|
||||
<div className="tj-text-sm font-weight-500" data-cy="constants-count-title">
|
||||
{constants.length} constants
|
||||
<div className="workspace-constant-header">
|
||||
<div className="tabs-and-search">
|
||||
<div className="tabs">
|
||||
<button
|
||||
className={`tab ${activeTab === Constants.Global ? 'active' : ''}`}
|
||||
onClick={() => handleTabChange(Constants.Global)}
|
||||
>
|
||||
Global constants
|
||||
<span className={`tab-count ${activeTab === Constants.Global ? 'active' : ''}`}>
|
||||
({globalCount})
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className={`tab ${activeTab === Constants.Secret ? 'active' : ''}`}
|
||||
onClick={() => handleTabChange(Constants.Secret)}
|
||||
>
|
||||
Secrets
|
||||
<span className={`tab-count ${activeTab === Constants.Secret ? 'active' : ''}`}>
|
||||
({secretCount})
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="search-bar">
|
||||
<input
|
||||
type="text"
|
||||
placeholder={activeTab === Constants.Global ? 'Search global constants' : 'Search secrets'}
|
||||
value={searchTerm}
|
||||
onChange={handleSearchChange}
|
||||
className="search-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="workspace-variable-container-wrap mt-2">
|
||||
<div className="container-xl" style={{ width: '880px', padding: '0px' }}>
|
||||
<div className="workspace-constant-table-card">
|
||||
<div className="mt-3">
|
||||
<Alert svg="tj-info">
|
||||
<div
|
||||
|
|
@ -330,9 +487,19 @@ const ManageOrgConstantsComponent = ({ darkMode }) => {
|
|||
}}
|
||||
>
|
||||
<div className="text-muted" data-cy="workspace-constant-helper-text">
|
||||
To resolve a Workspace constant use{' '}
|
||||
<strong style={{ fontWeight: 500, color: '#3E63DD' }}>{'{{constants.access_token}}'}</strong>
|
||||
{activeTab === Constants.Global ? (
|
||||
<>
|
||||
To resolve a global workspace constant use{' '}
|
||||
<strong style={{ fontWeight: 500, color: '#3E63DD' }}>{'{{constants.access_token}}'}</strong>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
To resolve a secret workspace constant use{' '}
|
||||
<strong style={{ fontWeight: 500, color: '#3E63DD' }}>{'{{secrets.access_token}}'}</strong>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button
|
||||
// Todo: Update link to documentation: workspace constants
|
||||
|
|
@ -350,21 +517,14 @@ const ManageOrgConstantsComponent = ({ darkMode }) => {
|
|||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
<Button.Content title={'Read Documentation'} iconSrc="assets/images/icons/student.svg" />
|
||||
<Button.Content title={'Read documentation'} iconSrc="assets/images/icons/student.svg" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Alert>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="workspace-variable-container-wrap mt-2">
|
||||
<div className="container-xl">
|
||||
<div className="workspace-constant-table-card">
|
||||
<div className="manage-sso-container h-100">
|
||||
<div className="d-flex manage-sso-wrapper-card h-100">
|
||||
<div className="d-flex manage-constant-wrapper-card">
|
||||
<ManageOrgConstantsComponent.EnvironmentsTabs
|
||||
allEnvironments={environments}
|
||||
currentEnvironment={activeTabEnvironment}
|
||||
|
|
@ -372,30 +532,9 @@ const ManageOrgConstantsComponent = ({ darkMode }) => {
|
|||
isLoading={isLoading}
|
||||
allConstants={constants}
|
||||
/>
|
||||
{constants.length > 0 ? (
|
||||
<div className="w-100 workspace-constant-card-body">
|
||||
<div className="align-items-center d-flex p-3 justify-content-between">
|
||||
<div className="tj-text-sm font-weight-500" data-cy="env-name">
|
||||
{capitalize(activeTabEnvironment?.name)}
|
||||
</div>
|
||||
<div className="workspace-setting-buttons-wrap">
|
||||
{canCreateVariable() && (
|
||||
<ButtonSolid
|
||||
data-cy="add-new-constant-button"
|
||||
vaiant="primary"
|
||||
onClick={() => {
|
||||
setMode(MODES.CREATE);
|
||||
setIsManageVarDrawerOpen(true);
|
||||
}}
|
||||
className="add-new-constant-button"
|
||||
customStyles={{ minWidth: '200px', height: '32px' }}
|
||||
disabled={isManageVarDrawerOpen}
|
||||
>
|
||||
Create new constant
|
||||
</ButtonSolid>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{(activeTab === Constants.Global && globalCount > 0) ||
|
||||
(activeTab === Constants.Secret && secretCount > 0) ? (
|
||||
<div className="w-100">
|
||||
<ConstantTable
|
||||
constants={currentTableData}
|
||||
onEditBtnClicked={onEditBtnClicked}
|
||||
|
|
@ -418,6 +557,7 @@ const ManageOrgConstantsComponent = ({ darkMode }) => {
|
|||
canCreateVariable={canCreateVariable()}
|
||||
setIsManageVarDrawerOpen={setIsManageVarDrawerOpen}
|
||||
isLoading={isLoading}
|
||||
searchTerm={searchTerm}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -510,5 +650,4 @@ const Footer = ({ darkMode, totalPage, pageCount, dataLoading, gotoNextPage, got
|
|||
|
||||
ManageOrgConstantsComponent.EnvironmentsTabs = RenderEnvironmentsTab;
|
||||
ManageOrgConstantsComponent.Footer = Footer;
|
||||
|
||||
export default ManageOrgConstantsComponent;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,15 @@ import { useDropzone } from 'react-dropzone';
|
|||
import BulkIcon from '@/_ui/Icon/BulkIcons';
|
||||
import { toast } from 'react-hot-toast';
|
||||
|
||||
export function FileDropzone({ handleClick, hiddenFileInput, errors, handleFileChange, inviteBulkUsers, onDrop }) {
|
||||
export function FileDropzone({
|
||||
handleClick,
|
||||
hiddenFileInput,
|
||||
errors,
|
||||
handleFileChange,
|
||||
inviteBulkUsers,
|
||||
onDrop,
|
||||
setFileUpload,
|
||||
}) {
|
||||
const [fileData, setFileData] = useState();
|
||||
const { getRootProps, getInputProps, isDragActive, acceptedFiles } = useDropzone({
|
||||
accept: { parsedFileType: ['text/csv'] },
|
||||
|
|
@ -53,11 +61,15 @@ export function FileDropzone({ handleClick, hiddenFileInput, errors, handleFileC
|
|||
onChange={(e) => {
|
||||
const file = e.target.files[0];
|
||||
setFileData(file);
|
||||
if (file === undefined) {
|
||||
setFileUpload(false);
|
||||
}
|
||||
if (Math.round(file.size / 1024) > 1024) {
|
||||
toast.error('File size cannot exceed more than 1MB');
|
||||
e.target.value = null;
|
||||
} else {
|
||||
handleFileChange(file);
|
||||
setFileUpload(true);
|
||||
}
|
||||
}}
|
||||
accept=".csv"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -24,29 +26,61 @@ function InviteUsersForm({
|
|||
userDrawerMode,
|
||||
setUserValues,
|
||||
creatingUser,
|
||||
darkMode,
|
||||
}) {
|
||||
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 [fileUpload, setFileUpload] = useState(false);
|
||||
const groupedOptions = [
|
||||
{
|
||||
label: 'default',
|
||||
options: roleGroups,
|
||||
},
|
||||
{
|
||||
label: 'custom',
|
||||
options: customGroups,
|
||||
},
|
||||
];
|
||||
const [selectedGroups, setSelectedGroups] = useState([]);
|
||||
useEffect(() => {
|
||||
setFileUpload(false);
|
||||
}, [activeTab]);
|
||||
|
||||
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]);
|
||||
|
||||
|
|
@ -56,6 +90,7 @@ function InviteUsersForm({
|
|||
toast.error('File size cannot exceed more than 1MB');
|
||||
} else {
|
||||
handleFileChange(file);
|
||||
setFileUpload(true);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
|
@ -65,44 +100,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 && EDIT_ROLE_MESSAGE?.[currentEditingUser?.role_group?.[0]?.name]?.[newRole?.value]?.())
|
||||
setIsChangeRoleModalOpen(true);
|
||||
else {
|
||||
editUser();
|
||||
}
|
||||
};
|
||||
|
||||
const editUser = () => {
|
||||
const { newGroupsToAdd, groupsToRemove, selectedGroupsIds } = getEditedGroups();
|
||||
manageUser(currentEditingUser.id, selectedGroupsIds, newRole?.value, newGroupsToAdd, groupsToRemove);
|
||||
setIsChangeRoleModalOpen(false);
|
||||
};
|
||||
|
||||
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 isEdited = () => {
|
||||
const { newGroupsToAdd, groupsToRemove } = getEditedGroups();
|
||||
const inValidUserDetail = !(fields?.['fullName'] && fields?.['email']);
|
||||
const { first_name, last_name } = currentEditingUser || {};
|
||||
return isEditing
|
||||
? fields['fullName'] !== `${first_name}${last_name && ` ${last_name}`}` ||
|
||||
groupsToRemove.length ||
|
||||
newRole ||
|
||||
newGroupsToAdd.length
|
||||
: true;
|
||||
: !inValidUserDetail || activeTab == 2;
|
||||
};
|
||||
|
||||
const isEditing = userDrawerMode === USER_DRAWER_MODES.EDIT;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isChangeRoleModalOpen && (
|
||||
<ModalBase
|
||||
title={
|
||||
<div className="my-3" data-cy="modal-title">
|
||||
<span className="tj-text-md font-weight-500">Edit user role</span>
|
||||
<div className="tj-text-sm text-muted" data-cy="user-email">
|
||||
{currentEditingUser?.email}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
handleConfirm={editUser}
|
||||
show={isChangeRoleModalOpen}
|
||||
darkMode={darkMode}
|
||||
handleClose={() => {
|
||||
setIsChangeRoleModalOpen(false);
|
||||
onCancel();
|
||||
onClose();
|
||||
}}
|
||||
confirmBtnProps={{ title: 'Continue', tooltipMessage: false }}
|
||||
>
|
||||
<div>{EDIT_ROLE_MESSAGE?.[currentEditingUser?.role_group?.[0]?.name]?.[newRole?.value]?.()}</div>
|
||||
</ModalBase>
|
||||
)}
|
||||
<div className="animation-fade invite-user-drawer-wrap">
|
||||
<div className="drawer-card-wrap invite-user-drawer-wrap">
|
||||
<div className="card-header">
|
||||
|
|
@ -216,7 +298,7 @@ function InviteUsersForm({
|
|||
? 'User groups'
|
||||
: t('header.organization.menus.manageUsers.selectGroup', 'Select Group')}
|
||||
</label>
|
||||
<UserGroupsSelect value={selectedGroups} onChange={onChangeHandler} options={groups} />
|
||||
<UserGroupsSelect value={selectedGroups} onChange={onChangeHandler} options={groupedOptions} />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -234,7 +316,9 @@ function InviteUsersForm({
|
|||
ToolJet won’t be able to recognise files in any other format.{' '}
|
||||
</p>
|
||||
<ButtonSolid
|
||||
href="../../assets/csv/sample_upload.csv"
|
||||
href={`${window.public_config?.TOOLJET_HOST}${
|
||||
window.public_config?.SUB_PATH ? window.public_config?.SUB_PATH : '/'
|
||||
}assets/csv/sample_upload.csv`}
|
||||
download="sample_upload.csv"
|
||||
variant="tertiary"
|
||||
className="download-template-btn"
|
||||
|
|
@ -255,6 +339,7 @@ function InviteUsersForm({
|
|||
handleFileChange={handleFileChange}
|
||||
inviteBulkUsers={inviteBulkUsers}
|
||||
onDrop={onDrop}
|
||||
setFileUpload={setFileUpload}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -274,7 +359,7 @@ function InviteUsersForm({
|
|||
form={activeTab == 1 ? 'inviteByEmail' : 'inviteBulkUsers'}
|
||||
type="submit"
|
||||
variant="primary"
|
||||
disabled={uploadingUsers || creatingUser || !isEdited()}
|
||||
disabled={uploadingUsers || creatingUser || !isEdited() || (activeTab !== 1 && !fileUpload)}
|
||||
data-cy={activeTab == 1 ? 'button-invite-users' : 'button-upload-users'}
|
||||
leftIcon={activeTab == 1 ? 'sent' : 'fileupload'}
|
||||
width="20"
|
||||
|
|
|
|||
|
|
@ -11,7 +11,9 @@ 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';
|
||||
import HeaderSkeleton from '@/_ui/FolderSkeleton/HeaderSkeleton';
|
||||
import SolidIcon from '@/_ui/Icon/SolidIcons';
|
||||
|
||||
class ManageOrgUsersComponent extends React.Component {
|
||||
constructor(props) {
|
||||
|
|
@ -28,6 +30,7 @@ class ManageOrgUsersComponent extends React.Component {
|
|||
errors: {},
|
||||
meta: {
|
||||
total_count: 0,
|
||||
currentPage: 1,
|
||||
},
|
||||
currentPage: 1,
|
||||
options: {},
|
||||
|
|
@ -37,6 +40,12 @@ class ManageOrgUsersComponent extends React.Component {
|
|||
userDrawerMode: USER_DRAWER_MODES.CREATE,
|
||||
newSelectedGroups: [],
|
||||
existingGroupsToRemove: [],
|
||||
showErrorModal: false,
|
||||
errorModalMessage: '',
|
||||
errorItemList: [],
|
||||
errorTitle: '',
|
||||
errorIconName: 'usergear',
|
||||
resetSearch: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -74,7 +83,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;
|
||||
}
|
||||
|
|
@ -97,11 +105,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) => {
|
||||
|
|
@ -111,7 +130,7 @@ class ManageOrgUsersComponent extends React.Component {
|
|||
.archive(id)
|
||||
.then(() => {
|
||||
toast.success('The user has been archived');
|
||||
this.setState({ archivingUser: null });
|
||||
this.setState({ archivingUser: null, resetSearch: !this.state.resetSearch });
|
||||
this.fetchUsers(this.state.currentPage, this.state.options);
|
||||
})
|
||||
.catch(({ error }) => {
|
||||
|
|
@ -127,7 +146,7 @@ class ManageOrgUsersComponent extends React.Component {
|
|||
.unarchive(id)
|
||||
.then(() => {
|
||||
toast.success('The user has been unarchived');
|
||||
this.setState({ unarchivingUser: null });
|
||||
this.setState({ unarchivingUser: null, resetSearch: !this.state.resetSearch });
|
||||
this.fetchUsers(this.state.currentPage, this.state.options);
|
||||
})
|
||||
.catch(({ error }) => {
|
||||
|
|
@ -136,6 +155,7 @@ class ManageOrgUsersComponent extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
//Need to work on that
|
||||
inviteBulkUsers = (event) => {
|
||||
event.preventDefault();
|
||||
if (this.handleFileValidation()) {
|
||||
|
|
@ -159,7 +179,25 @@ class ManageOrgUsersComponent extends React.Component {
|
|||
});
|
||||
})
|
||||
.catch(({ error }) => {
|
||||
toast.error(error, { position: 'top-center' });
|
||||
if (error?.error) {
|
||||
this.setState({
|
||||
showErrorModal: true,
|
||||
errorModalMessage: error.error,
|
||||
errorTitle: error?.title || 'Conflicting permissions',
|
||||
errorItemList: error?.data,
|
||||
errorIconName: 'usergear',
|
||||
});
|
||||
this.setState({ creatingUser: false, uploadingUsers: false });
|
||||
return;
|
||||
}
|
||||
toast.error(error || 'Please check the format of CSV file', {
|
||||
position: 'top-center',
|
||||
style: {
|
||||
minWidth: '200px',
|
||||
whiteSpace: 'nowrap', // Prevent text from wrapping to the next line
|
||||
wordBreak: 'keep-all', // Prevent word breaks
|
||||
},
|
||||
});
|
||||
this.setState({ uploadingUsers: false });
|
||||
});
|
||||
}
|
||||
|
|
@ -179,7 +217,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()) {
|
||||
|
|
@ -202,11 +240,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: role }),
|
||||
};
|
||||
service(currentOrgUserId, isEditing ? updateUserBody : createUserBody)
|
||||
.then(() => {
|
||||
|
|
@ -218,11 +258,22 @@ class ManageOrgUsersComponent extends React.Component {
|
|||
isInviteUsersDrawerOpen: false,
|
||||
currentEditingUser: null,
|
||||
userDrawerMode: USER_DRAWER_MODES.CREATE,
|
||||
resetSearch: !this.state.resetSearch,
|
||||
});
|
||||
})
|
||||
.catch(({ error }) => {
|
||||
if (error?.error) {
|
||||
this.setState({
|
||||
showErrorModal: true,
|
||||
errorModalMessage: error.error,
|
||||
errorTitle: error?.title || 'Conflicting permissions',
|
||||
errorItemList: error?.data,
|
||||
errorIconName: 'usergear',
|
||||
});
|
||||
this.setState({ creatingUser: false });
|
||||
return;
|
||||
}
|
||||
toast.error(error);
|
||||
this.setState({ creatingUser: false });
|
||||
});
|
||||
} else {
|
||||
this.setState({ creatingUser: false, file: null, isInviteUsersDrawerOpen: true });
|
||||
|
|
@ -252,6 +303,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);
|
||||
};
|
||||
|
|
@ -291,10 +352,25 @@ class ManageOrgUsersComponent extends React.Component {
|
|||
meta,
|
||||
currentEditingUser,
|
||||
userDrawerMode,
|
||||
showErrorModal,
|
||||
errorModalMessage,
|
||||
errorItemList,
|
||||
errorTitle,
|
||||
errorIconName,
|
||||
resetSearch,
|
||||
} = this.state;
|
||||
return (
|
||||
<ErrorBoundary showFallback={true}>
|
||||
<div className="wrapper org-users-page animation-fade">
|
||||
<EditRoleErrorModal
|
||||
darkMode={this.props.darkMode}
|
||||
show={showErrorModal}
|
||||
errorMessage={errorModalMessage}
|
||||
errorTitle={errorTitle}
|
||||
listItems={errorItemList}
|
||||
iconName={errorIconName}
|
||||
onClose={this.clearErrorState}
|
||||
/>
|
||||
{this.state.isInviteUsersDrawerOpen && (
|
||||
<ManageOrgUsersDrawer
|
||||
isInviteUsersDrawerOpen={this.state.isInviteUsersDrawerOpen}
|
||||
|
|
@ -311,6 +387,7 @@ class ManageOrgUsersComponent extends React.Component {
|
|||
currentEditingUser={currentEditingUser}
|
||||
setUserValues={this.setUserValues}
|
||||
creatingUser={this.state.creatingUser}
|
||||
darkMode={this.props.darkMode}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
@ -346,16 +423,24 @@ class ManageOrgUsersComponent extends React.Component {
|
|||
filterList={this.filterList}
|
||||
darkMode={this.props.darkMode}
|
||||
clearIconPressed={() => this.fetchUsers()}
|
||||
resetSearch={resetSearch}
|
||||
/>
|
||||
|
||||
{users?.length === 0 && (
|
||||
<div className="workspace-settings-table-wrap">
|
||||
<div className="d-flex justify-content-center flex-column tj-user-table-wrapper">
|
||||
<div className="d-flex justify-content-center align-items-center mb-2">
|
||||
<div className="user-not-found-svg">
|
||||
<SolidIcon name="warning-user-notfound" fill="var(--icon-strong)" />
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-center font-weight-bold" data-cy="text-no-result-found">
|
||||
No result found
|
||||
</span>
|
||||
<small className="text-center text-muted" data-cy="text-try-changing-filters">
|
||||
Try changing the filters
|
||||
<small className="text-center text-secondary" data-cy="text-try-changing-filters">
|
||||
There were no results found for your search. Please
|
||||
<br />
|
||||
try changing the filters and try again.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import Drawer from '@/_ui/Drawer';
|
||||
import InviteUsersForm from './InviteUsersForm';
|
||||
import { groupPermissionService } from '@/_services';
|
||||
import { groupPermissionService, groupPermissionV2Service } from '@/_services';
|
||||
import { authenticationService } from '../_services/authentication.service';
|
||||
import { USER_DRAWER_MODES } from '@/_helpers/utils';
|
||||
import { propTypes } from 'react-bootstrap/esm/Image';
|
||||
|
||||
const ManageOrgUsersDrawer = ({
|
||||
isInviteUsersDrawerOpen,
|
||||
|
|
@ -20,6 +21,7 @@ const ManageOrgUsersDrawer = ({
|
|||
userDrawerMode,
|
||||
setUserValues,
|
||||
creatingUser,
|
||||
darkMode,
|
||||
}) => {
|
||||
const [groups, setGroups] = useState([]);
|
||||
|
||||
|
|
@ -27,11 +29,13 @@ const ManageOrgUsersDrawer = ({
|
|||
|
||||
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;
|
||||
|
|
@ -41,19 +45,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);
|
||||
})
|
||||
|
|
@ -93,6 +95,7 @@ const ManageOrgUsersDrawer = ({
|
|||
userDrawerMode={userDrawerMode}
|
||||
setUserValues={setUserValues}
|
||||
creatingUser={creatingUser}
|
||||
darkMode={darkMode}
|
||||
/>
|
||||
</Drawer>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="mb-2 d-flex align-items-center">
|
||||
<SolidIcon name={type === 'default' ? 'usergear' : 'usergroup'} />
|
||||
<span className="ml-1 group-title">{type === 'default' ? 'USER ROLE' : 'Custom groups'}</span>
|
||||
{type === 'default' && <span style={{ color: 'red' }}>*</span>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<components.Option
|
||||
{...rest}
|
||||
|
|
@ -62,8 +72,12 @@ export function UserGroupsSelect(props) {
|
|||
className={isDisabled && 'disabled'}
|
||||
>
|
||||
<input
|
||||
style={{ width: '1.2rem', height: '1.2rem', borderRadius: '6px !important' }}
|
||||
type="checkbox"
|
||||
style={
|
||||
data.groupType === 'default'
|
||||
? { height: '1.3rem' }
|
||||
: { width: '1.2rem', height: '1.2rem', borderRadius: '6px !important' }
|
||||
}
|
||||
type={data.groupType === 'default' ? 'radio' : 'checkbox'}
|
||||
className="form-check-input"
|
||||
checked={isSelected}
|
||||
data-cy="group-check-input"
|
||||
|
|
@ -72,6 +86,13 @@ export function UserGroupsSelect(props) {
|
|||
</components.Option>
|
||||
);
|
||||
};
|
||||
const MultiValueRemove = (props) => {
|
||||
// Conditionally render the close icon
|
||||
if (props.data.groupType === 'default') {
|
||||
return null; // Do not render the close icon
|
||||
}
|
||||
return <components.MultiValueRemove {...props} />;
|
||||
};
|
||||
|
||||
const MultiValue = (props) => (
|
||||
<components.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)',
|
||||
|
|
@ -129,7 +156,7 @@ export function UserGroupsSelect(props) {
|
|||
}),
|
||||
menu: (base) => ({
|
||||
...base,
|
||||
background: 'unset',
|
||||
background: 'var(--surfaces-app-bg-default)',
|
||||
'.add-group-btn': {
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
|
|
@ -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'}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ const MarketplacePage = ({ darkMode, switchDarkMode }) => {
|
|||
<div className="marketplace-body">
|
||||
<div className="p-3">
|
||||
<div className="row g-4">
|
||||
<div className="marketplace-page-sidebar mt-3">
|
||||
<div className="marketplace-page-sidebar mt-3 mx-3">
|
||||
<div className="subheader mb-2">Plugins</div>
|
||||
<div className="list-group mb-3">
|
||||
{['Installed', 'Marketplace'].map((item, index) => (
|
||||
|
|
|
|||
6
frontend/src/OrganizationSettingsPage/constant.js
Normal file
6
frontend/src/OrganizationSettingsPage/constant.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export const workspaceSettingsLinks = [
|
||||
{ id: 'users', name: 'Users', route: 'users', conditions: ['admin'] },
|
||||
{ id: 'groups', name: 'Groups', route: 'groups', conditions: ['admin'] },
|
||||
{ id: 'workspacelogin', name: 'Workspace login', route: 'workspace-login', conditions: ['admin'] },
|
||||
{ id: 'workspacevariables', name: 'Workspace variables', route: 'workspace-variables', conditions: ['admin'] },
|
||||
];
|
||||
|
|
@ -1,57 +1,59 @@
|
|||
import React, { useEffect, useState, useContext } from 'react';
|
||||
import cx from 'classnames';
|
||||
import { useParams, Outlet, Link, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { Outlet, Link, useNavigate, useLocation } from 'react-router-dom';
|
||||
|
||||
import Layout from '@/_ui/Layout';
|
||||
import { authenticationService } from '@/_services';
|
||||
import { BreadCrumbContext } from '../App/App';
|
||||
import FolderList from '@/_ui/FolderList/FolderList';
|
||||
import { OrganizationList } from '../_components/OrganizationManager/List';
|
||||
import { getWorkspaceId } from '@/_helpers/utils';
|
||||
import { getSubpath } from '@/_helpers/routes';
|
||||
import { workspaceSettingsLinks } from './constant';
|
||||
|
||||
export function OrganizationSettings(props) {
|
||||
const [admin, setAdmin] = useState(authenticationService.currentSessionValue?.admin);
|
||||
const [selectedTab, setSelectedTab] = useState(admin ? 'Users & permissions' : 'manageEnvVars');
|
||||
const admin = authenticationService.currentSessionValue?.admin;
|
||||
const [selectedTab, setSelectedTab] = useState(admin ? workspaceSettingsLinks[0].id : 'workspacevariables');
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { updateSidebarNAV } = useContext(BreadCrumbContext);
|
||||
const { workspaceId } = useParams();
|
||||
const [conditionObj, setConditionObj] = useState({ admin: authenticationService.currentSessionValue?.admin });
|
||||
|
||||
const sideBarNavs = ['Users', 'Groups', 'Workspace login', 'Workspace variables'];
|
||||
const defaultOrgName = (groupName) => {
|
||||
switch (groupName) {
|
||||
case 'users':
|
||||
return 'Users';
|
||||
case 'groups':
|
||||
return 'Groups';
|
||||
case 'workspace-login':
|
||||
return 'Workspace login';
|
||||
case 'workspace-variables':
|
||||
return 'Workspace variables';
|
||||
default:
|
||||
return groupName;
|
||||
const checkConditions = (conditions, conditionsObj) => {
|
||||
if (!conditions || conditions.length === 0) {
|
||||
return true;
|
||||
}
|
||||
return conditions.every((condition) => conditionsObj?.[condition] === true);
|
||||
};
|
||||
|
||||
//Filtered Links from the workspace settings links array
|
||||
const filteredLinks = () =>
|
||||
workspaceSettingsLinks.filter((item) => {
|
||||
return checkConditions(item.conditions, conditionObj);
|
||||
});
|
||||
|
||||
const getMenuFromRoute = (route) => {
|
||||
return workspaceSettingsLinks?.find((e) => e.route === route) || {};
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = authenticationService.currentSession.subscribe((newOrd) => {
|
||||
setAdmin(newOrd?.admin);
|
||||
setConditionObj({ admin: newOrd?.admin });
|
||||
});
|
||||
admin ? updateSidebarNAV('Users') : updateSidebarNAV('Workspace variables');
|
||||
|
||||
() => subscription.unsubsciption();
|
||||
const selectedTabFromRoute = location.pathname.split('/').pop();
|
||||
if (selectedTabFromRoute === 'workspace-settings') {
|
||||
setSelectedTab(admin ? 'Users' : 'Workspace variables');
|
||||
const subPath = getSubpath();
|
||||
const path = subPath ? `${subPath}/${workspaceId}/workspace-settings` : `/${workspaceId}/workspace-settings`;
|
||||
window.location.href = admin ? `${path}/users` : `${path}/workspace-variables`;
|
||||
// No Sub routes added loading first one
|
||||
setSelectedTab(admin ? workspaceSettingsLinks[0].id : 'workspacevariables');
|
||||
} else {
|
||||
setSelectedTab(defaultOrgName(selectedTabFromRoute));
|
||||
setSelectedTab(getMenuFromRoute(selectedTabFromRoute)?.id);
|
||||
}
|
||||
updateSidebarNAV(defaultOrgName(selectedTabFromRoute));
|
||||
}, [navigate, workspaceId, authenticationService.currentSessionValue?.admin]);
|
||||
|
||||
return () => subscription.unsubscribe();
|
||||
}, [authenticationService.currentSessionValue?.admin]);
|
||||
|
||||
useEffect(() => {
|
||||
const menu = workspaceSettingsLinks?.find((m) => m.id === selectedTab);
|
||||
updateSidebarNAV(menu?.name || '');
|
||||
navigate(menu?.route || '');
|
||||
}, [selectedTab]);
|
||||
|
||||
return (
|
||||
<Layout switchDarkMode={props.switchDarkMode} darkMode={props.darkMode}>
|
||||
|
|
@ -59,12 +61,11 @@ export function OrganizationSettings(props) {
|
|||
<div className="row gx-0">
|
||||
<div className="organization-page-sidebar col ">
|
||||
<div className="workspace-nav-list-wrap">
|
||||
{sideBarNavs.map((item, index) => {
|
||||
{filteredLinks().map((item, index) => {
|
||||
const Wrapper = ({ children }) => <>{children}</>;
|
||||
return (
|
||||
<Wrapper key={index}>
|
||||
<Link
|
||||
to={`/${workspaceId}/workspace-settings/${item.toLowerCase().replace(/\s+/g, '-')}`} // Update the URL path here
|
||||
key={index}
|
||||
style={{
|
||||
textDecoration: 'none',
|
||||
|
|
@ -74,30 +75,26 @@ export function OrganizationSettings(props) {
|
|||
backgroundColor: 'inherit',
|
||||
}}
|
||||
>
|
||||
{admin && (
|
||||
<FolderList
|
||||
className="workspace-settings-nav-items"
|
||||
key={index}
|
||||
onClick={() => {
|
||||
setSelectedTab(defaultOrgName(item));
|
||||
if (item == 'Users') updateSidebarNAV('Users');
|
||||
else updateSidebarNAV(item);
|
||||
}}
|
||||
selectedItem={selectedTab == defaultOrgName(item)}
|
||||
renderBadgeForItems={['Workspace constants']}
|
||||
renderBadge={() => (
|
||||
<span
|
||||
style={{ width: '40px', textTransform: 'lowercase' }}
|
||||
className="badge bg-color-primary badge-pill"
|
||||
>
|
||||
new
|
||||
</span>
|
||||
)}
|
||||
dataCy={item.toLowerCase().replace(/\s+/g, '-')}
|
||||
>
|
||||
{item}
|
||||
</FolderList>
|
||||
)}
|
||||
<FolderList
|
||||
className="workspace-settings-nav-items"
|
||||
key={index}
|
||||
onClick={() => {
|
||||
setSelectedTab(item.id);
|
||||
}}
|
||||
selectedItem={selectedTab == item.id}
|
||||
renderBadgeForItems={[]}
|
||||
renderBadge={() => (
|
||||
<span
|
||||
style={{ width: '40px', textTransform: 'lowercase' }}
|
||||
className="badge bg-color-primary badge-pill"
|
||||
>
|
||||
new
|
||||
</span>
|
||||
)}
|
||||
dataCy={item.name.toLowerCase().replace(/\s+/g, '-')}
|
||||
>
|
||||
{item.name}
|
||||
</FolderList>
|
||||
</Link>
|
||||
</Wrapper>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,3 +3,4 @@ export * from './AdminRoute';
|
|||
export * from './AppsRoute';
|
||||
export * from './SwitchWorkspaceRoute';
|
||||
export * from './OrganizationInviteRoute';
|
||||
export * from './AuthRoute';
|
||||
|
|
|
|||
|
|
@ -333,7 +333,7 @@ class SignupPageComponent extends React.Component {
|
|||
<a href="https://www.tooljet.com/terms" data-cy="terms-of-service-link">
|
||||
Terms of Service{' '}
|
||||
</a>
|
||||
&
|
||||
<span>& </span>
|
||||
<a href="https://www.tooljet.com/privacy" data-cy="privacy-policy-link">
|
||||
{' '}
|
||||
Privacy Policy
|
||||
|
|
|
|||
|
|
@ -183,7 +183,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;
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import ToolJetDbOperations from '@/Editor/QueryManager/QueryEditors/TooljetDatab
|
|||
import { orgEnvironmentVariableService, orgEnvironmentConstantService } from '../_services';
|
||||
import { find, isEmpty } from 'lodash';
|
||||
import { ButtonSolid } from './AppButton';
|
||||
import { Constants } from '@/_helpers/utils';
|
||||
|
||||
const DynamicForm = ({
|
||||
schema,
|
||||
|
|
@ -53,9 +54,16 @@ const DynamicForm = ({
|
|||
React.useEffect(() => {
|
||||
if (isGDS) {
|
||||
orgEnvironmentConstantService.getConstantsFromEnvironment(currentAppEnvironmentId).then((data) => {
|
||||
const constants = {};
|
||||
data.constants.map((constant) => {
|
||||
constants[constant.name] = constant.value;
|
||||
const constants = {
|
||||
globals: {},
|
||||
secrets: {},
|
||||
};
|
||||
data.constants.forEach((constant) => {
|
||||
if (constant.type === Constants.Secret) {
|
||||
constants.secrets[constant.name] = constant.value;
|
||||
} else {
|
||||
constants.globals[constant.name] = constant.value;
|
||||
}
|
||||
});
|
||||
|
||||
setCurrentOrgEnvironmentConstants(constants);
|
||||
|
|
@ -202,7 +210,8 @@ const DynamicForm = ({
|
|||
return {
|
||||
type,
|
||||
placeholder: useEncrypted ? '**************' : description,
|
||||
className: `form-control${handleToggle(controller)}`,
|
||||
className: `form-control${handleToggle(controller)} mb-0`,
|
||||
style: { marginBottom: '0px !important' },
|
||||
value: options?.[key]?.value || '',
|
||||
...(type === 'textarea' && { rows: rows }),
|
||||
...(helpText && { helpText }),
|
||||
|
|
|
|||
|
|
@ -38,16 +38,17 @@ export default function LogoNavDropdown({ darkMode }) {
|
|||
<span>Database</span>
|
||||
</Link>
|
||||
)}
|
||||
<Link
|
||||
to={getPrivateRoute('data_sources')}
|
||||
className="dropdown-item tj-text tj-text-xsm"
|
||||
target="_blank"
|
||||
data-cy="data-source-option"
|
||||
>
|
||||
<SolidIcon name="datasource" width="20" />
|
||||
<span>Data sources</span>
|
||||
</Link>
|
||||
|
||||
{admin && (
|
||||
<Link
|
||||
to={getPrivateRoute('data_sources')}
|
||||
className="dropdown-item tj-text tj-text-xsm"
|
||||
target="_blank"
|
||||
data-cy="data-source-option"
|
||||
>
|
||||
<SolidIcon name="datasource" width="20" />
|
||||
<span>Data sources</span>
|
||||
</Link>
|
||||
)}
|
||||
<Link
|
||||
to={getPrivateRoute('workspace_constants')}
|
||||
className="dropdown-item tj-text tj-text-xsm"
|
||||
|
|
|
|||
|
|
@ -19,9 +19,9 @@ function MultiSelectUser({
|
|||
const listOfOptions = useRef([]);
|
||||
|
||||
useEffect(() => {
|
||||
setOptions(filterOptions(listOfOptions.current));
|
||||
setOptions(listOfOptions.current);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedValues, listOfOptions.current]);
|
||||
}, [JSON.stringify(selectedValues), listOfOptions.current]);
|
||||
|
||||
const searchFunction = useCallback(
|
||||
async (query) => {
|
||||
|
|
@ -35,14 +35,19 @@ function MultiSelectUser({
|
|||
);
|
||||
|
||||
function renderCustom(props, option) {
|
||||
const valuePresent = selectedValues.some((item) => item.value === option.value);
|
||||
return (
|
||||
<div className={`item-renderer`}>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
checked={valuePresent}
|
||||
onClick={(e) => {
|
||||
onSelect([...selectedValues, option]);
|
||||
if (!valuePresent) {
|
||||
onSelect([...selectedValues, option]);
|
||||
} else {
|
||||
onSelect([...selectedValues.filter((item) => item.value !== option.value)]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="d-flex flex-column" style={{ marginLeft: '12px' }}>
|
||||
|
|
@ -67,7 +72,7 @@ function MultiSelectUser({
|
|||
[selectedValues]
|
||||
);
|
||||
return (
|
||||
<div className="tj-ms tj-ms-count">
|
||||
<div className="tj-ms tj-ms-count" style={{ width: '100%', paddingRight: '0px' }}>
|
||||
<FilterPreview text={`${selectedValues.length} selected`} onClose={selectedValues.length ? onReset : undefined} />
|
||||
<Select
|
||||
className={className}
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ export const NotificationCenter = ({ darkMode }) => {
|
|||
<p className="empty-title mb-1" data-cy="empty-notification-title">
|
||||
{t('header.notificationCenter.youAreCaughtUp', `You're all caught up!`)}
|
||||
</p>
|
||||
<p className="empty-subtitle text-muted" data-cy="empty-notification-subtitle">
|
||||
<p className="empty-subtitle" data-cy="empty-notification-subtitle">
|
||||
{`${t('header.notificationCenter.youDontHaveany', `You don't have any`)} ${
|
||||
!isRead ? t('header.notificationCenter.un', 'un') : ''
|
||||
}${t('header.notificationCenter.read', 'read')} ${t(
|
||||
|
|
@ -91,7 +91,7 @@ export const NotificationCenter = ({ darkMode }) => {
|
|||
</div>
|
||||
<div className="card-footer text-center margin-auto">
|
||||
<span
|
||||
className="text-muted text-decoration-none cursor-pointer"
|
||||
className="text-decoration-none cursor-pointer"
|
||||
onClick={() => setIsRead(!isRead)}
|
||||
data-cy="notifications-card-footer"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useSpring, config, animated } from 'react-spring';
|
||||
import { resolveReferences } from '../../_helpers/utils';
|
||||
import { resolveReferences, verifyConstant } from '../../_helpers/utils';
|
||||
import { Alert } from '../../_ui/Alert';
|
||||
import useHeight from '@/_hooks/use-height-transition';
|
||||
import React from 'react';
|
||||
|
|
@ -7,9 +7,11 @@ import React from 'react';
|
|||
export const OrgConstantVariablesPreviewBox = ({ workspaceVariables, workspaceConstants, value, isFocused }) => {
|
||||
const getResolveValueType = (currentValue) => {
|
||||
if (!currentValue) return null;
|
||||
|
||||
if (currentValue.includes('secrets')) {
|
||||
return 'Workspace secret constant';
|
||||
}
|
||||
if (currentValue.includes('constants')) {
|
||||
return 'Workspace Constant';
|
||||
return 'Workspace global constant';
|
||||
}
|
||||
|
||||
if (currentValue.includes('client')) {
|
||||
|
|
@ -25,7 +27,8 @@ export const OrgConstantVariablesPreviewBox = ({ workspaceVariables, workspaceCo
|
|||
const shouldResolve =
|
||||
typeof value === 'string' &&
|
||||
((value.includes('%%') && (value.includes('client.') || value.includes('server.'))) ||
|
||||
(value.includes('{{') && value.includes('constants.')));
|
||||
(value.includes('{{') && value.includes('constants.')) ||
|
||||
(value.includes('{{') && value.includes('secrets.')));
|
||||
|
||||
if (!shouldResolve) return null;
|
||||
|
||||
|
|
@ -35,34 +38,31 @@ export const OrgConstantVariablesPreviewBox = ({ workspaceVariables, workspaceCo
|
|||
<ResolvedValue
|
||||
value={value}
|
||||
isFocused={isFocused}
|
||||
state={{ ...workspaceVariables, constants: workspaceConstants }}
|
||||
state={{
|
||||
...workspaceVariables,
|
||||
constants: workspaceConstants?.globals || {},
|
||||
secrets: workspaceConstants?.secrets || {},
|
||||
}}
|
||||
type={valueType}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const verifyConstant = (value, definedConstants) => {
|
||||
const constantRegex = /{{constants\.([a-zA-Z0-9_]+)}}/g;
|
||||
if (typeof value !== 'string') {
|
||||
return [];
|
||||
}
|
||||
const matches = value.match(constantRegex);
|
||||
if (!matches) {
|
||||
return [];
|
||||
}
|
||||
const resolvedMatches = matches.map((match) => {
|
||||
const cleanedMatch = match.replace(/{{constants\./, '').replace(/}}/, '');
|
||||
return Object.keys(definedConstants).includes(cleanedMatch) ? null : cleanedMatch;
|
||||
});
|
||||
const invalidConstants = resolvedMatches?.filter((item) => item != null);
|
||||
if (invalidConstants?.length) {
|
||||
return invalidConstants;
|
||||
}
|
||||
};
|
||||
|
||||
const ResolvedValue = ({ value, isFocused, state = {}, type }) => {
|
||||
const [preview, error] = resolveReferences(value, null, {}, true, true);
|
||||
const isSecret = type === 'Workspace secret constant';
|
||||
const hiddenSecretText = 'Values of secret constants are hidden';
|
||||
const invalidConstants = verifyConstant(value, state.constants, state.secrets);
|
||||
let preview;
|
||||
let error;
|
||||
if (invalidConstants?.length) {
|
||||
[preview, error] = [value, `Undefined constants: ${invalidConstants}`];
|
||||
} else {
|
||||
[preview, error] = resolveReferences(value, state, null, {}, true, true);
|
||||
|
||||
if (isSecret && !error) {
|
||||
preview = hiddenSecretText;
|
||||
}
|
||||
}
|
||||
const previewType = typeof preview;
|
||||
|
||||
let resolvedValue = preview;
|
||||
|
|
@ -71,8 +71,9 @@ const ResolvedValue = ({ value, isFocused, state = {}, type }) => {
|
|||
? 'HiddenEnvironmentVariable'
|
||||
: error?.toString();
|
||||
const isValidError = error && errorMessage !== 'HiddenEnvironmentVariable';
|
||||
const isUndefinedConstantsError = error && errorMessage.includes('Undefined constants:');
|
||||
|
||||
if (error && !isValidError) {
|
||||
if (error && (!isValidError || isUndefinedConstantsError)) {
|
||||
resolvedValue = errorMessage;
|
||||
}
|
||||
|
||||
|
|
@ -97,7 +98,7 @@ const ResolvedValue = ({ value, isFocused, state = {}, type }) => {
|
|||
}
|
||||
};
|
||||
|
||||
const isConstant = type === 'Workspace Constant';
|
||||
const isConstant = type === 'Workspace global constant' || type === 'Workspace secret constant';
|
||||
|
||||
const [heightRef, currentHeight] = useHeight();
|
||||
|
||||
|
|
@ -117,10 +118,14 @@ const ResolvedValue = ({ value, isFocused, state = {}, type }) => {
|
|||
ref={heightRef}
|
||||
className={`dynamic-variable-preview px-1 py-1 ${isValidError ? 'bg-red-lt' : 'bg-green-lt'}`}
|
||||
>
|
||||
<div className="alert-banner-type-text">
|
||||
<div className="alert-banner-type-text" data-cy="variable-preview">
|
||||
<div className="d-flex my-1">
|
||||
<div className="flex-grow-1" style={{ fontWeight: 800, textTransform: 'capitalize' }}>
|
||||
{isValidError ? 'Error' : ` ${type} - ${previewType}`}
|
||||
<div
|
||||
className="flex-grow-1"
|
||||
style={{ fontWeight: 800, textTransform: 'capitalize' }}
|
||||
data-cy="alert-banner-type-text"
|
||||
>
|
||||
{isValidError ? 'Error' : isConstant ? null : ` ${type} - ${previewType}`}
|
||||
</div>
|
||||
</div>
|
||||
{getPreviewContent(resolvedValue, previewType)}
|
||||
|
|
|
|||
|
|
@ -262,7 +262,7 @@ export function GithubSSOModal({ settings, onClose, onUpdateSSOSettings, isInsta
|
|||
<label className="form-label" data-cy="redirect-url-label">
|
||||
{t('header.organization.menus.manageSSO.github.redirectUrl', 'Redirect URL')}
|
||||
</label>
|
||||
<div className="d-flex justify-content-between form-control align-items-center">
|
||||
<div className="d-flex justify-content-between form-control-org-login align-items-center">
|
||||
<p data-cy="redirect-url" id="redirect-url">{`${window.public_config?.TOOLJET_HOST}${
|
||||
window.public_config?.SUB_PATH ? window.public_config?.SUB_PATH : '/'
|
||||
}sso/git/${configId}`}</p>
|
||||
|
|
|
|||
|
|
@ -193,7 +193,7 @@ export function GoogleSSOModal({ settings, onClose, onUpdateSSOSettings, isInsta
|
|||
<label className="form-label" data-cy="redirect-url-label">
|
||||
{t('header.organization.menus.manageSSO.google.redirectUrl', 'Redirect URL')}
|
||||
</label>
|
||||
<div className="d-flex justify-content-between form-control align-items-center">
|
||||
<div className="d-flex justify-content-between form-control-org-login align-items-center">
|
||||
<p data-cy="redirect-url" id="redirect-url">{`${window.public_config?.TOOLJET_HOST}${
|
||||
window.public_config?.SUB_PATH ? window.public_config?.SUB_PATH : '/'
|
||||
}sso/google/${configId}`}</p>
|
||||
|
|
|
|||
|
|
@ -341,7 +341,7 @@ class OrganizationLogin extends React.Component {
|
|||
</label>
|
||||
<div className="help-text danger-text-login">
|
||||
<div data-cy="enable-sign-up-helper-text">
|
||||
Users will be able to sign up without being invited
|
||||
Users will be able to sign up as end-users without being invited
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -80,11 +80,13 @@ class SSOConfiguration extends React.Component {
|
|||
|
||||
componentDidMount() {
|
||||
const initialState = this.initializeOptionStates(this.props.ssoOptions);
|
||||
const enabledSSOCount = this.getCountOfEnabledSSO();
|
||||
this.setState({ ...initialState });
|
||||
this.setState({ ssoOptions: this.props.ssoOptions });
|
||||
this.setState({ defaultSSO: this.props.defaultSSO });
|
||||
this.setState({ isAnySSOEnabled: this.props.isAnySSOEnabled });
|
||||
this.setState({ instanceSSO: this.props.instanceSSO });
|
||||
this.setState({ inheritedInstanceSSO: enabledSSOCount });
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState } from 'react';
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { organizationService } from '@/_services';
|
||||
import AlertDialog from '@/_ui/AlertDialog';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
|
@ -7,6 +7,7 @@ import { validateName, handleHttpErrorMessages } from '@/_helpers/utils';
|
|||
import { appendWorkspaceId, getHostURL } from '@/_helpers/routes';
|
||||
import _ from 'lodash';
|
||||
import { FormWrapper } from '@/_components/FormWrapper';
|
||||
import { toast } from 'react-hot-toast';
|
||||
|
||||
export const CreateOrganization = ({ showCreateOrg, setShowCreateOrg }) => {
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
|
|
@ -18,10 +19,10 @@ export const CreateOrganization = ({ showCreateOrg, setShowCreateOrg }) => {
|
|||
const [isSlugDisabled, setSlugDisabled] = useState(true);
|
||||
const darkMode = localStorage.getItem('darkMode') === 'true';
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isSlugSet = useRef(false); // Flag to track if slug has been initially set
|
||||
const sluginput = useRef('');
|
||||
const createOrganization = () => {
|
||||
let emptyError = false;
|
||||
|
||||
[name, slug].map((field, index) => {
|
||||
if (!field?.value?.trim()) {
|
||||
index === 0
|
||||
|
|
@ -42,6 +43,7 @@ export const CreateOrganization = ({ showCreateOrg, setShowCreateOrg }) => {
|
|||
setIsCreating(true);
|
||||
organizationService.createOrganization({ name: name.value, slug: slugValue }).then(
|
||||
() => {
|
||||
toast.success('Workspace created successfully');
|
||||
setIsCreating(false);
|
||||
const newPath = appendWorkspaceId(slugValue, location.pathname, true);
|
||||
window.history.replaceState(null, null, newPath);
|
||||
|
|
@ -77,7 +79,6 @@ export const CreateOrganization = ({ showCreateOrg, setShowCreateOrg }) => {
|
|||
!(field === 'slug'),
|
||||
field === 'slug'
|
||||
);
|
||||
|
||||
/* If the basic validation is passing. then check the uniqueness */
|
||||
if (error?.status === true) {
|
||||
try {
|
||||
|
|
@ -92,7 +93,6 @@ export const CreateOrganization = ({ showCreateOrg, setShowCreateOrg }) => {
|
|||
};
|
||||
}
|
||||
}
|
||||
|
||||
const disabled = !error?.status;
|
||||
const updatedValue = {
|
||||
value,
|
||||
|
|
@ -126,19 +126,56 @@ export const CreateOrganization = ({ showCreateOrg, setShowCreateOrg }) => {
|
|||
setShowCreateOrg(false);
|
||||
setNameDisabled(true);
|
||||
setSlugDisabled(true);
|
||||
isSlugSet.current = false;
|
||||
};
|
||||
|
||||
const delayedSlugChange = _.debounce(async (value) => {
|
||||
setSlugProgress(true);
|
||||
await handleInputChange(value, 'slug');
|
||||
}, 300);
|
||||
|
||||
const delayedNameChange = _.debounce(async (value) => {
|
||||
setWorkspaceNameProgress(true);
|
||||
await handleInputChange(value, 'name');
|
||||
}, 300);
|
||||
useEffect(() => {
|
||||
if (!isSlugSet.current && name.value && !slugProgress && !workspaceNameProgress) {
|
||||
const defaultValue =
|
||||
name?.value
|
||||
.replace(/\s+/g, '')
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9-\s]/g, '') || '';
|
||||
setSlug({ value: defaultValue, error: '' });
|
||||
|
||||
const isDisabled = isCreating || isNameDisabled || isSlugDisabled || slugProgress || workspaceNameProgress;
|
||||
const checkWorkspaceUniqueness = async () => {
|
||||
try {
|
||||
await organizationService.checkWorkspaceUniqueness(null, defaultValue);
|
||||
sluginput.current.value = defaultValue;
|
||||
} catch (errResponse) {
|
||||
let error = {
|
||||
status: false,
|
||||
errorMsg: errResponse?.error,
|
||||
};
|
||||
setSlug({ value: defaultValue, error: error?.errorMsg });
|
||||
sluginput.current.value = defaultValue;
|
||||
}
|
||||
};
|
||||
checkWorkspaceUniqueness();
|
||||
setSlugDisabled(false);
|
||||
setSlugProgress(false);
|
||||
}
|
||||
if (slugProgress && !isSlugSet.current) {
|
||||
// this is to denote that the user has tried editing the slug -- so now slug and name are independent of each other
|
||||
isSlugSet.current = true;
|
||||
}
|
||||
}, [name.value, slug.value, slugProgress, workspaceNameProgress, isSlugSet]);
|
||||
const isDisabled =
|
||||
isCreating ||
|
||||
isNameDisabled ||
|
||||
isSlugDisabled ||
|
||||
slugProgress ||
|
||||
workspaceNameProgress ||
|
||||
slug?.error ||
|
||||
name?.error;
|
||||
|
||||
return (
|
||||
<AlertDialog
|
||||
|
|
@ -168,6 +205,8 @@ export const CreateOrganization = ({ showCreateOrg, setShowCreateOrg }) => {
|
|||
<label className="label tj-input-error" data-cy="workspace-error-label">
|
||||
{name?.error || ''}
|
||||
</label>
|
||||
) : name.value && !workspaceNameProgress ? (
|
||||
<label className="label label-success" data-cy="slug-sucess-label">{`Workspace name accepted!`}</label>
|
||||
) : (
|
||||
<label className="label label-info" data-cy="workspace-name-info-label">
|
||||
Name must be unique and max 50 characters
|
||||
|
|
@ -183,6 +222,7 @@ export const CreateOrganization = ({ showCreateOrg, setShowCreateOrg }) => {
|
|||
className={`form-control ${slug?.error ? 'is-invalid' : 'is-valid'}`}
|
||||
placeholder={t('header.organization.workspaceSlug', 'Unique workspace slug')}
|
||||
disabled={isCreating}
|
||||
ref={sluginput}
|
||||
maxLength={50}
|
||||
onChange={async (e) => {
|
||||
e.persist();
|
||||
|
|
@ -203,6 +243,7 @@ export const CreateOrganization = ({ showCreateOrg, setShowCreateOrg }) => {
|
|||
</svg>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{slug?.error ? (
|
||||
<label className="label tj-input-error" data-cy="input-label-error">
|
||||
{slug?.error || ''}
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ export const ERROR_MESSAGES = {
|
|||
|
||||
export const TOOLTIP_MESSAGES = {
|
||||
SHARE_URL_UNAVAILABLE: 'Share URL is unavailable until current version is released',
|
||||
RELEASE_VERSION_URL_UNAVAILABLE: 'Release the version to make it public',
|
||||
};
|
||||
|
||||
export const DATA_SOURCE_TYPE = {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
export const APP_ERROR_TYPE = {
|
||||
IMPORT_EXPORT_SERVICE: {
|
||||
UNSUPPORTED_VERSION_ERROR: "Can't import higher version application to lower version",
|
||||
UNSUPPORTED_VERSION_ERROR: 'Apps built on later versions of ToolJet cannot be imported',
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -17,6 +17,37 @@ import { componentTypes } from '@/Editor/WidgetManager/components';
|
|||
|
||||
const reservedKeyword = ['app', 'window'];
|
||||
|
||||
export const Constants = {
|
||||
Global: 'Global',
|
||||
Secret: 'Secret',
|
||||
};
|
||||
|
||||
export const verifyConstant = (value, definedConstants = {}, definedSecrets = {}) => {
|
||||
const globalConstantRegex = /{{constants\.([a-zA-Z0-9_]+)}}/g;
|
||||
const secretConstantRegex = /{{secrets\.([a-zA-Z0-9_]+)}}/g;
|
||||
if (typeof value !== 'string') {
|
||||
return [];
|
||||
}
|
||||
const matches = [...(value.match(globalConstantRegex) || []), ...(value.match(secretConstantRegex) || [])];
|
||||
if (!matches) {
|
||||
return [];
|
||||
}
|
||||
const resolvedMatches = matches.map((match) => {
|
||||
const cleanedMatch = match
|
||||
.replace(/{{constants\./, '')
|
||||
.replace(/{{secrets\./, '')
|
||||
.replace(/}}/, '');
|
||||
|
||||
return Object.keys(definedConstants).includes(cleanedMatch) || Object.keys(definedSecrets).includes(cleanedMatch)
|
||||
? null
|
||||
: cleanedMatch;
|
||||
});
|
||||
const invalidConstants = resolvedMatches?.filter((item) => item != null);
|
||||
if (invalidConstants?.length) {
|
||||
return invalidConstants;
|
||||
}
|
||||
};
|
||||
|
||||
export function findProp(obj, prop, defval) {
|
||||
if (typeof defval === 'undefined') defval = null;
|
||||
prop = prop.split('.');
|
||||
|
|
@ -73,6 +104,7 @@ function resolveCode(code, state, customObjects = {}, withError = false, reserve
|
|||
'client',
|
||||
'server',
|
||||
'constants',
|
||||
'secrets',
|
||||
'parameters',
|
||||
'moment',
|
||||
'_',
|
||||
|
|
@ -90,6 +122,7 @@ function resolveCode(code, state, customObjects = {}, withError = false, reserve
|
|||
isJsCode ? undefined : state?.client,
|
||||
isJsCode ? undefined : state?.server,
|
||||
state?.constants, // Passing constants as an argument allows the evaluated code to access and utilize the constants value correctly.
|
||||
state?.secrets || {},
|
||||
state?.parameters,
|
||||
moment,
|
||||
_,
|
||||
|
|
@ -98,10 +131,8 @@ function resolveCode(code, state, customObjects = {}, withError = false, reserve
|
|||
);
|
||||
} catch (err) {
|
||||
error = err;
|
||||
// console.log('eval_error', err);
|
||||
}
|
||||
}
|
||||
|
||||
if (withError) return [result, error];
|
||||
return result;
|
||||
}
|
||||
|
|
@ -227,10 +258,10 @@ export function resolveReferences(
|
|||
|
||||
if (dynamicVariables) {
|
||||
if (dynamicVariables.length === 1 && dynamicVariables[0] === object) {
|
||||
object = resolveReferences(dynamicVariables[0], null, customObjects);
|
||||
object = resolveReferences(dynamicVariables[0], state, null, customObjects, false, false);
|
||||
} else {
|
||||
for (const dynamicVariable of dynamicVariables) {
|
||||
const value = resolveReferences(dynamicVariable, null, customObjects);
|
||||
const value = resolveReferences(dynamicVariable, state, null, customObjects, false, false);
|
||||
if (typeof value !== 'function') {
|
||||
object = object.replace(dynamicVariable, value);
|
||||
}
|
||||
|
|
@ -1068,7 +1099,7 @@ export const validateName = (
|
|||
checkReservedWords = false,
|
||||
allowAllCases = false
|
||||
) => {
|
||||
const newName = name;
|
||||
const newName = name.trim();
|
||||
let errorMsg = '';
|
||||
if (emptyCheck && !newName) {
|
||||
errorMsg = `${nameType} can't be empty`;
|
||||
|
|
@ -1266,11 +1297,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;
|
||||
|
|
@ -1403,3 +1436,14 @@ export const removeNestedDoubleCurlyBraces = (str) => {
|
|||
|
||||
return transformedInput.join('');
|
||||
};
|
||||
export const validatePassword = (value) => {
|
||||
if (!value.trim()) {
|
||||
return 'Password is required';
|
||||
}
|
||||
if (value.length < 5) {
|
||||
return 'Password must be at least 5 characters long';
|
||||
}
|
||||
if (value.length > 100) {
|
||||
return 'Password can be at max 100 characters long';
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
190
frontend/src/_services/groupPermission.v2.service.js
Normal file
190
frontend/src/_services/groupPermission.v2.service.js
Normal file
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -7,20 +7,22 @@ export const orgEnvironmentConstantService = {
|
|||
update,
|
||||
remove,
|
||||
getConstantsFromEnvironment,
|
||||
getConstantsFromApp,
|
||||
getConstantsFromPublicApp,
|
||||
getAllSecrets,
|
||||
};
|
||||
|
||||
function getAll(decryptValue = false) {
|
||||
function getAll(type = null) {
|
||||
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
|
||||
return fetch(`${config.apiUrl}/organization-constants?decryptValue=${decryptValue}`, requestOptions).then(
|
||||
handleResponse
|
||||
);
|
||||
const queryParams = type ? `?type=${type}` : '';
|
||||
return fetch(`${config.apiUrl}/organization-constants${queryParams}`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function create(name, value, environments) {
|
||||
function create(name, value, type, environments) {
|
||||
const body = {
|
||||
constant_name: name,
|
||||
value: value,
|
||||
type: type,
|
||||
environments: environments,
|
||||
};
|
||||
|
||||
|
|
@ -28,10 +30,10 @@ function create(name, value, environments) {
|
|||
return fetch(`${config.apiUrl}/organization-constants`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function update(id, value, envronmentId) {
|
||||
function update(id, value, environmentId) {
|
||||
const body = {
|
||||
value,
|
||||
environment_id: envronmentId,
|
||||
environment_id: environmentId,
|
||||
};
|
||||
|
||||
const requestOptions = { method: 'PATCH', headers: authHeader(), credentials: 'include', body: JSON.stringify(body) };
|
||||
|
|
@ -45,17 +47,26 @@ function remove(id, environmentId) {
|
|||
);
|
||||
}
|
||||
|
||||
function getConstantsFromEnvironment(environmentId, decryptValue = false) {
|
||||
function getConstantsFromEnvironment(environmentId, type = null) {
|
||||
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
|
||||
const queryParams = type ? `?type=${type}` : '';
|
||||
return fetch(
|
||||
`${config.apiUrl}/organization-constants/environment/${environmentId}?decryptValue=${decryptValue}`,
|
||||
`${config.apiUrl}/organization-constants/environment/${environmentId}${queryParams}`,
|
||||
requestOptions
|
||||
).then(handleResponse);
|
||||
}
|
||||
|
||||
function getConstantsFromPublicApp(slug, decryptValue = false) {
|
||||
const requestOptions = { method: 'GET' };
|
||||
return fetch(`${config.apiUrl}/organization-constants/${slug}?decryptValue=${decryptValue}`, requestOptions).then(
|
||||
handleResponse
|
||||
);
|
||||
function getAllSecrets() {
|
||||
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
|
||||
return fetch(`${config.apiUrl}/organization-constants/secrets`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function getConstantsFromApp(slug) {
|
||||
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
|
||||
return fetch(`${config.apiUrl}/organization-constants/${slug}`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
||||
function getConstantsFromPublicApp(slug) {
|
||||
const requestOptions = { method: 'GET', headers: authHeader(), credentials: 'include' };
|
||||
return fetch(`${config.apiUrl}/organization-constants/public/${slug}`, requestOptions).then(handleResponse);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,13 +7,15 @@ import { useEditorStore } from '@/_stores/editorStore';
|
|||
import { useQueryPanelStore } from '@/_stores/queryPanelStore';
|
||||
import update from 'immutability-helper';
|
||||
const { diff } = require('deep-object-diff');
|
||||
|
||||
import { useAppDataStore } from './appDataStore';
|
||||
import { authenticationService } from '@/_services';
|
||||
const initialState = {
|
||||
queries: {},
|
||||
components: {},
|
||||
globals: {
|
||||
theme: { name: 'light' },
|
||||
urlparams: null,
|
||||
currentUser: {},
|
||||
},
|
||||
errors: {},
|
||||
variables: {},
|
||||
|
|
@ -53,9 +55,25 @@ export const useCurrentStateStore = create(
|
|||
},
|
||||
setEditorReady: (isEditorReady) => set({ isEditorReady }),
|
||||
initializeCurrentStateOnVersionSwitch: () => {
|
||||
//fetch user for current app
|
||||
const currentSession = authenticationService.currentSessionValue;
|
||||
const currentUser = useAppDataStore.getState().currentUser;
|
||||
const userVars = {
|
||||
email: currentUser?.email,
|
||||
firstName: currentUser?.first_name,
|
||||
lastName: currentUser?.last_name,
|
||||
groups: currentSession?.group_permissions
|
||||
? ['all_users', ...currentSession.group_permissions.map((group) => group.name)]
|
||||
: ['all_users'],
|
||||
role: currentSession?.role?.name,
|
||||
};
|
||||
const newInitialState = {
|
||||
...initialState,
|
||||
constants: get().constants,
|
||||
globals: {
|
||||
...get().globals,
|
||||
currentUser: userVars,
|
||||
},
|
||||
};
|
||||
set({ ...newInitialState }, false, {
|
||||
type: 'INITIALIZE_CURRENT_STATE_ON_VERSION_SWITCH',
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { create, zustandDevTools } from './utils';
|
||||
import { getDefaultOptions } from './storeHelper';
|
||||
import { dataqueryService } from '@/_services';
|
||||
import { dataqueryService, orgEnvironmentConstantService } from '@/_services';
|
||||
// import debounce from 'lodash/debounce';
|
||||
import { useAppDataStore } from '@/_stores/appDataStore';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
|
@ -13,9 +13,12 @@ import { handleReferenceTransactions } from './handleReferenceTransactions';
|
|||
import { useAppVersionStore } from '@/_stores/appVersionStore';
|
||||
import { useEditorStore } from '@/_stores/editorStore';
|
||||
import { useQueryPanelStore } from '@/_stores/queryPanelStore';
|
||||
import { Constants } from '@/_helpers/utils';
|
||||
|
||||
const secretValue = '**********';
|
||||
const initialState = {
|
||||
dataQueries: [],
|
||||
secrets: [],
|
||||
sortBy: 'updated_at',
|
||||
sortOrder: 'desc',
|
||||
loadingDataQueries: true,
|
||||
|
|
@ -38,6 +41,7 @@ export const useDataQueriesStore = create(
|
|||
fetchDataQueries: async (appVersionId, selectFirstQuery = false, runQueriesOnAppLoad = false, ref) => {
|
||||
get().loadingDataQueries && set({ loadingDataQueries: true });
|
||||
const data = await dataqueryService.getAll(appVersionId);
|
||||
const { constants } = await orgEnvironmentConstantService.getAllSecrets();
|
||||
|
||||
const diff = _.differenceWith(data.data_queries, get().dataQueries, _.isEqual);
|
||||
const referencesManager = useResolveStore.getState().referenceMapper;
|
||||
|
|
@ -57,6 +61,10 @@ export const useDataQueriesStore = create(
|
|||
set((state) => ({
|
||||
dataQueries: sortByAttribute(data.data_queries, state.sortBy, state.sortOrder),
|
||||
loadingDataQueries: false,
|
||||
secrets: constants.reduce((acc, constant) => {
|
||||
acc[constant.name] = secretValue;
|
||||
return acc;
|
||||
}, {}),
|
||||
}));
|
||||
|
||||
// Compute query state to be added in the current state
|
||||
|
|
|
|||
|
|
@ -300,4 +300,37 @@
|
|||
border-radius: 6px;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.add-plugin-card{
|
||||
border: 1px dashed var(--border-default, #CCD1D5);
|
||||
background-color: var(--interactive-weak);
|
||||
|
||||
&:hover{
|
||||
border: 1px dashed var(--border-strong, #CCD1D5);
|
||||
background-color: var(--interactive-default);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.add-plugin-card-title{
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: var(--text-default);
|
||||
}
|
||||
|
||||
|
||||
.dark-theme{
|
||||
.add-plugin-card{
|
||||
border: 1px dashed var(--border-default, #CCD1D5);
|
||||
background-color: var(--interactive-weak);
|
||||
|
||||
&:hover{
|
||||
border: 1px dashed var(--border-strong, #CCD1D5);
|
||||
background-color: var(--interactive-default);
|
||||
}
|
||||
}
|
||||
}
|
||||
159
frontend/src/_styles/groups-permissions.scss
Normal file
159
frontend/src/_styles/groups-permissions.scss
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
@import "./typography.scss";
|
||||
@import "./designtheme.scss";
|
||||
|
||||
|
||||
|
||||
.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 */
|
||||
min-height: calc(100vh - 300px - 100px);
|
||||
display: flex;
|
||||
align-items: center; /* Center items vertically */
|
||||
justify-content: center; /* Center items horizontally */
|
||||
text-align: center;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
||||
.menu{
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center; /* Center items vertically */
|
||||
justify-content: center; /* Center items horizontally */
|
||||
}
|
||||
|
||||
.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: 190px;
|
||||
}
|
||||
.add-icon {
|
||||
width: 135px;
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.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 */
|
||||
max-height: calc(100vh - 370px - 100px);
|
||||
min-height: calc(100vh - 370px - 100px);
|
||||
}
|
||||
|
||||
.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 */
|
||||
max-height: calc(100vh - 300px - 100px);
|
||||
min-height: calc(100vh - 300px - 100px);
|
||||
}
|
||||
|
||||
|
||||
.side-button-cont {
|
||||
justify-self: flex-end;
|
||||
display: flex;
|
||||
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 */
|
||||
justify-content: flex-end;
|
||||
// margin-bottom: 20px;
|
||||
// margin-top: auto;
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.tj-text-xsm{
|
||||
color: var(--slate11);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.delete-icon-cont {
|
||||
margin-left: 200px;
|
||||
|
||||
.icon-class{
|
||||
border: none !important;
|
||||
background-color: none !important;
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -461,9 +461,7 @@
|
|||
line-break: anywhere;
|
||||
text-align: center;
|
||||
|
||||
span {
|
||||
color: #466BF2;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.signup-password-wrap,
|
||||
|
|
|
|||
|
|
@ -2293,6 +2293,24 @@ progress {
|
|||
transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out
|
||||
}
|
||||
|
||||
|
||||
.form-control-org-login {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: .4375rem .75rem;
|
||||
font-size: .875rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.4285714;
|
||||
background: var(--base);
|
||||
background-clip: padding-box;
|
||||
border: 1px solid var(--slate7);
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
border-radius: 4px;
|
||||
transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion:reduce) {
|
||||
.form-control {
|
||||
transition: none
|
||||
|
|
@ -3546,7 +3564,8 @@ fieldset:disabled .btn {
|
|||
}
|
||||
|
||||
.dropdown-menu.show {
|
||||
display: block
|
||||
display: block;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.dropdown-header {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -6028,7 +6029,7 @@ div#driver-page-overlay {
|
|||
|
||||
&:focus-visible {
|
||||
background: var(--slate1);
|
||||
border: 1px solid var(--slate8);
|
||||
border: 1px solid var(--indigo9);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
|
|
@ -6044,6 +6045,10 @@ div#driver-page-overlay {
|
|||
color: var(--slate9);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
&::placeholder {
|
||||
color: var(--slate9);
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -8961,25 +8966,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 {
|
||||
|
|
@ -8987,6 +9005,10 @@ tbody {
|
|||
border-bottom: none !important;
|
||||
padding: 0 !important;
|
||||
width: 282px;
|
||||
|
||||
&[data-name="role-header"] {
|
||||
width:120px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -9000,17 +9022,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 {
|
||||
|
|
@ -9477,15 +9503,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;
|
||||
|
||||
|
|
@ -9655,14 +9687,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 {
|
||||
|
|
@ -9672,7 +9705,7 @@ tbody {
|
|||
p {
|
||||
padding: 8px 12px;
|
||||
gap: 10px;
|
||||
width: 206px;
|
||||
width: 230px;
|
||||
height: 36px;
|
||||
font-weight: 500;
|
||||
color: var(--slate11) !important;
|
||||
|
|
@ -9719,13 +9752,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 {
|
||||
|
|
@ -9796,6 +9835,9 @@ tbody {
|
|||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.manage-group-users-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
|
@ -9806,13 +9848,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;
|
||||
|
|
@ -9822,8 +9864,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;
|
||||
|
||||
|
|
@ -9890,7 +9941,7 @@ tbody {
|
|||
border-bottom: 1px solid var(--slate5);
|
||||
width: 612px;
|
||||
height: 36px;
|
||||
padding: 8px 12px;
|
||||
padding: 8px 12px 8px 2px;
|
||||
align-items: center;
|
||||
|
||||
|
||||
|
|
@ -9900,6 +9951,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,
|
||||
|
|
@ -9925,12 +9985,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);
|
||||
}
|
||||
|
||||
|
|
@ -10837,6 +10897,12 @@ tbody {
|
|||
border-bottom-left-radius: 6px;
|
||||
border-bottom-right-radius: 6px;
|
||||
|
||||
|
||||
.user-detail{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.user-filter-search {
|
||||
|
|
@ -11514,7 +11580,10 @@ tbody {
|
|||
}
|
||||
|
||||
.constant-table-card {
|
||||
min-height: 370px;
|
||||
min-height: 420px;
|
||||
padding: 16px;
|
||||
padding-top: 0px;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
|
|
@ -12821,7 +12890,7 @@ tbody {
|
|||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
max-width: 185px;
|
||||
max-width: 210px;
|
||||
}
|
||||
|
||||
.group-chip {
|
||||
|
|
@ -13207,6 +13276,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;
|
||||
|
|
@ -13220,7 +13310,6 @@ tbody {
|
|||
|
||||
.widget-version-identifier {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: 0px;
|
||||
border-radius: 0px 8px 0px 8px;
|
||||
height: 16px;
|
||||
|
|
@ -13333,8 +13422,22 @@ div.ds-svg-container svg {
|
|||
}
|
||||
}
|
||||
|
||||
.tabs-component{
|
||||
.tab-pane{
|
||||
top: initial !important;
|
||||
}
|
||||
}
|
||||
.mb-0 {
|
||||
margin-bottom: 0px !important;
|
||||
}
|
||||
|
||||
.tabs-component{
|
||||
.tab-pane{
|
||||
top: initial !important;
|
||||
}
|
||||
}
|
||||
|
||||
.user-not-found-svg{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--slate3);
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -181,6 +181,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.tj-ms-usergroup{
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.tj-ms-count {
|
||||
border-radius: 2px;
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ function FolderList({
|
|||
variant="tertiary"
|
||||
onMouseEnter={handleMouseEnterInside}
|
||||
onMouseLeave={handleMouseLeaveInside}
|
||||
data-cy="groups-list-option-button"
|
||||
></ButtonSolid>
|
||||
</div>
|
||||
<Overlay
|
||||
|
|
|
|||
|
|
@ -11,8 +11,9 @@ export default ({ getter, options = [['', '']], optionchanged, isRenderedAsQuery
|
|||
}
|
||||
|
||||
function removeKeyValuePair(index) {
|
||||
options.splice(index, 1);
|
||||
optionchanged(getter, options);
|
||||
const newOptions = [...options];
|
||||
newOptions.splice(index, 1);
|
||||
optionchanged(getter, newOptions);
|
||||
}
|
||||
|
||||
function keyValuePairValueChanged(value, keyIndex, index) {
|
||||
|
|
|
|||
|
|
@ -1,78 +1,94 @@
|
|||
input.form-control,
|
||||
textarea,
|
||||
.input-control {
|
||||
gap: 16px !important;
|
||||
background: var(--base) !important;
|
||||
border: 1px solid var(--slate7) !important;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 4px !important;
|
||||
color: var(--slate12) !important;
|
||||
transition: none;
|
||||
height: 35px;
|
||||
padding-left: 0.4375rem;
|
||||
padding-right: 0.4375rem;
|
||||
padding-top: 0.75rem;
|
||||
padding-bottom: 0.75rem;
|
||||
overflow-x: 'auto';
|
||||
white-space: 'nowrap';
|
||||
textarea,
|
||||
.input-control {
|
||||
gap: 16px !important;
|
||||
background: var(--base) !important;
|
||||
border: 1px solid var(--slate7) !important;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 4px !important;
|
||||
color: var(--slate12) !important;
|
||||
transition: none;
|
||||
height: 35px;
|
||||
padding-left: 0.4375rem;
|
||||
padding-right: 0.4375rem;
|
||||
padding-top: 0.75rem;
|
||||
padding-bottom: 0.75rem;
|
||||
overflow-x: 'auto';
|
||||
white-space: 'nowrap';
|
||||
|
||||
|
||||
&:hover {
|
||||
background: var(--slate1) !important;
|
||||
border: 1px solid var(--slate8) !important;
|
||||
-webkit-box-shadow: none !important;
|
||||
box-shadow: none !important;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
background: var(--indigo2) !important;
|
||||
border: 1px solid var(--indigo9) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
&.input-error-border {
|
||||
border-color: #DB4324 !important;
|
||||
}
|
||||
|
||||
&:-webkit-autofill {
|
||||
box-shadow: 0 0 0 1000px var(--base) inset !important;
|
||||
-webkit-text-fill-color: var(--slate12) !important;
|
||||
|
||||
&:hover {
|
||||
background: var(--slate1) !important;
|
||||
border: 1px solid var(--slate8) !important;
|
||||
-webkit-box-shadow: none !important;
|
||||
box-shadow: none !important;
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 1000px var(--slate1) inset !important;
|
||||
-webkit-text-fill-color: var(--slate12) !important;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
background: var(--indigo2) !important;
|
||||
border: 1px solid var(--indigo9) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
&.input-error-border {
|
||||
border-color: #DB4324 !important;
|
||||
}
|
||||
|
||||
&:-webkit-autofill {
|
||||
box-shadow: 0 0 0 1000px var(--base) inset !important;
|
||||
box-shadow: 0 0 0 1000px var(--indigo2) inset !important;
|
||||
-webkit-text-fill-color: var(--slate12) !important;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 0 1000px var(--slate1) inset !important;
|
||||
-webkit-text-fill-color: var(--slate12) !important;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
box-shadow: 0 0 0 1000px var(--indigo2) inset !important;
|
||||
-webkit-text-fill-color: var(--slate12) !important;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.empty-key-value {
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
width: 625px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 10px;
|
||||
color: #687076;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
border: 1px dashed #E6E8EB;
|
||||
}
|
||||
}
|
||||
|
||||
.trash {
|
||||
height: 32px;
|
||||
display: flex;
|
||||
justify-content: 'center';
|
||||
align-items: 'center';
|
||||
}
|
||||
|
||||
.empty-key-value {
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
width: 625px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 10px;
|
||||
color: #687076;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
border: 1px dashed #E6E8EB;
|
||||
}
|
||||
|
||||
.trash {
|
||||
height: 32px;
|
||||
display: flex;
|
||||
justify-content: 'center';
|
||||
align-items: 'center';
|
||||
}
|
||||
|
||||
.empty-version {
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
width: auto;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 10px;
|
||||
color: #687076;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
border: 1px dashed #E6E8EB;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue