{
const rowActions = (row): IAction[] => [
{
'aria-label': 'deleteRole',
- title: t('access-management.roles.delete-action'),
+ title: t('common.actions.delete'),
onClick: () => {
setRoleToDelete(row.name);
}
@@ -120,7 +123,13 @@ const RoleTableDisplay = () => {
)}
- {row.name}
+
+ {row.name}
+
{
@@ -155,7 +164,7 @@ const RoleTableDisplay = () => {
{roles.length == 0
- ? t('access-management.roles.no-roles-body')
+ ? t('access-management.roles.no-roles-body', { brandname: brandname })
: t('access-management.roles.no-filtered-roles-body')}
@@ -164,6 +173,7 @@ const RoleTableDisplay = () => {
);
};
+
const createRoleButtonHelper = (isEmptyPage?: boolean) => {
const emptyPageButtonProp = { style: { marginTop: global_spacer_xl.value } };
const normalPageButtonProps = { style: { marginLeft: global_spacer_sm.value } };
@@ -180,34 +190,6 @@ const RoleTableDisplay = () => {
);
};
- if (loading) {
- return (
-
-
- {t('access-management.roles.loading-roles')}>}
- icon={}
- headingLevel="h4"
- />
-
-
- );
- }
-
- if (error) {
- return (
-
-
- {t('access-management.roles.loading-roles-error')}>}
- icon={}
- headingLevel="h4"
- />
-
-
- );
- }
-
const displayContent = () => {
if (roles.length === 0) {
return (
@@ -222,6 +204,15 @@ const RoleTableDisplay = () => {
);
}
+
+ if (loading) {
+ return ;
+ }
+
+ if (error) {
+ return ;
+ }
+
return (
diff --git a/src/app/Caches/Create/CreateCacheWizard.tsx b/src/app/Caches/Create/CreateCacheWizard.tsx
index b6adbaa0f..b9ca53c79 100644
--- a/src/app/Caches/Create/CreateCacheWizard.tsx
+++ b/src/app/Caches/Create/CreateCacheWizard.tsx
@@ -245,7 +245,7 @@ const CreateCacheWizard = (props: { cacheManager: CacheManager; create: boolean
onClick={(event) => getPreviousStep(event, activeStep, onBack)}
data-cy="wizardBackButton"
>
- {t('caches.create.back-button-label')}
+ {t('common.actions.back')}
);
diff --git a/src/app/Caches/DetailCache.tsx b/src/app/Caches/DetailCache.tsx
index 7b41abb19..53911879a 100644
--- a/src/app/Caches/DetailCache.tsx
+++ b/src/app/Caches/DetailCache.tsx
@@ -189,7 +189,7 @@ const DetailCache = (props: { cacheName: string }) => {
search: location.search
}}
>
-
+
diff --git a/src/app/Common/DataContainerBreadcrumb.tsx b/src/app/Common/DataContainerBreadcrumb.tsx
index 35eb1b5c2..ddf824773 100644
--- a/src/app/Common/DataContainerBreadcrumb.tsx
+++ b/src/app/Common/DataContainerBreadcrumb.tsx
@@ -1,8 +1,15 @@
import { Breadcrumb, BreadcrumbItem } from '@patternfly/react-core';
import { Link } from 'react-router-dom';
import * as React from 'react';
+import { useTranslation } from 'react-i18next';
-const DataContainerBreadcrumb = (props: { currentPage: string; cacheName?: string }) => {
+const DataContainerBreadcrumb = (props: {
+ currentPage: string;
+ parentPage?: string;
+ cacheName?: string;
+ label?: string;
+}) => {
+ const { t } = useTranslation();
const addCacheName = () => {
if (props.cacheName) {
return (
@@ -20,17 +27,17 @@ const DataContainerBreadcrumb = (props: { currentPage: string; cacheName?: strin
}
return;
};
-
+ const label = props.label ? props.label : 'cache-managers.title';
return (
-
+
- Data container
+ {t(label)}
{addCacheName()}
diff --git a/src/app/Common/SelectMultiWithChips.tsx b/src/app/Common/SelectMultiWithChips.tsx
index 23f5df8c3..b92c739a8 100644
--- a/src/app/Common/SelectMultiWithChips.tsx
+++ b/src/app/Common/SelectMultiWithChips.tsx
@@ -24,6 +24,7 @@ const SelectMultiWithChips = (props: {
selection: string[];
create?: boolean;
closeOnSelect?: boolean;
+ readonly?: string[];
}) => {
const [isOpen, setIsOpen] = useState(false);
const [inputValue, setInputValue] = useState('');
@@ -192,6 +193,8 @@ const SelectMultiWithChips = (props: {
ev.stopPropagation();
onSelect(selection);
}}
+ isReadOnly={props.readonly && props.readonly.includes(selection)}
+ disabled={props.readonly && props.readonly.includes(selection)}
>
{selection}
@@ -243,6 +246,7 @@ const SelectMultiWithChips = (props: {
data-cy={`option-typeahead-${option.id !== undefined ? option.id : option.value}`}
{...option}
ref={null}
+ description={option.description}
/>
))}
diff --git a/src/app/Common/TableErrorState.tsx b/src/app/Common/TableErrorState.tsx
index 9f1855a14..ad8bfff7e 100644
--- a/src/app/Common/TableErrorState.tsx
+++ b/src/app/Common/TableErrorState.tsx
@@ -2,9 +2,9 @@ import {
Bullseye,
EmptyState,
EmptyStateBody,
+ EmptyStateHeader,
EmptyStateIcon,
- EmptyStateVariant,
- EmptyStateHeader
+ EmptyStateVariant
} from '@patternfly/react-core';
import { ExclamationCircleIcon } from '@patternfly/react-icons';
import { global_danger_color_200 } from '@patternfly/react-tokens';
@@ -12,17 +12,15 @@ import * as React from 'react';
import { useTranslation } from 'react-i18next';
const TableErrorState = (props: { error: string; detail?: string }) => {
+ const { t } = useTranslation();
const displayErrorBody = () => {
if (props.detail) {
return props.detail as string;
} else {
- return 'There was an error retrieving data.\n' + ' Check your connection and try again.';
+ return t('common.loading-error-message');
}
};
- const { t } = useTranslation();
- const brandname = t('brandname.brandname');
-
return (
diff --git a/src/app/Common/TableLoadingState.tsx b/src/app/Common/TableLoadingState.tsx
new file mode 100644
index 000000000..490fad976
--- /dev/null
+++ b/src/app/Common/TableLoadingState.tsx
@@ -0,0 +1,20 @@
+import {
+ Bullseye,
+ EmptyState,
+ EmptyStateHeader,
+ EmptyStateIcon,
+ EmptyStateVariant,
+ Spinner
+} from '@patternfly/react-core';
+import * as React from 'react';
+
+const TableLoadingState = (props: { message: string }) => {
+ return (
+
+
+ } headingLevel="h4" />
+
+
+ );
+};
+export { TableLoadingState };
diff --git a/src/app/assets/languages/en.json b/src/app/assets/languages/en.json
index 457654cfe..89cb5c0ed 100644
--- a/src/app/assets/languages/en.json
+++ b/src/app/assets/languages/en.json
@@ -13,8 +13,12 @@
"actions": "Actions",
"refresh": "Refresh",
"back": "Back",
- "cancel": "Cancel"
- }
+ "cancel": "Cancel",
+ "save": "Save",
+ "delete": "Delete",
+ "remove": "Remove"
+ },
+ "loading-error-message": "There was an error retrieving data. Check your connection and try again."
},
"layout": {
"console-name": "Server Management Console",
@@ -230,7 +234,6 @@
"templates-placeholder": "Select a cache template",
"create-button-label": "Create",
"next-button-label": "Next",
- "back-button-label": "Back",
"cancel-button-label": "Cancel",
"download-button-label": "Download",
"getting-started": {
@@ -545,6 +548,7 @@
"actions": {
"action-manage-indexes": "Manage indexes",
"action-manage-backups": "Manage backups",
+ "refresh": "Refresh",
"back": "Back"
},
"entries": {
@@ -885,8 +889,8 @@
"no-filtered-roles-body": "No roles match your search criteria.",
"roles-hint-link": "Learn more in the {{brandname}} documentation",
"loading-roles": "Loading roles",
+ "loading-roles-error": "Unexpected error loading roles",
"create-button": "Create role",
- "delete-action": "Delete",
"no-roles-status": "No roles",
"permission-all": "All the permissions",
"permission-admin": "Allows performing \"administrative\" operations",
@@ -904,8 +908,6 @@
"permission-none": "No permissions",
"modal-create-title": "Create role",
"modal-create-description": "Create a new role.",
- "modal-save-action": "Save",
- "modal-cancel-button": "Cancel",
"modal-role-name": "Name",
"modal-role-description": "Description",
"modal-permissions": "Permissions",
@@ -918,6 +920,29 @@
"modal-delete-description-2": "You can always recreate the role.",
"delete-success": "Role {{name}} has been deleted",
"delete-error": "Unexpected error deleting the role {{name}}"
+ },
+ "role": {
+ "breadcrumb": "Detail of role {{roleName}}",
+ "actions": "Actions",
+ "tab-general": "General settings",
+ "tab-permissions": "Permissions",
+ "tab-caches": "Accessible caches",
+ "loading": "Loading role {{roleName}}",
+ "error": "An error occurred while retrieving the role {{roleName}}",
+ "implicit-warning": "The general settings for predefined roles are not editable.",
+ "permission-name": "Permission name",
+ "permission-description": "Description",
+ "add-permission-button": "Add permission",
+ "permissions-search-placeholder": "Filter by name",
+ "no-permissions-found": "No permission found",
+ "no-filtered-permissions-body": "No permission match your search criteria.",
+ "permissions-implicit-warning": "The permissions for predefined roles are not editable.",
+ "update-success": "Role {{name}} has been updated",
+ "update-error": "Unexpected error updating the role {{name}}",
+ "modal-add-permission-title": "Add permissions",
+ "modal-remove-permission-title": "Remove permission {{name}}",
+ "modal-remove-permission-description-1": "Are you sure you want to remove this permission from role {{roleName}}? After removing, the role's access will be changed.",
+ "modal-remove-permission-description-2": "You can always add the role permission again."
}
}
}
diff --git a/src/app/routes.tsx b/src/app/routes.tsx
index 0de59723f..ad388ed7c 100644
--- a/src/app/routes.tsx
+++ b/src/app/routes.tsx
@@ -19,6 +19,7 @@ import { ConsoleServices } from '@services/ConsoleServices';
import { ConsoleACL } from '@services/securityService';
import { NotAuthorized } from '@app/NotAuthorized/NotAuthorized';
import { NotFound } from '@app/NotFound/NotFound';
+import { RoleDetail } from '@app/AccessManagement/RoleDetail';
let routeFocusTimer: number;
@@ -176,7 +177,8 @@ const routes: IAppRoute[] = [
path: '/access-management',
title: 'Access Management',
menu: true,
- admin: true
+ admin: true,
+ subRoutes: ['role']
},
{
component: ConnectedClients,
@@ -186,6 +188,15 @@ const routes: IAppRoute[] = [
title: 'Connected Clients',
menu: true,
admin: true
+ },
+ {
+ component: RoleDetail,
+ exact: true,
+ label: 'Role detail',
+ path: '/access-management/role/:roleName',
+ title: 'Role detail',
+ menu: false,
+ admin: true
}
];
diff --git a/src/app/services/rolesHook.ts b/src/app/services/rolesHook.ts
index 6330c5be9..c78ce3a76 100644
--- a/src/app/services/rolesHook.ts
+++ b/src/app/services/rolesHook.ts
@@ -68,6 +68,59 @@ export function useFetchAvailableRoles() {
};
}
+export function useUpdateRole(roleName: string, roleDescription: string, permissions: string[], call: () => void) {
+ const { addAlert } = useApiAlert();
+ const { t } = useTranslation();
+
+ const onUpdateRole = () => {
+ ConsoleServices.security()
+ .updateRole(
+ roleName,
+ roleDescription,
+ permissions,
+ t('access-management.role.update-success', { name: roleName }),
+ t('access-management.role.update-error', { name: roleName })
+ )
+ .then((actionResponse) => {
+ addAlert(actionResponse);
+ })
+ .finally(() => call());
+ };
+ return {
+ onUpdateRole
+ };
+}
+
+export function useRemovePermission(roleName: string, permission: string, permissions: string[], call: () => void) {
+ const { addAlert } = useApiAlert();
+ const { t } = useTranslation();
+
+ const onRemovePermission = () => {
+ const perms: string[] = [];
+ permissions.forEach((perm) => {
+ if (perm !== permission) {
+ perms.push(perm);
+ }
+ });
+
+ ConsoleServices.security()
+ .updateRole(
+ roleName,
+ '',
+ perms,
+ t('access-management.role.update-success', { name: roleName }),
+ t('access-management.role.update-error', { name: roleName })
+ )
+ .then((actionResponse) => {
+ addAlert(actionResponse);
+ })
+ .finally(() => call());
+ };
+ return {
+ onRemovePermission
+ };
+}
+
export function useCreateRole(roleName: string, roleDescription: string, permissions: string[], call: () => void) {
const { addAlert } = useApiAlert();
@@ -104,6 +157,34 @@ export function useDeleteRole(roleName: string, call: () => void) {
};
}
+export function useDescribeRole(roleName: string) {
+ const [role, setRole] = useState();
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState('');
+
+ useEffect(() => {
+ if (loading) {
+ ConsoleServices.security()
+ .describeRole(roleName)
+ .then((either) => {
+ if (either.isRight()) {
+ setRole(either.value);
+ } else {
+ setError(either.value.message);
+ }
+ })
+ .finally(() => setLoading(false));
+ }
+ }, [loading]);
+
+ return {
+ role,
+ loading,
+ error,
+ setLoading
+ };
+}
+
export function useFlushCache(call: () => void) {
const { addAlert } = useApiAlert();
const { t } = useTranslation();
diff --git a/src/services/displayUtils.ts b/src/services/displayUtils.ts
index 44b8664b1..27f293b1f 100644
--- a/src/services/displayUtils.ts
+++ b/src/services/displayUtils.ts
@@ -470,7 +470,9 @@ class DisplayUtils {
// Try parse and stringify
try {
return JSON.stringify(JSON.parse(content), null, 2);
- } catch (err) {}
+ } catch (err) {
+ /* empty */
+ }
}
return content as string;
diff --git a/src/services/infinispanRefData.ts b/src/services/infinispanRefData.ts
index f98c95662..abf19f445 100644
--- a/src/services/infinispanRefData.ts
+++ b/src/services/infinispanRefData.ts
@@ -247,3 +247,24 @@ export enum RoleFilterOption {
cacheManagerPermissions = 'Cache Manager Permissions',
cachePermissions = 'Cache Permissions'
}
+
+export enum Permission {
+ ALL = 'all'
+}
+
+export const PERMISSIONS_MAP = new Map([
+ ['ALL', 'access-management.roles.permission-all'],
+ ['ADMIN', 'access-management.roles.permission-admin'],
+ ['ALL_READ', 'access-management.roles.permission-all-read'],
+ ['READ', 'access-management.roles.permission-read'],
+ ['BULK_READ', 'access-management.roles.permission-bulk-read'],
+ ['ALL_WRITE', 'access-management.roles.permission-all-write'],
+ ['WRITE', 'access-management.roles.permission-write'],
+ ['BULK_WRITE', 'access-management.roles.permission-bulk-write'],
+ ['MONITOR', 'access-management.roles.permission-monitor'],
+ ['CREATE', 'access-management.roles.permission-create'],
+ ['EXEC', 'access-management.roles.permission-exec'],
+ ['LISTEN', 'access-management.roles.permission-listen'],
+ ['LIFECYCLE', 'access-management.roles.permission-lifecycle'],
+ ['NONE', 'access-management.roles.permission-none']
+]);
diff --git a/src/services/securityService.ts b/src/services/securityService.ts
index 8b517a895..8d3b86871 100644
--- a/src/services/securityService.ts
+++ b/src/services/securityService.ts
@@ -178,6 +178,23 @@ export class SecurityService {
);
}
+ /**
+ * Retrieve security roles
+ *
+ */
+ public async describeRole(roleName: string): Promise> {
+ return this.fetchCaller.get(
+ this.endpoint + '/permissions/' + roleName,
+ (data) =>
+ {
+ name: roleName,
+ description: data.description,
+ permissions: data.permissions,
+ implicit: data.implicit
+ }
+ );
+ }
+
/**
* Created a new role
* @param roleName
@@ -210,6 +227,30 @@ export class SecurityService {
});
}
+ /**
+ * updates an existing role
+ *
+ * @param roleName
+ * @param roleDescription
+ * @param permissions
+ * @param messageOk
+ * @param messageError
+ */
+ public async updateRole(
+ roleName: string,
+ roleDescription: string,
+ permissions: string[],
+ messageOk: string,
+ messageError: string
+ ) {
+ return this.fetchCaller.put({
+ url: this.endpoint + '/permissions/' + roleName + '?' + permissions.map((p) => 'permission=' + p).join('&'),
+ successMessage: messageOk,
+ errorMessage: messageError,
+ body: roleDescription
+ });
+ }
+
/**
* Flush security cache
* @param messageOk
|