Update Users, Admins and tags page with new roles and policy (#7050)

* Update Users, Admins and tags page with new roles and policy

* Fix unit tests

* Minor change
This commit is contained in:
Sachin Chaurasiya 2022-08-30 21:11:46 +05:30 committed by GitHub
parent 7cd5579c84
commit c366d39737
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 192 additions and 159 deletions

View File

@ -11,55 +11,28 @@
* limitations under the License.
*/
import { Menu, MenuProps } from 'antd';
import { Empty, Menu, MenuProps } from 'antd';
import { ItemType } from 'antd/lib/menu/hooks/useItems';
import { AxiosError } from 'axios';
import { camelCase } from 'lodash';
import React, { useEffect, useMemo, useState } from 'react';
import React, { useMemo } from 'react';
import { useHistory, useParams } from 'react-router-dom';
import { GLOBAL_SETTING_PERMISSION_RESOURCES } from '../../constants/globalSettings.constants';
import {
getGlobalSettingMenuItem,
getGlobalSettingsMenuWithPermission,
MenuList,
} from '../../utils/GlobalSettingsUtils';
import { getSettingPath } from '../../utils/RouterUtils';
import { showErrorToast } from '../../utils/ToastUtils';
import Loader from '../Loader/Loader';
import { usePermissionProvider } from '../PermissionProvider/PermissionProvider';
import {
ResourceEntity,
UIPermission,
} from '../PermissionProvider/PermissionProvider.interface';
const GlobalSettingLeftPanel = () => {
const history = useHistory();
const { tab, settingCategory } = useParams<{ [key: string]: string }>();
const [settingResourcePermission, setSettingResourcePermission] =
useState<UIPermission>({} as UIPermission);
const [isLoading, setIsLoading] = useState<boolean>(true);
const { getResourcePermission } = usePermissionProvider();
const fetchResourcesPermission = async (resource: ResourceEntity) => {
setIsLoading(true);
try {
const response = await getResourcePermission(resource);
setSettingResourcePermission((prev) => ({
...prev,
[resource]: response,
}));
} catch (error) {
showErrorToast(error as AxiosError);
} finally {
setIsLoading(false);
}
};
const { permissions } = usePermissionProvider();
const menuItems: ItemType[] = useMemo(
() =>
getGlobalSettingsMenuWithPermission(settingResourcePermission).reduce(
getGlobalSettingsMenuWithPermission(permissions).reduce(
(acc: ItemType[], curr: MenuList) => {
const menuItem = getGlobalSettingMenuItem(
curr.category,
@ -77,7 +50,7 @@ const GlobalSettingLeftPanel = () => {
},
[] as ItemType[]
),
[setSettingResourcePermission]
[permissions]
);
const onClick: MenuProps['onClick'] = (e) => {
@ -86,29 +59,16 @@ const GlobalSettingLeftPanel = () => {
history.push(getSettingPath(category, option));
};
useEffect(() => {
// TODO: This will make number of API calls, need to think of better solution
GLOBAL_SETTING_PERMISSION_RESOURCES.forEach((resource) => {
fetchResourcesPermission(resource);
});
}, []);
if (isLoading) {
return <Loader />;
}
return (
<>
{menuItems.length ? (
<Menu
className="global-setting-left-panel"
items={menuItems}
mode="inline"
selectedKeys={[`${settingCategory}.${tab}`]}
onClick={onClick}
/>
) : null}
</>
return menuItems.length ? (
<Menu
className="global-setting-left-panel"
items={menuItems}
mode="inline"
selectedKeys={[`${settingCategory}.${tab}`]}
onClick={onClick}
/>
) : (
<Empty className="tw-mt-8" />
);
};

View File

@ -121,25 +121,12 @@ const PermissionProvider: FC<PermissionProviderProps> = ({ children }) => {
[resource]: operationPermission,
}));
/**
* Store updated resource permission
*/
setPermissions((prev) => ({
...prev,
[resource]: operationPermission,
}));
return operationPermission;
}
};
useEffect(() => {
/**
* Only fetch permission if user is logged In
*/
if (currentUser && currentUser.id) {
fetchLoggedInUserPermissions();
}
fetchLoggedInUserPermissions();
}, [currentUser]);
return (

View File

@ -19,17 +19,22 @@ import React, { FC, useMemo, useState } from 'react';
import { Link, useHistory } from 'react-router-dom';
import { updateUser } from '../../axiosAPIs/userAPI';
import { getUserPath, PAGE_SIZE, ROUTES } from '../../constants/constants';
import { NO_PERMISSION_FOR_ACTION } from '../../constants/HelperTextUtil';
import { CreateUser } from '../../generated/api/teams/createUser';
import { Operation } from '../../generated/entity/policies/policy';
import { EntityReference, User } from '../../generated/entity/teams/user';
import { Paging } from '../../generated/type/paging';
import jsonData from '../../jsons/en';
import { getEntityName, getTeamsText } from '../../utils/CommonUtils';
import { checkPermission } from '../../utils/PermissionsUtils';
import SVGIcons, { Icons } from '../../utils/SvgUtils';
import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils';
import DeleteWidgetModal from '../common/DeleteWidget/DeleteWidgetModal';
import NextPrevious from '../common/next-previous/NextPrevious';
import Searchbar from '../common/searchbar/Searchbar';
import Loader from '../Loader/Loader';
import { usePermissionProvider } from '../PermissionProvider/PermissionProvider';
import { ResourceEntity } from '../PermissionProvider/PermissionProvider.interface';
import './usersList.less';
interface UserListV1Props {
@ -57,12 +62,23 @@ const UserListV1: FC<UserListV1Props> = ({
onPagingChange,
afterDeleteAction,
}) => {
const { permissions } = usePermissionProvider();
const history = useHistory();
const [selectedUser, setSelectedUser] = useState<User>();
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [showReactiveModal, setShowReactiveModal] = useState(false);
const showRestore = showDeletedUser && !isDataLoading;
const createPermission = useMemo(
() => checkPermission(Operation.Create, ResourceEntity.USER, permissions),
[permissions]
);
const deletePermission = useMemo(
() => checkPermission(Operation.Delete, ResourceEntity.USER, permissions),
[permissions]
);
const handleAddNewUser = () => {
history.push(ROUTES.CREATE_USER);
};
@ -156,8 +172,11 @@ const UserListV1: FC<UserListV1Props> = ({
/>
</Tooltip>
)}
<Tooltip placement="bottom" title="Delete">
<Tooltip
placement="bottom"
title={deletePermission ? 'Delete' : NO_PERMISSION_FOR_ACTION}>
<Button
disabled={!deletePermission}
icon={
<SVGIcons
alt="Delete"
@ -199,9 +218,15 @@ const UserListV1: FC<UserListV1Props> = ({
/>
<span className="tw-ml-2">Deleted Users</span>
</span>
<Button type="primary" onClick={handleAddNewUser}>
Add User
</Button>
<Tooltip
title={createPermission ? 'Add User' : NO_PERMISSION_FOR_ACTION}>
<Button
disabled={!createPermission}
type="primary"
onClick={handleAddNewUser}>
Add User
</Button>
</Tooltip>
</Space>
</Col>

View File

@ -28,6 +28,10 @@ type Props = {
permission?: Operation;
};
/**
* @deprecated
* TODO: Remove this component once we have new permission structure everywhere
*/
const NonAdminAction = ({
children,
className = '',

View File

@ -190,6 +190,41 @@ const mockCategory = [
},
];
jest.mock('../../components/PermissionProvider/PermissionProvider', () => ({
usePermissionProvider: jest.fn().mockReturnValue({
getEntityPermission: jest.fn().mockReturnValue({
Create: true,
Delete: true,
ViewAll: true,
EditAll: true,
EditDescription: true,
EditDisplayName: true,
}),
permissions: {
tagCategory: {
Create: true,
Delete: true,
ViewAll: true,
EditAll: true,
EditDescription: true,
EditDisplayName: true,
},
tag: {
Create: true,
Delete: true,
ViewAll: true,
EditAll: true,
EditDescription: true,
EditDisplayName: true,
},
},
}),
}));
jest.mock('../../utils/PermissionsUtils', () => ({
checkPermission: jest.fn().mockReturnValue(true),
}));
jest.mock('../../axiosAPIs/tagAPI', () => ({
createTag: jest.fn(),
createTagCategory: jest.fn(),

View File

@ -12,14 +12,12 @@
*/
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Card } from 'antd';
import { Button, Card, Tooltip } from 'antd';
import { AxiosError } from 'axios';
import classNames from 'classnames';
import { isEmpty, isUndefined, toLower } from 'lodash';
import { FormErrorData, LoadingState } from 'Models';
import React, { useEffect, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { Link, useHistory, useParams } from 'react-router-dom';
import { useAuthContext } from '../../authentication/auth-provider/AuthProvider';
import {
createTag,
createTagCategory,
@ -29,11 +27,9 @@ import {
updateTag,
updateTagCategory,
} from '../../axiosAPIs/tagAPI';
import { Button } from '../../components/buttons/Button/Button';
import Description from '../../components/common/description/Description';
import Ellipses from '../../components/common/Ellipses/Ellipses';
import ErrorPlaceHolder from '../../components/common/error-with-placeholder/ErrorPlaceHolder';
import NonAdminAction from '../../components/common/non-admin-action/NonAdminAction';
import RichTextEditorPreviewer from '../../components/common/rich-text-editor/RichTextEditorPreviewer';
import PageContainerV1 from '../../components/containers/PageContainerV1';
import PageLayout, {
@ -43,7 +39,12 @@ import Loader from '../../components/Loader/Loader';
import ConfirmationModal from '../../components/Modals/ConfirmationModal/ConfirmationModal';
import FormModal from '../../components/Modals/FormModal';
import { ModalWithMarkdownEditor } from '../../components/Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor';
import { TITLE_FOR_NON_ADMIN_ACTION } from '../../constants/constants';
import { usePermissionProvider } from '../../components/PermissionProvider/PermissionProvider';
import {
OperationPermission,
ResourceEntity,
} from '../../components/PermissionProvider/PermissionProvider.interface';
import { NO_PERMISSION_FOR_ACTION } from '../../constants/HelperTextUtil';
import { delimiterRegex } from '../../constants/regex.constants';
import {
CreateTagCategory,
@ -52,7 +53,6 @@ import {
import { Operation } from '../../generated/entity/policies/accessControl/rule';
import { TagCategory, TagClass } from '../../generated/entity/tags/tagCategory';
import { EntityReference } from '../../generated/type/entityReference';
import { useAuth } from '../../hooks/authHooks';
import jsonData from '../../jsons/en';
import {
getActiveCatClass,
@ -61,6 +61,7 @@ import {
isEven,
isUrlFriendlyName,
} from '../../utils/CommonUtils';
import { checkPermission } from '../../utils/PermissionsUtils';
import {
getExplorePathWithInitFilters,
getTagPath,
@ -85,10 +86,9 @@ type DeleteTagsType = {
};
const TagsPage = () => {
const { getEntityPermission, permissions } = usePermissionProvider();
const history = useHistory();
const { tagCategoryName } = useParams<Record<string, string>>();
const { isAdminUser } = useAuth();
const { isAuthDisabled } = useAuthContext();
const [categories, setCategoreis] = useState<Array<TagCategory>>([]);
const [currentCategory, setCurrentCategory] = useState<TagCategory>();
const [isEditCategory, setIsEditCategory] = useState<boolean>(false);
@ -104,6 +104,35 @@ const TagsPage = () => {
data: undefined,
state: false,
});
const [categoryPermissions, setCategoryPermissions] =
useState<OperationPermission>({} as OperationPermission);
const createCategoryPermission = useMemo(
() =>
checkPermission(
Operation.Create,
ResourceEntity.TAG_CATEGORY,
permissions
),
[permissions]
);
const createTagPermission = useMemo(
() => checkPermission(Operation.Create, ResourceEntity.TAG, permissions),
[permissions]
);
const fetchCurrentCategoryPermission = async () => {
try {
const response = await getEntityPermission(
ResourceEntity.TAG_CATEGORY,
currentCategory?.id as string
);
setCategoryPermissions(response);
} catch (error) {
showErrorToast(error as AxiosError);
}
};
const fetchCategories = () => {
setIsLoading(true);
@ -405,6 +434,7 @@ const TagsPage = () => {
setCurrentCategory(categories[0]);
if (currentCategory) {
setCurrentCategory(currentCategory);
fetchCurrentCategoryPermission();
}
}, [categories, currentCategory]);
@ -426,24 +456,25 @@ const TagsPage = () => {
style={{ fontSize: '14px' }}>
Tag Categories
</span>
<NonAdminAction
position="bottom"
title={TITLE_FOR_NON_ADMIN_ACTION}>
<Tooltip
title={
createCategoryPermission
? 'Add Category'
: NO_PERMISSION_FOR_ACTION
}>
<Button
className={classNames('tw-px-2 ', {
'tw-opacity-40': !isAdminUser && !isAuthDisabled,
})}
className="tw-px-2 "
data-testid="add-category"
disabled={!createCategoryPermission}
size="small"
theme="primary"
variant="contained"
type="primary"
onClick={() => {
setIsAddingCategory((prevState) => !prevState);
setErrorDataCategory(undefined);
}}>
<FontAwesomeIcon icon="plus" />
</Button>
</NonAdminAction>
</Tooltip>
</div>
}>
<>
@ -505,44 +536,39 @@ const TagsPage = () => {
{currentCategory.displayName ?? currentCategory.name}
</div>
<div>
<NonAdminAction
position="bottom"
title={TITLE_FOR_NON_ADMIN_ACTION}>
<Tooltip
title={
createTagPermission || categoryPermissions.EditAll
? 'Add Tag'
: NO_PERMISSION_FOR_ACTION
}>
<Button
className={classNames('tw-h-8 tw-rounded tw-mb-3', {
'tw-opacity-40': !isAdminUser && !isAuthDisabled,
})}
className="tw-h-8 tw-rounded tw-mb-3"
data-testid="add-new-tag-button"
disabled={
!createTagPermission && !categoryPermissions.EditAll
}
size="small"
theme="primary"
variant="contained"
type="primary"
onClick={() => {
setIsAddingTag((prevState) => !prevState);
setErrorDataTag(undefined);
}}>
Add new tag
</Button>
</NonAdminAction>
<NonAdminAction
position="bottom"
title={TITLE_FOR_NON_ADMIN_ACTION}>
<Button
className={classNames(
'tw-h-8 tw-rounded tw-mb-3 tw-ml-2',
{
'tw-opacity-40': !isAdminUser && !isAuthDisabled,
}
)}
data-testid="delete-tag-category-button"
size="small"
theme="primary"
variant="outlined"
onClick={() => {
deleteTagHandler();
}}>
Delete category
</Button>
</NonAdminAction>
</Tooltip>
<Button
className="tw-h-8 tw-rounded tw-mb-3 tw-ml-2"
data-testid="delete-tag-category-button"
disabled={!categoryPermissions.Delete}
size="small"
type="primary"
onClick={() => {
deleteTagHandler();
}}>
Delete category
</Button>
</div>
</div>
)}
@ -554,6 +580,7 @@ const TagsPage = () => {
entityName={
currentCategory?.displayName ?? currentCategory?.name
}
hasEditAccess={categoryPermissions.EditDescription}
isEdit={isEditCategory}
onCancel={() => setIsEditCategory(false)}
onDescriptionEdit={() => setIsEditCategory(true)}
@ -605,10 +632,8 @@ const TagsPage = () => {
</span>
)}
</div>
<NonAdminAction
permission={Operation.EditDescription}
position="left"
title={TITLE_FOR_NON_ADMIN_ACTION}>
{categoryPermissions.EditDescription && (
<button
className="tw-self-start tw-w-8 tw-h-auto tw-opacity-0 tw-ml-1 group-hover:tw-opacity-100 focus:tw-outline-none"
onClick={() => {
@ -623,7 +648,7 @@ const TagsPage = () => {
width="16px"
/>
</button>
</NonAdminAction>
)}
</div>
<div className="tw-mt-1" data-testid="usage">
<span className="tw-text-grey-muted tw-mr-1">
@ -649,40 +674,37 @@ const TagsPage = () => {
</td>
<td className="tableBody-cell">
<div className="tw-text-center">
<NonAdminAction
position="bottom"
title={TITLE_FOR_NON_ADMIN_ACTION}>
<button
className="link-text"
data-testid="delete-tag"
onClick={() =>
setDeleteTags({
data: {
id: tag.id as string,
name: tag.name,
categoryName: currentCategory.name,
isCategory: false,
status: 'waiting',
},
state: true,
})
}>
{deleteTags.data?.id === tag.id ? (
deleteTags.data?.status === 'success' ? (
<FontAwesomeIcon icon="check" />
) : (
<Loader size="small" type="default" />
)
<button
className="link-text"
data-testid="delete-tag"
disabled={!categoryPermissions.EditAll}
onClick={() =>
setDeleteTags({
data: {
id: tag.id as string,
name: tag.name,
categoryName: currentCategory.name,
isCategory: false,
status: 'waiting',
},
state: true,
})
}>
{deleteTags.data?.id === tag.id ? (
deleteTags.data?.status === 'success' ? (
<FontAwesomeIcon icon="check" />
) : (
<SVGIcons
alt="delete"
icon="icon-delete"
title="Delete"
width="16px"
/>
)}
</button>
</NonAdminAction>
<Loader size="small" type="default" />
)
) : (
<SVGIcons
alt="delete"
icon="icon-delete"
title="Delete"
width="16px"
/>
)}
</button>
</div>
</td>
</tr>