Update Glossary with new roles and policy (#7067)

* Update Glossary with new roles and policy

* Fix unit tests

* Fix tests

* Fix tests
This commit is contained in:
Sachin Chaurasiya 2022-08-31 01:46:33 +05:30 committed by GitHub
parent b6309b6274
commit 503873a0a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 535 additions and 338 deletions

View File

@ -22,6 +22,44 @@ import React from 'react';
import { mockedAssetData, mockedGlossaries } from '../../mocks/Glossary.mock';
import GlossaryV1 from './GlossaryV1.component';
jest.mock('../PermissionProvider/PermissionProvider', () => ({
usePermissionProvider: jest.fn().mockReturnValue({
getEntityPermission: jest.fn().mockReturnValue({
Create: true,
Delete: true,
ViewAll: true,
EditAll: true,
EditDescription: true,
EditDisplayName: true,
EditCustomFields: true,
}),
permissions: {
glossaryTerm: {
Create: true,
Delete: true,
ViewAll: true,
EditAll: true,
EditDescription: true,
EditDisplayName: true,
EditCustomFields: true,
},
glossary: {
Create: true,
Delete: true,
ViewAll: true,
EditAll: true,
EditDescription: true,
EditDisplayName: true,
EditCustomFields: true,
},
},
}),
}));
jest.mock('../../utils/PermissionsUtils', () => ({
checkPermission: jest.fn().mockReturnValue(true),
}));
jest.mock('react-router-dom', () => ({
useHistory: jest.fn(),
useParams: jest.fn().mockReturnValue({
@ -29,18 +67,6 @@ jest.mock('react-router-dom', () => ({
}),
}));
jest.mock('../../authentication/auth-provider/AuthProvider', () => {
return {
useAuthContext: jest.fn(() => ({
isAuthDisabled: false,
isAuthenticated: true,
isProtectedRoute: jest.fn().mockReturnValue(true),
isTourRoute: jest.fn().mockReturnValue(false),
onLogoutHandler: jest.fn(),
})),
};
});
jest.mock('../../components/GlossaryDetails/GlossaryDetails.component', () => {
return jest.fn().mockReturnValue(<>Glossary-Details component</>);
});
@ -77,6 +103,12 @@ jest.mock('antd', () => ({
})}
</div>
)),
Button: jest
.fn()
.mockImplementation(({ children }) => <button>{children}</button>),
Tooltip: jest
.fn()
.mockImplementation(({ children }) => <span>{children}</span>),
}));
jest.mock('../common/title-breadcrumb/title-breadcrumb.component', () =>

View File

@ -12,28 +12,38 @@
*/
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Col, Dropdown, Input, Menu, Row, Space, Typography } from 'antd';
import classNames from 'classnames';
import {
Button as ButtonAntd,
Col,
Dropdown,
Input,
Menu,
Row,
Space,
Tooltip,
Typography,
} from 'antd';
import { AxiosError } from 'axios';
import { cloneDeep, isEmpty } from 'lodash';
import { GlossaryTermAssets, LoadingState } from 'Models';
import RcTree from 'rc-tree';
import { DataNode, EventDataNode } from 'rc-tree/lib/interface';
import React, { Fragment, useEffect, useRef, useState } from 'react';
import { useAuthContext } from '../../authentication/auth-provider/AuthProvider';
import React, { Fragment, useEffect, useMemo, useRef, useState } from 'react';
import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants';
import { TITLE_FOR_NON_ADMIN_ACTION } from '../../constants/constants';
import { NO_PERMISSION_FOR_ACTION } from '../../constants/HelperTextUtil';
import { Glossary } from '../../generated/entity/data/glossary';
import { GlossaryTerm } from '../../generated/entity/data/glossaryTerm';
import { useAuth } from '../../hooks/authHooks';
import { Operation } from '../../generated/entity/policies/policy';
import { useAfterMount } from '../../hooks/useAfterMount';
import { ModifiedGlossaryData } from '../../pages/GlossaryPage/GlossaryPageV1.component';
import { getEntityDeleteMessage, getEntityName } from '../../utils/CommonUtils';
import { generateTreeData } from '../../utils/GlossaryUtils';
import { checkPermission } from '../../utils/PermissionsUtils';
import { getGlossaryPath } from '../../utils/RouterUtils';
import SVGIcons, { Icons } from '../../utils/SvgUtils';
import { showErrorToast } from '../../utils/ToastUtils';
import { Button } from '../buttons/Button/Button';
import ErrorPlaceHolder from '../common/error-with-placeholder/ErrorPlaceHolder';
import NonAdminAction from '../common/non-admin-action/NonAdminAction';
import Searchbar from '../common/searchbar/Searchbar';
import TitleBreadcrumb from '../common/title-breadcrumb/title-breadcrumb.component';
import { TitleBreadcrumbProps } from '../common/title-breadcrumb/title-breadcrumb.interface';
@ -43,6 +53,11 @@ import GlossaryDetails from '../GlossaryDetails/GlossaryDetails.component';
import GlossaryTermsV1 from '../GlossaryTerms/GlossaryTermsV1.component';
import Loader from '../Loader/Loader';
import EntityDeleteModal from '../Modals/EntityDeleteModal/EntityDeleteModal';
import { usePermissionProvider } from '../PermissionProvider/PermissionProvider';
import {
OperationPermission,
ResourceEntity,
} from '../PermissionProvider/PermissionProvider.interface';
import './GlossaryV1.style.less';
const { Title } = Typography;
@ -50,7 +65,6 @@ type Props = {
assetData: GlossaryTermAssets;
deleteStatus: LoadingState;
isSearchResultEmpty: boolean;
isHasAccess: boolean;
glossaryList: ModifiedGlossaryData[];
selectedKey: string;
expandedKey: string[];
@ -80,7 +94,6 @@ const GlossaryV1 = ({
assetData,
deleteStatus = 'initial',
isSearchResultEmpty,
isHasAccess,
glossaryList,
selectedKey,
expandedKey,
@ -104,8 +117,7 @@ const GlossaryV1 = ({
onRelatedTermClick,
currentPage,
}: Props) => {
const { isAdminUser } = useAuth();
const { isAuthDisabled } = useAuthContext();
const { getEntityPermission, permissions } = usePermissionProvider();
const treeRef = useRef<RcTree<DataNode>>(null);
const [treeData, setTreeData] = useState<DataNode[]>([]);
const [breadcrumb, setBreadcrumb] = useState<
@ -124,6 +136,59 @@ const GlossaryV1 = ({
);
const [isNameEditing, setIsNameEditing] = useState(false);
const [displayName, setDisplayName] = useState<string>();
const [glossaryPermission, setGlossaryPermission] =
useState<OperationPermission>({} as OperationPermission);
const [glossaryTermPermission, setGlossaryTermPermission] =
useState<OperationPermission>({} as OperationPermission);
const fetchGlossaryPermission = async () => {
try {
const response = await getEntityPermission(
ResourceEntity.GLOSSARY,
selectedData?.id as string
);
setGlossaryPermission(response);
} catch (error) {
showErrorToast(error as AxiosError);
}
};
const fetchGlossaryTermPermission = async () => {
try {
const response = await getEntityPermission(
ResourceEntity.GLOSSARY_TERM,
selectedData?.id as string
);
setGlossaryTermPermission(response);
} catch (error) {
showErrorToast(error as AxiosError);
}
};
const createGlossaryPermission = useMemo(
() =>
checkPermission(Operation.Create, ResourceEntity.GLOSSARY, permissions),
[permissions]
);
const createGlossaryTermPermission = useMemo(
() =>
checkPermission(
Operation.Create,
ResourceEntity.GLOSSARY_TERM,
permissions
),
[permissions]
);
const editDisplayNamePermission = useMemo(() => {
return isGlossaryActive
? glossaryPermission.EditDisplayName
: glossaryTermPermission.EditDisplayName;
}, [glossaryPermission, glossaryTermPermission]);
/**
* To create breadcrumb from the fqn
* @param fqn fqn of glossary or glossary term
@ -269,16 +334,20 @@ const GlossaryV1 = ({
typingInterval={500}
onSearch={handleSearchText}
/>
<NonAdminAction
position="bottom"
title={TITLE_FOR_NON_ADMIN_ACTION}>
<Tooltip
title={
createGlossaryPermission
? 'Add Glossary'
: NO_PERMISSION_FOR_ACTION
}>
<button
className="tw--mt-1 tw-w-full tw-flex-center tw-gap-2 tw-py-1 tw-text-primary tw-border tw-rounded-md"
disabled={!createGlossaryPermission}
onClick={handleAddGlossaryClick}>
<SVGIcons alt="plus" icon={Icons.ICON_PLUS_PRIMERY} />{' '}
<span>Add Glossary</span>
</button>
</NonAdminAction>
</Tooltip>
</div>
{isSearchResultEmpty ? (
<p className="tw-text-grey-muted tw-text-center">
@ -311,6 +380,10 @@ const GlossaryV1 = ({
useEffect(() => {
setDisplayName(selectedData?.displayName);
if (selectedData) {
fetchGlossaryPermission();
fetchGlossaryTermPermission();
}
}, [selectedData]);
return glossaryList.length ? (
@ -327,32 +400,45 @@ const GlossaryV1 = ({
/>
</div>
<div className="tw-relative tw-mr-2 tw--mt-2" id="add-term-button">
<NonAdminAction position="bottom" title={TITLE_FOR_NON_ADMIN_ACTION}>
<Button
className={classNames('tw-h-8 tw-rounded tw-mb-1 tw-mr-2', {
'tw-opacity-40': isHasAccess,
})}
<Tooltip
title={
createGlossaryTermPermission
? 'Add Term'
: NO_PERMISSION_FOR_ACTION
}>
<ButtonAntd
className="tw-h-8 tw-rounded tw-mb-1 tw-mr-2"
data-testid="add-new-tag-button"
size="small"
theme="primary"
variant="contained"
disabled={!createGlossaryTermPermission}
type="primary"
onClick={handleAddGlossaryTermClick}>
Add term
</Button>
</NonAdminAction>
<NonAdminAction position="bottom" title={TITLE_FOR_NON_ADMIN_ACTION}>
<Dropdown
align={{ targetOffset: [-12, 0] }}
overlay={manageButtonContent()}
overlayStyle={{ width: '350px' }}
placement="bottomRight"
trigger={['click']}
visible={showActions}
onVisibleChange={setShowActions}>
</ButtonAntd>
</Tooltip>
<Dropdown
align={{ targetOffset: [-12, 0] }}
disabled={
isGlossaryActive
? !glossaryPermission.Delete
: !glossaryTermPermission.Delete
}
overlay={manageButtonContent()}
overlayStyle={{ width: '350px' }}
placement="bottomRight"
trigger={['click']}
visible={showActions}
onVisibleChange={setShowActions}>
<Tooltip
title={
glossaryPermission.Delete
? 'Manage Glossary'
: NO_PERMISSION_FOR_ACTION
}>
<Button
className="tw-rounded tw-justify-center tw-w-8 tw-h-8 glossary-manage-button tw-mb-1 tw-flex"
data-testid="manage-button"
disabled={isHasAccess}
disabled={!glossaryPermission.Delete}
size="small"
theme="primary"
variant="outlined"
@ -361,8 +447,8 @@ const GlossaryV1 = ({
<FontAwesomeIcon icon="ellipsis-vertical" />
</span>
</Button>
</Dropdown>
</NonAdminAction>
</Tooltip>
</Dropdown>
</div>
</div>
{isChildLoading ? (
@ -411,14 +497,24 @@ const GlossaryV1 = ({
) : (
<Space className="display-name">
<Title level={4}>{getEntityName(selectedData)}</Title>
<button onClick={() => setIsNameEditing(true)}>
<SVGIcons
alt="icon-tag"
className="tw-mx-1"
icon={Icons.EDIT}
width="16"
/>
</button>
<Tooltip
title={
editDisplayNamePermission
? 'Edit Displayname'
: NO_PERMISSION_FOR_ACTION
}>
<ButtonAntd
disabled={!editDisplayNamePermission}
type="text"
onClick={() => setIsNameEditing(true)}>
<SVGIcons
alt="icon-tag"
className="tw-mx-1"
icon={Icons.EDIT}
width="16"
/>
</ButtonAntd>
</Tooltip>
</Space>
)}
</div>
@ -427,7 +523,7 @@ const GlossaryV1 = ({
<GlossaryDetails
glossary={selectedData as Glossary}
handleUserRedirection={handleUserRedirection}
isHasAccess={isHasAccess}
permissions={glossaryPermission}
updateGlossary={updateGlossary}
/>
) : (
@ -437,7 +533,7 @@ const GlossaryV1 = ({
glossaryTerm={selectedData as GlossaryTerm}
handleGlossaryTermUpdate={handleGlossaryTermUpdate}
handleUserRedirection={handleUserRedirection}
isHasAccess={isHasAccess}
permissions={glossaryTermPermission}
onAssetPaginate={onAssetPaginate}
onRelatedTermClick={onRelatedTermClick}
/>
@ -460,19 +556,16 @@ const GlossaryV1 = ({
<ErrorPlaceHolder>
<p className="tw-text-center">No glossaries found</p>
<p className="tw-text-center">
<NonAdminAction position="bottom" title={TITLE_FOR_NON_ADMIN_ACTION}>
<Button
className={classNames('tw-h-8 tw-rounded tw-my-3', {
'tw-opacity-40': !isAdminUser && !isAuthDisabled,
})}
data-testid="add-webhook-button"
size="small"
theme="primary"
variant="contained"
onClick={handleAddGlossaryClick}>
Add New Glossary
</Button>
</NonAdminAction>
<Button
className="tw-h-8 tw-rounded tw-my-3"
data-testid="add-webhook-button"
disabled={!createGlossaryPermission}
size="small"
theme="primary"
variant="contained"
onClick={handleAddGlossaryClick}>
Add New Glossary
</Button>
</p>
</ErrorPlaceHolder>
</PageLayout>

View File

@ -12,29 +12,20 @@
*/
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Card as AntdCard } from 'antd';
import { Button as ButtonAntd, Card as AntdCard, Tooltip } from 'antd';
import { AxiosError } from 'axios';
import classNames from 'classnames';
import { cloneDeep, debounce, includes, isEqual } from 'lodash';
import { EntityTags, FormattedUsersData } from 'Models';
import React, { useCallback, useEffect, useState } from 'react';
import { useAuthContext } from '../../authentication/auth-provider/AuthProvider';
import { WILD_CARD_CHAR } from '../../constants/char.constants';
import {
TITLE_FOR_NON_ADMIN_ACTION,
TITLE_FOR_NON_OWNER_ACTION,
TITLE_FOR_UPDATE_OWNER,
} from '../../constants/constants';
import { EntityType } from '../../enums/entity.enum';
import { NO_PERMISSION_FOR_ACTION } from '../../constants/HelperTextUtil';
import { Glossary } from '../../generated/entity/data/glossary';
import { Operation } from '../../generated/entity/policies/policy';
import { EntityReference } from '../../generated/type/entityReference';
import { LabelType, State, TagSource } from '../../generated/type/tagLabel';
import { useAuth } from '../../hooks/authHooks';
import jsonData from '../../jsons/en';
import { getEntityName, hasEditAccess } from '../../utils/CommonUtils';
import { getEntityName } from '../../utils/CommonUtils';
import { getOwnerList } from '../../utils/ManageUtils';
import { hasPemission } from '../../utils/PermissionsUtils';
import SVGIcons, { Icons } from '../../utils/SvgUtils';
import {
getTagCategories,
@ -47,27 +38,24 @@ import {
searchFormattedUsersAndTeams,
suggestFormattedUsersAndTeams,
} from '../../utils/UserDataUtils';
import { Button } from '../buttons/Button/Button';
import Card from '../common/Card/Card';
import DescriptionV1 from '../common/description/DescriptionV1';
import NonAdminAction from '../common/non-admin-action/NonAdminAction';
import ProfilePicture from '../common/ProfilePicture/ProfilePicture';
import DropDownList from '../dropdown/DropDownList';
import ReviewerModal from '../Modals/ReviewerModal/ReviewerModal.component';
import { OperationPermission } from '../PermissionProvider/PermissionProvider.interface';
import TagsContainer from '../tags-container/tags-container';
import TagsViewer from '../tags-viewer/tags-viewer';
import Tags from '../tags/tags';
type props = {
isHasAccess: boolean;
permissions: OperationPermission;
glossary: Glossary;
updateGlossary: (value: Glossary) => void;
handleUserRedirection?: (name: string) => void;
};
const GlossaryDetails = ({ isHasAccess, glossary, updateGlossary }: props) => {
const { userPermissions } = useAuth();
const { isAuthDisabled } = useAuthContext();
const GlossaryDetails = ({ permissions, glossary, updateGlossary }: props) => {
const [isDescriptionEditable, setIsDescriptionEditable] = useState(false);
const [isTagEditable, setIsTagEditable] = useState<boolean>(false);
const [tagList, setTagList] = useState<Array<string>>([]);
@ -264,13 +252,6 @@ const GlossaryDetails = ({ isHasAccess, glossary, updateGlossary }: props) => {
setListVisible(false);
};
const isOwner = () => {
return hasEditAccess(
glossary?.owner?.type || '',
glossary?.owner?.id || ''
);
};
const handleTagContainerClick = () => {
if (!isTagEditable) {
fetchTags();
@ -293,44 +274,39 @@ const GlossaryDetails = ({ isHasAccess, glossary, updateGlossary }: props) => {
const AddReviewerButton = () => {
return (
<NonAdminAction position="bottom" title={TITLE_FOR_NON_ADMIN_ACTION}>
<button
className="tw-text-primary"
<Tooltip
placement="topRight"
title={permissions.EditAll ? 'Add Reviewer' : NO_PERMISSION_FOR_ACTION}>
<ButtonAntd
className="tw-p-0"
data-testid="add-new-reviewer"
disabled={isHasAccess}
disabled={!permissions.EditAll}
type="text"
onClick={() => setShowRevieweModal(true)}>
<SVGIcons alt="edit" icon={Icons.EDIT} title="Edit" width="16px" />
</button>
</NonAdminAction>
</ButtonAntd>
</Tooltip>
);
};
const ownerAction = () => {
return (
<span className="tw-relative">
<NonAdminAction
html={<p>{TITLE_FOR_UPDATE_OWNER}</p>}
isOwner={isOwner()}
permission={Operation.EditOwner}
position="left">
<Button
<Tooltip
placement="topRight"
title={
permissions.EditOwner ? 'Update Owner' : NO_PERMISSION_FOR_ACTION
}>
<ButtonAntd
className="tw-p-0"
data-testid="owner-dropdown"
disabled={
!hasPemission(
Operation.EditOwner,
EntityType.GLOSSARY,
userPermissions
) &&
!isAuthDisabled &&
!hasEditAccess
}
size="custom"
theme="primary"
variant="text"
disabled={!permissions.EditOwner}
size="small"
type="text"
onClick={handleSelectOwnerDropdown}>
<SVGIcons alt="edit" icon={Icons.EDIT} title="Edit" width="16px" />
</Button>
</NonAdminAction>
</ButtonAntd>
</Tooltip>
{listVisible && (
<DropDownList
horzPosRight
@ -379,20 +355,24 @@ const GlossaryDetails = ({ isHasAccess, glossary, updateGlossary }: props) => {
<span>{getEntityName(term)}</span>
</div>
<span>
<NonAdminAction
html={<p>{TITLE_FOR_NON_OWNER_ACTION}</p>}
isOwner={isOwner()}
position="bottom">
<span
className={classNames('tw-h-8 tw-rounded tw-mb-3')}
data-testid="remove"
onClick={() => handleRemoveReviewer(term.id)}>
<FontAwesomeIcon
className="tw-cursor-pointer"
icon="remove"
/>
</span>
</NonAdminAction>
<Tooltip
title={
permissions.EditAll
? 'Remove Reviewer'
: NO_PERMISSION_FOR_ACTION
}>
<ButtonAntd disabled={!permissions.EditAll} type="text">
<span
className={classNames('tw-h-8 tw-rounded tw-mb-3')}
data-testid="remove"
onClick={() => handleRemoveReviewer(term.id)}>
<FontAwesomeIcon
className="tw-cursor-pointer"
icon="remove"
/>
</span>
</ButtonAntd>
</Tooltip>
</span>
</div>
))}
@ -426,52 +406,51 @@ const GlossaryDetails = ({ isHasAccess, glossary, updateGlossary }: props) => {
)}
</>
)}
<NonAdminAction
isOwner={Boolean(glossary.owner)}
permission={Operation.EditTags}
position="bottom"
title={TITLE_FOR_NON_OWNER_ACTION}
trigger="click">
<div className="tw-inline-block" onClick={handleTagContainerClick}>
<TagsContainer
buttonContainerClass="tw--mt-0"
containerClass="tw-flex tw-items-center tw-gap-2"
dropDownHorzPosRight={false}
editable={isTagEditable}
isLoading={isTagLoading}
selectedTags={getSelectedTags()}
showTags={false}
size="small"
tagList={getTagOptionsFromFQN(tagList)}
type="label"
onCancel={() => {
handleTagSelection();
}}
onSelectionChange={(tags) => {
handleTagSelection(tags);
}}>
{glossary?.tags && glossary?.tags.length ? (
<button className=" tw-ml-1 focus:tw-outline-none">
<SVGIcons
alt="edit"
icon="icon-edit"
title="Edit"
width="16px"
/>
</button>
) : (
<span>
<Tags
className="tw-text-primary"
startWith="+ "
tag="Add tag"
type="label"
/>
</span>
)}
</TagsContainer>
</div>
</NonAdminAction>
<div className="tw-inline-block" onClick={handleTagContainerClick}>
<TagsContainer
buttonContainerClass="tw--mt-0"
containerClass="tw-flex tw-items-center tw-gap-2"
dropDownHorzPosRight={false}
editable={isTagEditable}
isLoading={isTagLoading}
selectedTags={getSelectedTags()}
showTags={false}
size="small"
tagList={getTagOptionsFromFQN(tagList)}
type="label"
onCancel={() => {
handleTagSelection();
}}
onSelectionChange={(tags) => {
handleTagSelection(tags);
}}>
{glossary?.tags && glossary?.tags.length ? (
<button
className=" tw-ml-1 focus:tw-outline-none"
disabled={!permissions.EditTags}>
<SVGIcons
alt="edit"
icon="icon-edit"
title="Edit"
width="16px"
/>
</button>
) : (
<ButtonAntd
className="tw-p-0"
disabled={!permissions.EditTags}
type="text">
<Tags
className="tw-text-primary"
startWith="+ "
tag="Add tag"
type="label"
/>
</ButtonAntd>
)}
</TagsContainer>
</div>
</div>
<div className="tw-flex tw-gap-3">
<div className="tw-w-9/12">
@ -481,6 +460,7 @@ const GlossaryDetails = ({ isHasAccess, glossary, updateGlossary }: props) => {
removeBlur
description={glossary?.description}
entityName={glossary?.displayName ?? glossary?.name}
hasEditAccess={permissions.EditDescription}
isEdit={isDescriptionEditable}
onCancel={onCancel}
onDescriptionEdit={onDescriptionEdit}

View File

@ -14,6 +14,7 @@
import { findByText, getByTestId, render } from '@testing-library/react';
import React from 'react';
import { mockedGlossaries } from '../../mocks/Glossary.mock';
import { OperationPermission } from '../PermissionProvider/PermissionProvider.interface';
import GlossaryDetails from './GlossaryDetails.component';
jest.mock('react-router-dom', () => ({
@ -61,7 +62,15 @@ jest.mock('../common/ProfilePicture/ProfilePicture', () => {
const mockProps = {
glossary: mockedGlossaries[0],
isHasAccess: true,
permissions: {
Create: true,
Delete: true,
ViewAll: true,
EditAll: true,
EditDescription: true,
EditDisplayName: true,
EditCustomFields: true,
} as OperationPermission,
updateGlossary: jest.fn(),
};

View File

@ -17,8 +17,47 @@ import {
mockedAssetData,
mockedGlossaryTerms,
} from '../../mocks/Glossary.mock';
import { OperationPermission } from '../PermissionProvider/PermissionProvider.interface';
import GlossaryTerms from './GlossaryTermsV1.component';
jest.mock('../PermissionProvider/PermissionProvider', () => ({
usePermissionProvider: jest.fn().mockReturnValue({
getEntityPermission: jest.fn().mockReturnValue({
Create: true,
Delete: true,
ViewAll: true,
EditAll: true,
EditDescription: true,
EditDisplayName: true,
EditCustomFields: true,
}),
permissions: {
glossaryTerm: {
Create: true,
Delete: true,
ViewAll: true,
EditAll: true,
EditDescription: true,
EditDisplayName: true,
EditCustomFields: true,
},
glossary: {
Create: true,
Delete: true,
ViewAll: true,
EditAll: true,
EditDescription: true,
EditDisplayName: true,
EditCustomFields: true,
},
},
}),
}));
jest.mock('../../utils/PermissionsUtils', () => ({
checkPermission: jest.fn().mockReturnValue(true),
}));
jest.mock('react-router-dom', () => ({
useHistory: jest.fn(),
useParams: jest.fn().mockReturnValue({
@ -26,18 +65,6 @@ jest.mock('react-router-dom', () => ({
}),
}));
jest.mock('../../authentication/auth-provider/AuthProvider', () => {
return {
useAuthContext: jest.fn(() => ({
isAuthDisabled: false,
isAuthenticated: true,
isProtectedRoute: jest.fn().mockReturnValue(true),
isTourRoute: jest.fn().mockReturnValue(false),
onLogoutHandler: jest.fn(),
})),
};
});
jest.mock('../../components/tags-container/tags-container', () => {
return jest.fn().mockReturnValue(<>Tags-container component</>);
});
@ -99,10 +126,22 @@ jest.mock('antd', () => ({
)),
}));
jest.mock('./SummaryDetail', () =>
jest.fn().mockReturnValue(<div>SummaryDetails</div>)
);
const mockProps = {
assetData: mockedAssetData,
currentPage: 1,
isHasAccess: true,
permissions: {
Create: true,
Delete: true,
ViewAll: true,
EditAll: true,
EditDescription: true,
EditDisplayName: true,
EditCustomFields: true,
} as OperationPermission,
glossaryTerm: mockedGlossaryTerms[0],
handleGlossaryTermUpdate: jest.fn(),
onAssetPaginate: jest.fn(),

View File

@ -20,19 +20,12 @@ import {
Input,
Row,
Space,
Tooltip,
Typography,
} from 'antd';
import { AxiosError } from 'axios';
import classNames from 'classnames';
import {
cloneDeep,
includes,
isEmpty,
isEqual,
isString,
isUndefined,
kebabCase,
} from 'lodash';
import { cloneDeep, includes, isEmpty, isEqual } from 'lodash';
import {
EntityTags,
FormattedGlossaryTermData,
@ -40,10 +33,7 @@ import {
GlossaryTermAssets,
} from 'Models';
import React, { Fragment, useEffect, useState } from 'react';
import {
TITLE_FOR_NON_ADMIN_ACTION,
TITLE_FOR_NON_OWNER_ACTION,
} from '../../constants/constants';
import { NO_PERMISSION_FOR_ACTION } from '../../constants/HelperTextUtil';
import {
GlossaryTerm,
TermReference,
@ -59,21 +49,22 @@ import {
} from '../../utils/TagsUtils';
import { showErrorToast } from '../../utils/ToastUtils';
import DescriptionV1 from '../common/description/DescriptionV1';
import NonAdminAction from '../common/non-admin-action/NonAdminAction';
import ProfilePicture from '../common/ProfilePicture/ProfilePicture';
import TabsPane from '../common/TabsPane/TabsPane';
import GlossaryReferenceModal from '../Modals/GlossaryReferenceModal/GlossaryReferenceModal';
import RelatedTermsModal from '../Modals/RelatedTermsModal/RelatedTermsModal';
import ReviewerModal from '../Modals/ReviewerModal/ReviewerModal.component';
import { OperationPermission } from '../PermissionProvider/PermissionProvider.interface';
import TagsContainer from '../tags-container/tags-container';
import TagsViewer from '../tags-viewer/tags-viewer';
import Tags from '../tags/tags';
import SummaryDetail from './SummaryDetail';
import AssetsTabs from './tabs/AssetsTabs.component';
const { Text } = Typography;
type Props = {
assetData: GlossaryTermAssets;
isHasAccess: boolean;
permissions: OperationPermission;
glossaryTerm: GlossaryTerm;
currentPage: number;
handleGlossaryTermUpdate: (data: GlossaryTerm) => void;
@ -82,21 +73,14 @@ type Props = {
handleUserRedirection?: (name: string) => void;
};
type SummaryDetailsProps = {
title: string;
children: React.ReactElement;
setShow?: (value: React.SetStateAction<boolean>) => void;
data?: FormattedGlossaryTermData[] | TermReference[] | string;
};
const GlossaryTermsV1 = ({
assetData,
isHasAccess,
glossaryTerm,
handleGlossaryTermUpdate,
onAssetPaginate,
onRelatedTermClick,
currentPage,
permissions,
}: Props) => {
const [isTagEditable, setIsTagEditable] = useState<boolean>(false);
const [tagList, setTagList] = useState<Array<string>>([]);
@ -301,7 +285,7 @@ const GlossaryTermsV1 = ({
const handleValidation = (
event: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
) => {
if (isHasAccess) {
if (permissions.EditAll) {
return;
}
const value = event.target.value;
@ -344,28 +328,18 @@ const GlossaryTermsV1 = ({
const addReviewerButton = () => {
return (
<NonAdminAction position="bottom" title={TITLE_FOR_NON_ADMIN_ACTION}>
<button
className="tw-text-primary tw-flex tw-items-center"
<Tooltip
placement="topRight"
title={permissions.EditAll ? 'Add Reviewer' : NO_PERMISSION_FOR_ACTION}>
<Button
className="tw-p-0"
data-testid="add-new-reviewer"
disabled={isHasAccess}
disabled={!permissions.EditAll}
type="text"
onClick={() => setShowRevieweModal(true)}>
<SVGIcons alt="edit" icon={Icons.EDIT} title="Edit" width="16px" />
</button>
</NonAdminAction>
);
};
const addButton = (onClick: () => void) => {
return (
<NonAdminAction position="bottom" title={TITLE_FOR_NON_ADMIN_ACTION}>
<span
className="tw-cursor-pointer"
data-testid="add-button"
onClick={onClick}>
<SVGIcons alt="icon-plus-primary" icon="icon-plus-primary-outlined" />
</span>
</NonAdminAction>
</Button>
</Tooltip>
);
};
@ -398,9 +372,7 @@ const GlossaryTermsV1 = ({
<span>{getEntityName(term)}</span>
</div>
<span>
<NonAdminAction
html={<p>{TITLE_FOR_NON_OWNER_ACTION}</p>}
position="bottom">
<Button disabled={!permissions.EditAll} type="text">
<span
className={classNames('tw-h-8 tw-rounded tw-mb-3')}
data-testid="remove"
@ -410,7 +382,7 @@ const GlossaryTermsV1 = ({
icon="remove"
/>
</span>
</NonAdminAction>
</Button>
</span>
</div>
))}
@ -437,34 +409,6 @@ const GlossaryTermsV1 = ({
);
};
const SummaryDetail = ({
title,
children,
setShow,
data,
...props
}: SummaryDetailsProps) => {
return (
<Space direction="vertical" {...props}>
<Space>
<Text type="secondary">{title}</Text>
<div className="tw-ml-2" data-testid={`section-${kebabCase(title)}`}>
{addButton(() => setShow && setShow(true))}
</div>
</Space>
{!isString(data) && !isUndefined(data) && data.length > 0 ? (
<div
className="tw-flex"
data-testid={`${kebabCase(title)}-container`}>
{children}
</div>
) : (
<div data-testid={`${kebabCase(title)}-container`}>{children}</div>
)}
</Space>
);
};
const SummaryTab = () => {
return (
<Row gutter={16}>
@ -482,6 +426,7 @@ const GlossaryTermsV1 = ({
<Divider className="m-r-1" />
<SummaryDetail
data={relatedTerms}
hasAccess={permissions.EditAll}
key="related_term"
setShow={setShowRelatedTermsModal}
title="Related Terms">
@ -510,6 +455,7 @@ const GlossaryTermsV1 = ({
<Divider className="m-r-1" />
<SummaryDetail
hasAccess={permissions.EditAll}
key="synonyms"
setShow={setIsSynonymsEditing}
title="Synonyms">
@ -558,6 +504,7 @@ const GlossaryTermsV1 = ({
<SummaryDetail
data={references}
hasAccess={permissions.EditAll}
key="references"
setShow={setIsReferencesEditing}
title="References">
@ -629,50 +576,49 @@ const GlossaryTermsV1 = ({
)}
</>
)}
<NonAdminAction
position="bottom"
title={TITLE_FOR_NON_ADMIN_ACTION}
trigger="click">
<div className="tw-inline-block" onClick={handleTagContainerClick}>
<TagsContainer
buttonContainerClass="tw--mt-0"
containerClass="tw-flex tw-items-center tw-gap-2"
dropDownHorzPosRight={false}
editable={isTagEditable}
isLoading={isTagLoading}
selectedTags={getSelectedTags()}
showTags={false}
size="small"
tagList={getTagOptionsFromFQN(tagList)}
type="label"
onCancel={() => {
handleTagSelection();
}}
onSelectionChange={(tags) => {
handleTagSelection(tags);
}}>
{glossaryTerm?.tags && glossaryTerm?.tags.length ? (
<button className="tw-ml-1 focus:tw-outline-none">
<SVGIcons
alt="edit"
icon="icon-edit"
title="Edit"
width="16px"
/>
</button>
) : (
<span>
<Tags
className="tw-text-primary"
startWith="+ "
tag="Add tag"
type="label"
/>
</span>
)}
</TagsContainer>
</div>
</NonAdminAction>
<div className="tw-inline-block" onClick={handleTagContainerClick}>
<TagsContainer
buttonContainerClass="tw--mt-0"
containerClass="tw-flex tw-items-center tw-gap-2"
dropDownHorzPosRight={false}
editable={isTagEditable}
isLoading={isTagLoading}
selectedTags={getSelectedTags()}
showTags={false}
size="small"
tagList={getTagOptionsFromFQN(tagList)}
type="label"
onCancel={() => {
handleTagSelection();
}}
onSelectionChange={(tags) => {
handleTagSelection(tags);
}}>
{glossaryTerm?.tags && glossaryTerm?.tags.length ? (
<button className="tw-ml-1 focus:tw-outline-none">
<SVGIcons
alt="edit"
icon="icon-edit"
title="Edit"
width="16px"
/>
</button>
) : (
<Button
className="tw-p-0"
disabled={!permissions.EditTags}
type="text">
<Tags
className="tw-text-primary"
startWith="+ "
tag="Add tag"
type="label"
/>
</Button>
)}
</TagsContainer>
</div>
</div>
<div className="tw-flex tw-flex-col tw-flex-grow">

View File

@ -0,0 +1,57 @@
import { Button, Space, Tooltip, Typography } from 'antd';
import { isString, isUndefined, kebabCase } from 'lodash';
import { FormattedGlossaryTermData } from 'Models';
import React from 'react';
import { NO_PERMISSION_FOR_ACTION } from '../../constants/HelperTextUtil';
import { TermReference } from '../../generated/entity/data/glossaryTerm';
import SVGIcons from '../../utils/SvgUtils';
interface SummaryDetailsProps {
title: string;
children: React.ReactElement;
hasAccess: boolean;
setShow?: (value: React.SetStateAction<boolean>) => void;
data?: FormattedGlossaryTermData[] | TermReference[] | string;
}
const SummaryDetail = ({
title,
children,
setShow,
data,
hasAccess,
...props
}: SummaryDetailsProps) => {
return (
<Space direction="vertical" {...props}>
<Space>
<Typography.Text type="secondary">{title}</Typography.Text>
<div className="tw-ml-2" data-testid={`section-${kebabCase(title)}`}>
<Tooltip title={hasAccess ? 'Add' : NO_PERMISSION_FOR_ACTION}>
<Button
className="tw-cursor-pointer"
data-testid="add-button"
disabled={!hasAccess}
size="small"
type="text"
onClick={() => setShow && setShow(true)}>
<SVGIcons
alt="icon-plus-primary"
icon="icon-plus-primary-outlined"
/>
</Button>
</Tooltip>
</div>
</Space>
{!isString(data) && !isUndefined(data) && data.length > 0 ? (
<div className="tw-flex" data-testid={`${kebabCase(title)}-container`}>
{children}
</div>
) : (
<div data-testid={`${kebabCase(title)}-container`}>{children}</div>
)}
</Space>
);
};
export default SummaryDetail;

View File

@ -11,19 +11,17 @@
* limitations under the License.
*/
import { Space, Typography } from 'antd';
import { Space, Tooltip, Typography } from 'antd';
import classNames from 'classnames';
import { isUndefined } from 'lodash';
import { EntityFieldThreads } from 'Models';
import React, { Fragment } from 'react';
import { EntityField } from '../../../constants/feed.constants';
import { NO_PERMISSION_FOR_ACTION } from '../../../constants/HelperTextUtil';
import { Table } from '../../../generated/entity/data/table';
import { Operation } from '../../../generated/entity/policies/accessControl/rule';
import { getHtmlForNonAdminAction } from '../../../utils/CommonUtils';
import { getEntityFeedLink } from '../../../utils/EntityUtils';
import SVGIcons, { Icons } from '../../../utils/SvgUtils';
import { ModalWithMarkdownEditor } from '../../Modals/ModalWithMarkdownEditor/ModalWithMarkdownEditor';
import NonAdminAction from '../non-admin-action/NonAdminAction';
import PopOver from '../popover/PopOver';
import RichTextEditorPreviewer from '../rich-text-editor/RichTextEditorPreviewer';
const { Text } = Typography;
@ -47,7 +45,6 @@ interface Props {
onEntityFieldSelect?: (value: string) => void;
}
const DescriptionV1 = ({
owner,
hasEditAccess,
onDescriptionEdit,
description = '',
@ -67,14 +64,12 @@ const DescriptionV1 = ({
const editButton = () => {
return !isReadOnly ? (
<NonAdminAction
html={getHtmlForNonAdminAction(Boolean(owner))}
isOwner={hasEditAccess}
permission={Operation.EditDescription}
position="right">
<Tooltip
title={hasEditAccess ? 'Edit Description' : NO_PERMISSION_FOR_ACTION}>
<button
className="focus:tw-outline-none tw-text-primary"
data-testid="edit-description"
disabled={!hasEditAccess}
onClick={onDescriptionEdit}>
<SVGIcons
alt="edit"
@ -83,7 +78,7 @@ const DescriptionV1 = ({
width="16px"
/>
</button>
</NonAdminAction>
</Tooltip>
) : (
<></>
);

View File

@ -23,7 +23,6 @@ import {
} from 'Models';
import React, { useCallback, useEffect, useState } from 'react';
import { useHistory, useParams } from 'react-router-dom';
import { useAuthContext } from '../../authentication/auth-provider/AuthProvider';
import {
deleteGlossary,
deleteGlossaryTerm,
@ -42,7 +41,6 @@ import { myDataSearchIndex } from '../../constants/Mydata.constants';
import { SearchIndex } from '../../enums/search.enum';
import { Glossary } from '../../generated/entity/data/glossary';
import { GlossaryTerm } from '../../generated/entity/data/glossaryTerm';
import { useAuth } from '../../hooks/authHooks';
import jsonData from '../../jsons/en';
import { formatDataResponse } from '../../utils/APIUtils';
import {
@ -66,8 +64,6 @@ export type ModifiedGlossaryData = Glossary & {
const GlossaryPageV1 = () => {
const { glossaryName } = useParams<Record<string, string>>();
const { isAdminUser } = useAuth();
const { isAuthDisabled } = useAuthContext();
const history = useHistory();
const [isLoading, setIsLoading] = useState<boolean>(true);
const [isChildLoading, setIsChildLoading] = useState(true);
@ -724,7 +720,6 @@ const GlossaryPageV1 = () => {
handleUserRedirection={handleUserRedirection}
isChildLoading={isChildLoading}
isGlossaryActive={isGlossaryActive}
isHasAccess={!isAdminUser && !isAuthDisabled}
isSearchResultEmpty={isSearchResultEmpty}
loadingKey={loadingKey}
searchText={searchText}

View File

@ -12,7 +12,7 @@
*/
import { isEmpty } from 'lodash';
import React, { FunctionComponent } from 'react';
import React, { FunctionComponent, useMemo } from 'react';
import { Redirect, Route, Switch } from 'react-router-dom';
import AppState from '../AppState';
import { usePermissionProvider } from '../components/PermissionProvider/PermissionProvider';
@ -212,6 +212,32 @@ const EditRulePage = withSuspenseFallback(
const AuthenticatedAppRouter: FunctionComponent = () => {
const { permissions } = usePermissionProvider();
const glossaryPermission = useMemo(
() =>
checkPermission(Operation.ViewAll, ResourceEntity.GLOSSARY, permissions),
[permissions]
);
const glossaryTermPermission = useMemo(
() =>
checkPermission(
Operation.ViewAll,
ResourceEntity.GLOSSARY_TERM,
permissions
),
[permissions]
);
const tagCategoryPermission = useMemo(
() =>
checkPermission(
Operation.ViewAll,
ResourceEntity.TAG_CATEGORY,
permissions
),
[permissions]
);
return (
<Switch>
<Route exact component={MyDataPage} path={ROUTES.MY_DATA} />
@ -251,8 +277,18 @@ const AuthenticatedAppRouter: FunctionComponent = () => {
{!isEmpty(AppState.userDetails) && <Redirect to={ROUTES.HOME} />}
</Route>
<Route exact component={SwaggerPage} path={ROUTES.SWAGGER} />
<Route exact component={TagsPage} path={ROUTES.TAGS} />
<Route exact component={TagsPage} path={ROUTES.TAG_DETAILS} />
<AdminProtectedRoute
exact
component={TagsPage}
hasPermission={tagCategoryPermission}
path={ROUTES.TAGS}
/>
<AdminProtectedRoute
exact
component={TagsPage}
hasPermission={tagCategoryPermission}
path={ROUTES.TAG_DETAILS}
/>
<Route exact component={DatabaseDetails} path={ROUTES.DATABASE_DETAILS} />
<Route
exact
@ -303,9 +339,24 @@ const AuthenticatedAppRouter: FunctionComponent = () => {
/>
<Route exact component={EntityVersionPage} path={ROUTES.ENTITY_VERSION} />
<Route exact component={EditWebhookPage} path={ROUTES.EDIT_WEBHOOK} />
<Route exact component={GlossaryPageV1} path={ROUTES.GLOSSARY} />
<Route exact component={GlossaryPageV1} path={ROUTES.GLOSSARY_DETAILS} />
<Route exact component={GlossaryPageV1} path={ROUTES.GLOSSARY_TERMS} />
<AdminProtectedRoute
exact
component={GlossaryPageV1}
hasPermission={glossaryPermission}
path={ROUTES.GLOSSARY}
/>
<AdminProtectedRoute
exact
component={GlossaryPageV1}
hasPermission={glossaryPermission}
path={ROUTES.GLOSSARY_DETAILS}
/>
<AdminProtectedRoute
exact
component={GlossaryPageV1}
hasPermission={glossaryTermPermission}
path={ROUTES.GLOSSARY_TERMS}
/>
<Route exact component={UserPage} path={ROUTES.USER_PROFILE} />
<Route exact component={UserPage} path={ROUTES.USER_PROFILE_WITH_TAB} />
<Route exact component={MlModelPage} path={ROUTES.MLMODEL_DETAILS} />